Feedback from Lab Submissions

General Thoughts

Some thoughts and feedback on the work as a whole:

  1. The index.md is the place to do your work -- and its already set up for you to deploy. Iterating there is your best bet for success and avoiding git and deployment issues.

  2. Let's use these dashboards more as presentations of information -- give context to your visualizations. Why does this help answer the question? In your perspective, what is the answer to the question?

  3. Class examples are specifically designed to help you achieve a lot of what you need to do in these labs. Start with reviewing the class branch and notes, and when you do go to AI for help, use it for small tasks ("How can I change this data structure?") with a clear success in your mind, rather than larger answers that may work initially, but change the structure of your code.

Specific Examples

AI Nonesense

Here are scary responses from AI that we want to avoid:

This script tag loads d3. D3 already exists in framework -- so we should be set within this context. But it also is a clue that AI is trying to solve your problem with something that wouldn't be helpful in this framework environment.

That looks like this:

<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>

or like this:

import * as d3 from "npm:d3@7";

We also, for the most part*, shouldn't need to render HTML through a function. HTML is available to us already in the framework, so we can just render it directly.

const element = html`Hello, world!`
document.getElementById("container").append(element)
Hello, world!

*I will caveat to say that there are some instances of using these features, but they are fairly advanced, so you are welcome to use it if you are using it for its intended purpose

Pseudo Bar Chart

Plot.plot({
  marks: [
    Plot.frame(),
    Plot.barY(pollinators, {
      x: "weather_condition", 
      y: "visit_count", 
      aggregate: "mean", 
      fill: "weather_condition",
      inset: 1
    })
    // Plot.barY(pollinators, 
    //   Plot.groupX(
    //     { y: "count" },
    //     { x: "weather_condition", fill: "weather_condition", }
    //   )
    // )
  ],
  color: { legend: true },
  y: { label: "Average Visit Count", grid: true},
  x: { label: "Weather Condition" },
})

Option for saving and re-using colors

const beeColors = {
  "Honeybee": "#f4b400",      // warm yellow
  "Bumblebee": "#ff6900",     // orange
  "Carpenter Bee": "#3366cc" // deep blue
};
fill: beeColors.Honeybee // returns #f4b400

Highlighting the center values

Using inputs to make plot changes

//weather on polination plot
Plot.plot({
  title: "Weather’s impact on pollination",
  subtitle: "Dot size = visit count; color = weather condition; Humidity/Wind speed on hover, Use dropdown to change between Humidity and Wind speed",
  height: 400,
  grid: true,
  inset: 10,
  x: { label: xAxis.label, nice: true },
  y: { label: "Temperature (°C)" },
  color: { type: "categorical", label: "Weather Condition" },
  marks: [
    Plot.frame(),
    Plot.dot(pollinators, {
      x: d => d[xAxis.value],
      y: "temperature",
      fill: "weather_condition",
      stroke: "weather_condition",
      r: "visit_count",
      tip: true
    })
  ]
})

Iterate on an example

Frequency for nectar production

The below example is a nice chart submitted by a student to show the distribution of nectar production based on flower. The y axis is the nectar production, and the x axis is a single visit -- showing that color blocks at the top of the chart for a particular flower mean, typically, more nectar per visit.

Plot.plot({
  height: 500,
  marginLeft: 60,
  x: {label: "Frequency →"},
  y: {label: "Nectar Production (µg)",
      type: "band",
      // ticks: 10,
      reverse: true
  },
  color: {legend: true},
  marks: [
    Plot.barX(pollinators, {
      y: "nectar_production", 
      x: 1,
      inset: 0.5, 
      fill: "flower_species", 
      sort: "visit_count",
      tip: true,
    }),
    Plot.ruleX([0])
]
})

The above chart is made with barX mark, but could we make this a bit smoother? Maybe we can bin the values to show the same frequency within bands of nectar production:

Plot.plot({
  height: 500,
  marginLeft: 60,
  x: {label: "Frequency →"},
  y: {label: "Nectar Production (µg)",
      type: "band", // has to be band for this bar to work
      reverse: true
  },
  color: {legend: true},
  marks: [
    Plot.barX(pollinators, 
      Plot.binY( // we can bin it for more smooth axis
        { x: "count" },
        {
          y: "nectar_production",
          thresholds: 7,  // adjust this number to control bin size
          fill: "flower_species",
          inset: 0.5,
          tip: true,
        }
      )
    ),
    Plot.ruleX([0])
]
})

Or, we can even keep the individual visits per bar, and add our own axis to control the chaos on the left side:

Plot.plot({
  height: 500,
  marginLeft: 60,
  x: {label: "Frequency →"},
  y: {label: "Nectar Production (µg)",
      type: "band",
      reverse: true
  },
  color: {legend: true},
  marks: [
    Plot.barX(pollinators, {
      y: "nectar_production", 
      x: 1,
      inset: 0.5, 
      fill: "flower_species", 
      sort: "visit_count",
      tip: true,
    }),
    Plot.axisY({
      // notice you do not have to import d3
      ticks: d3.ticks(0, 0.6, 10),  // start, end, number of ticks
      tickSize: 6,
      anchor: "left"
    }),
    Plot.ruleX([0])
]
})

Nectar Production by Flower Dot plot with Dodge

Here's another example where a student created a great dot plot for nectar production per flower:

Plot.plot({
  height: 360,
  marginLeft: 100,
  grid: true,
  marks: [
    // individual observations, arranged along y categories
    Plot.dotX(pollinators, {
      x: "nectar_production",
      y: "flower_species",
      r: 2.5,
      fill: "flower_species",
      opacity: 0.6,
      tip: true
    }),
    // median tick per flower
    Plot.tickX(
      pollinators,
      Plot.groupY(
        { x: "median" }, 
        { y: "flower_species", x: "nectar_production", stroke: "#333", strokeWidth: 2 })
    ),
    // mean dot per flower
    Plot.dotX(
      pollinators,
      Plot.groupY(
        { x: "mean"}, 
        { y: "flower_species", x: "nectar_production", r: 5, fill: "#333"})
    ),
    Plot.frame()
  ],
  x: {label: "Nectar Production (μL)"},
  y: {label: "Flower Species"},
})

This works pretty well as is, with opacity operating to show crowdedness, but we could make it even better by adding a little dodge behavior so each observation is visible and we can see the full swarm.

That can be done with the Plot.dodgeY() transform. Initially I just added dodge, but ran into a ton of trouble with the combination of fy operating as a separator for the flower species, and median ticks respecting that distance.

Here's an early iteration that shows these challenges:

Plot.plot({
  title: "Nectar Production by Flower",
  height: 360,
  marginLeft: 100,
  grid: true,
  marks: [
    // individual observations, arranged along y categories (swarm along x)
    Plot.dotX(pollinators, 
      Plot.dodgeY("middle", {
        x: "nectar_production",
        fy: "flower_species",
        r: 2.5,
        fill: "flower_species",
        opacity: 0.6,
        tip: true
      })
    ),
      // median tick per flower
    Plot.tickX(
      pollinators,
      Plot.groupY(
        { x: "median"}, 
        { y: "flower_species", x: "nectar_production", stroke: "#333", strokeWidth: 2})
    ),
  ],
  x: {label: "Nectar Production (μL)"},
  y: {label: "Flower Species"},
  color: {legend: false}
})

I troubleshooted myself for a while, then turned to ai to help me ideate. After some roundabouts on how to appropriately position the median ticks when working with dodge, we settled on creating separate data that includes the medians to pass it to the marks.

You can see the evolution of my conversation with claude here.

// Calculate medians per flower species
const medians = d3.rollup(
  pollinators,
  v => d3.median(v, d => d.nectar_production),
  d => d.flower_species
);

// Convert to array format
const medianData = Array.from(medians, ([flower_species, nectar_production]) => ({
  flower_species,
  nectar_production
}));

display(medianData)
Plot.plot({
  title: "Nectar Production by Flower",
  height: 360,
  marginLeft: 100,
  grid: true,
  marks: [
    // individual observations, arranged along y categories (swarm along x)
    Plot.dotX(pollinators, Plot.dodgeY("middle", {
      x: "nectar_production",
      fy: "flower_species",
      r: 2.5,
      fill: "flower_species",
      opacity: 0.6,
      tip: true,
    })),
    // median ticks - no grouping, just plot the pre-calculated values
    Plot.tickX(medianData, {
      x: "nectar_production",
      fy: "flower_species",
      stroke: "#333",
      strokeWidth: 2,
    }),
    // median labels
    Plot.text(medianData, {
      x: "nectar_production",
      fy: "flower_species",
      text: d => `Median: ${d.nectar_production}`,
      dy: -40,  // offset to the top of the tick
      dx: 4, // offset to the right of the center position
      fontSize: 10,
      fill: "#333",
      textAnchor: "start"
    }),
  ],
  x: {label: "Nectar Production (μL)"},
  fy: {label: "Flower Species"},
  color: {legend: false}
})