|
| 1 | +--- |
| 2 | +title: "Intro to Observable Plot" |
| 3 | +toc: true |
| 4 | +--- |
| 5 | + |
| 6 | +# Intro to Observable Plot |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## What is Observable Plot? |
| 11 | + |
| 12 | +**Observable Plot** is a declarative JavaScript library for data visualization. In this course we use it for all charts. Unlike “chart type” libraries, but similar to the many recomendations of our readings, Plot is **marks-based**. That means you build a chart by combining **marks** (dots, bars, lines, areas, etc.) and binding them to data. There is no separate “scatter template” or “bar template”—you choose the marks and the scales do the rest. |
| 13 | + |
| 14 | +Note on using Plot in Framwork: Plot, similar to d3.js, is available globally in `.md` pages; you do not import it. |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## Plot.plot() |
| 19 | + |
| 20 | +Every chart is created with `Plot.plot(options)`. Even without options, an SVG is rendered. You may not see anything here, but there is an SVG rendered from the `Plot.plot()` code below. You can inspect the window and see an svg. |
| 21 | + |
| 22 | +```js echo |
| 23 | +Plot.plot() |
| 24 | +``` |
| 25 | + |
| 26 | +The most important option is **`marks`**: an array of marks drawn in order (last on top). Marks include things like dots, bars, lines, etc. |
| 27 | + |
| 28 | +Let's say our data is the following: |
| 29 | +```js echo |
| 30 | +const sample = [ |
| 31 | + { name: "A", value: 10, color: "blue" }, |
| 32 | + { name: "B", value: 25, color: "green" }, |
| 33 | + { name: "C", value: 15, color: "orange" }, |
| 34 | + { name: "D", value: 30, color: "red" } |
| 35 | +]; |
| 36 | +``` |
| 37 | + |
| 38 | +```js echo |
| 39 | +Plot.plot({ |
| 40 | + marginLeft: 50, |
| 41 | + title: "Sample Bar Chart", |
| 42 | + marks: [ |
| 43 | + Plot.barY(sample, { |
| 44 | + x: "name", |
| 45 | + y: "value", |
| 46 | + fill: "steelblue", |
| 47 | + tip: true |
| 48 | + }) |
| 49 | + ] |
| 50 | +}) |
| 51 | +``` |
| 52 | + |
| 53 | +Common top-level options include **`title`**, **`width`**, **`height`**, **`marginLeft`** (and other margins), **`grid: true`**, and **`marks`**. In Framework, you can pass the built-in **`width`** variable so the chart resizes with the page. |
| 54 | + |
| 55 | +--- |
| 56 | + |
| 57 | +## Marks |
| 58 | + |
| 59 | +Common helpful marks include: |
| 60 | + |
| 61 | +| Mark | Typical use | |
| 62 | +|------|-------------| |
| 63 | +| `Plot.dot()` | Scatter (one point per row) | |
| 64 | +| `Plot.barY()` / `Plot.barX()` | Bar charts | |
| 65 | +| `Plot.lineY()` / `Plot.lineX()` | Line / time series | |
| 66 | +| `Plot.areaY()` | Area under a line | |
| 67 | +| `Plot.rectY()` | Histograms (with a bin transform) | |
| 68 | +| `Plot.ruleY()` / `Plot.ruleX()` | Reference lines (e.g. at zero) | |
| 69 | +| `Plot.frame()` | Border around the plot | |
| 70 | + |
| 71 | +Marks take **data** and **options**. The structure is something like this: |
| 72 | + |
| 73 | +``` echo |
| 74 | +Plot.plot({ |
| 75 | + marks: [ |
| 76 | + Plot.[MARK]([DATA], [OPTIONS]) |
| 77 | + ] |
| 78 | +}) |
| 79 | +``` |
| 80 | +Each argument serves a specific purpose: |
| 81 | +1. Data - the data to be used to render the marks (e.g. `Plot.dot(sample, {...})`). |
| 82 | +2. Options - options about this specific mark, including channels and/or constants about how to tie the data to the mark. |
| 83 | + |
| 84 | +### Data join |
| 85 | + |
| 86 | +Marks within Plot leverage something we can call a **data join**: in many instances, one row in your data array becomes one graphical element (one dot, one bar, etc.). The mark function takes two arguments: |
| 87 | + |
| 88 | +The **channels** in the options tell Plot how to read each row, i.e., `x: "name"` means “use this row’s `name` for the x position,” and `y: "value"` means “use this row’s `value` for the y position.” |
| 89 | + |
| 90 | +So for `sample` with 4 rows, `Plot.dot(sample, { x: "name", y: "value" })` draws **4 dots**—one per row—with positions coming from those fields. |
| 91 | + |
| 92 | +Constants (e.g. `r: 6`) apply the same to every mark; channel names (e.g. `fill: "color"`) directly render the data points color as the background. |
| 93 | + |
| 94 | +```js echo |
| 95 | +Plot.plot({ |
| 96 | + grid: true, |
| 97 | + marks: [ |
| 98 | + Plot.ruleY([0], { stroke: "gray", strokeDasharray: "2 2" }), |
| 99 | + Plot.dot(sample, { |
| 100 | + x: "name", // ← which key to read for x positioning |
| 101 | + y: "value", // ← which key to read for y positioning |
| 102 | + r: 6, // ← set value for radius |
| 103 | + fill: "color", // ← which key to read for fill |
| 104 | + tip: true |
| 105 | + }) |
| 106 | + ] |
| 107 | +}) |
| 108 | +``` |
| 109 | + |
| 110 | +You can also use functions to pass values to the marks options. When using functions, it will run the function on the associated data that it is making a mark for. This allows you to manipulate values and how they should be rendered. |
| 111 | + |
| 112 | +```js echo |
| 113 | +Plot.plot({ |
| 114 | + grid: true, |
| 115 | + marks: [ |
| 116 | + Plot.ruleY([0], { stroke: "gray", strokeDasharray: "2 2" }), |
| 117 | + Plot.dot(sample, { |
| 118 | + x: (d, i) => d["name"] + " element", |
| 119 | + y: (d, i) => d["value"] * 100, |
| 120 | + r: (d, i) => 10 + i * 15, |
| 121 | + fill: (d, i) => ["red", "orange", "yellow", "green"][i], |
| 122 | + tip: true |
| 123 | + }) |
| 124 | + ] |
| 125 | +}) |
| 126 | +``` |
| 127 | + |
| 128 | +--- |
| 129 | + |
| 130 | +## Transforms |
| 131 | + |
| 132 | +Sometimes, data is not in the exact format that we may want when considering a visual. Let's take the (preloaded) penguins dataset. Say you want to count times a penguin is listed as each of the species. The dataset looks like this: |
| 133 | + |
| 134 | +```js |
| 135 | +Inputs.table(penguins) |
| 136 | +``` |
| 137 | + |
| 138 | +There is no column for "count". Each row is a penguin, with its own species designation, along with other metrics. We need to run a transformation on the data to determine how many penguins fit into each species. This could be done with Javascript, which we will get into more in future classes, but it also can be done within the Plot arguments with a **transform**. |
| 139 | + |
| 140 | +When simply counting rows to then plot into a bar chart, we transform with `groupX` within the `barY` mark. That is because we are transforming based on the x axis to some reduction which will be applied to the y axis. |
| 141 | + |
| 142 | +```js echo |
| 143 | +Plot.plot({ |
| 144 | + marks: [ |
| 145 | + Plot.barY(penguins, |
| 146 | + Plot.groupX( |
| 147 | + { y: "count" }, // ← this is the "reducer", or, what aggregation/function/etc to apply |
| 148 | + { x: "species" } // ← this is the pivot point for the reducer -- what are we counting on? |
| 149 | + ) |
| 150 | + ) |
| 151 | + ] |
| 152 | +}) |
| 153 | +``` |
| 154 | + |
| 155 | +There are a great many transforms you can do to the data, but the transform you use depends a lot on the data, the scales, and the intended marks. For a histogram, you **bin** the data and then draw **rects**. Use the **`Plot.binX()`** transform: it groups values into bins and can reduce with `y: "count"` (or `"sum"`, `"mean"`, etc.). |
| 156 | + |
| 157 | +```js echo |
| 158 | +const values = [3, 5, 2, 7, 4, 5, 4, 4, 6, 4, 5, 8, 4, 6]; |
| 159 | +const raw = values.map((v, i) => ({ id: i, value: v })); |
| 160 | +``` |
| 161 | + |
| 162 | +```js echo |
| 163 | +Plot.plot({ |
| 164 | + y: { grid: true }, |
| 165 | + marks: [ |
| 166 | + Plot.rectY(raw, Plot.binX( |
| 167 | + { y: "count" }, |
| 168 | + { x: "value", thresholds: 6 } |
| 169 | + )), |
| 170 | + Plot.ruleY([0]) |
| 171 | + ] |
| 172 | +}) |
| 173 | +``` |
| 174 | + |
| 175 | +--- |
| 176 | + |
| 177 | +## Plot tips |
| 178 | + |
| 179 | +- You can put `Plot.plot({...})` inside a **`${ ... }`** expression in your markdown or HTML so it renders inline (as in the examples above). If you use a code block, and create something else in the code block, don't forget to use `display` or `view` around your plot. |
| 180 | +- You can place plots inside **cards** or **grids** as in the [Intro to Observable Framework](/lessons/3_intro_to_observable_framework) lesson. |
| 181 | +- Use **`view()`** and **Inputs** to drive reactive variables (e.g. a selected category); use those variables in your data pipeline or in Plot options so the chart updates when the input changes. |
| 182 | + |
| 183 | +For more marks, options, and transforms, see the [Observable Plot documentation](https://observablehq.com/plot). |
0 commit comments