|
| 1 | +<script setup> |
| 2 | + |
| 3 | +import * as Plot from "@observablehq/plot"; |
| 4 | +import * as d3 from "d3"; |
| 5 | +import {computed, shallowRef, onMounted} from "vue"; |
| 6 | + |
| 7 | +const aapl = shallowRef([]); |
| 8 | +const gistemp = shallowRef([]); |
| 9 | +const tsa = shallowRef([{Date: new Date("2020-01-01")}]); |
| 10 | +const temperature = shallowRef([{date: new Date("2020-01-01")}]); |
| 11 | + |
| 12 | +onMounted(() => { |
| 13 | + d3.csv("../data/aapl.csv", d3.autoType).then((data) => (aapl.value = data)); |
| 14 | + d3.csv("../data/gistemp.csv", d3.autoType).then((data) => (gistemp.value = data)); |
| 15 | + d3.csv("../data/tsa.csv",d3.autoType).then((data) => (tsa.value = data)); |
| 16 | + d3.csv("../data/sf-sj-temperatures.csv", d3.autoType).then((data) => (temperature.value = data.filter((d) => d.date.getUTCFullYear() === 2020))); |
| 17 | +}); |
| 18 | + |
| 19 | +</script> |
| 20 | + |
| 21 | +# Difference mark <VersionBadge pr="1896" /> |
| 22 | + |
| 23 | +The **difference mark** puts a metric in context by comparing it. Like the [area mark](./area.md), the region between two lines is filled; unlike the area mark, alternating color shows when the metric is above or below the comparison value. |
| 24 | + |
| 25 | +In the simplest case, the difference mark compares a metric to a constant. For example, the plot below shows the [global surface temperature anomaly](https://data.giss.nasa.gov/gistemp/) from 1880–2016; 0° represents the 1951–1980 average; above-average temperatures are in <span style="border-bottom: solid var(--vp-c-red) 3px;">red</span>, while below-average temperatures are in <span style="border-bottom: solid var(--vp-c-blue) 3px;">blue</span>. (It’s getting hotter.) |
| 26 | + |
| 27 | +:::plot |
| 28 | +```js |
| 29 | +Plot.differenceY(gistemp, { |
| 30 | + x: "Date", |
| 31 | + y: "Anomaly", |
| 32 | + positiveFill: "red", |
| 33 | + negativeFill: "blue", |
| 34 | + tip: true |
| 35 | +}).plot({y: {grid: true}}) |
| 36 | +``` |
| 37 | +::: |
| 38 | + |
| 39 | +A 24-month [moving average](../transforms/window.md) improves readability by smoothing out the noise. |
| 40 | + |
| 41 | +:::plot |
| 42 | +```js |
| 43 | +Plot.differenceY( |
| 44 | + gistemp, |
| 45 | + Plot.windowY(12 * 2, { |
| 46 | + x: "Date", |
| 47 | + y: "Anomaly", |
| 48 | + positiveFill: "red", |
| 49 | + negativeFill: "blue", |
| 50 | + tip: true |
| 51 | + }) |
| 52 | +).plot({y: {grid: true}}) |
| 53 | +``` |
| 54 | +::: |
| 55 | + |
| 56 | +More powerfully, the difference mark compares two metrics. For example, the plot below shows the number of travelers per day through TSA checkpoints in 2020 compared to 2019. (This in effect compares a metric against itself, but as the data represents each year as a separate column, it is equivalent to two metrics.) In the first two months of 2020, there were on average <span style="border-bottom: solid #01ab63 3px;">more travelers</span> per day than 2019; yet when COVID-19 hit, there were many <span style="border-bottom: solid #4269d0 3px;">fewer travelers</span> per day, dropping almost to zero. |
| 57 | + |
| 58 | +:::plot |
| 59 | +```js |
| 60 | +Plot.plot({ |
| 61 | + x: {tickFormat: "%b"}, |
| 62 | + y: {grid: true, label: "Travelers"}, |
| 63 | + marks: [ |
| 64 | + Plot.axisY({label: "Travelers per day (thousands, 2020 vs. 2019)", tickFormat: (d) => d / 1000}), |
| 65 | + Plot.ruleY([0]), |
| 66 | + Plot.differenceY(tsa, {x: "Date", y1: "2019", y2: "2020", tip: {format: {x: "%B %-d"}}}) |
| 67 | + ] |
| 68 | +}) |
| 69 | +``` |
| 70 | +::: |
| 71 | + |
| 72 | +If the data is “tall” rather than “wide” — that is, if the two metrics we wish to compare are represented by separate *rows* rather than separate *columns* — we can use the [group transform](../transforms/group.md) with the [find reducer](../transforms/group.md#find): group the rows by **x** (date), then find the desired **y1** and **y2** for each group. The plot below shows daily minimum temperature for San Francisco compared to San Jose. Notice how the insulating fog keeps San Francisco <span style="border-bottom: solid #01ab63 3px;">warmer</span> in winter and <span style="border-bottom: solid #4269d0 3px;">cooler</span> in summer, reducing seasonal variation. |
| 73 | + |
| 74 | +:::plot |
| 75 | +```js |
| 76 | +Plot.plot({ |
| 77 | + x: {tickFormat: "%b"}, |
| 78 | + y: {grid: true}, |
| 79 | + marks: [ |
| 80 | + Plot.ruleY([32]), |
| 81 | + Plot.differenceY( |
| 82 | + temperature, |
| 83 | + Plot.windowY( |
| 84 | + 14, |
| 85 | + Plot.groupX( |
| 86 | + { |
| 87 | + y1: Plot.find((d) => d.station === "SJ"), |
| 88 | + y2: Plot.find((d) => d.station === "SF") |
| 89 | + }, |
| 90 | + { |
| 91 | + x: "date", |
| 92 | + y: "tmin", |
| 93 | + tip: true |
| 94 | + } |
| 95 | + ) |
| 96 | + ) |
| 97 | + ) |
| 98 | + ] |
| 99 | +}) |
| 100 | +``` |
| 101 | +::: |
| 102 | + |
| 103 | +The difference mark can also be used to compare a metric to itself using the [shift transform](../transforms/shift.md). The chart below shows year-over-year growth in the price of Apple stock. |
| 104 | + |
| 105 | +:::plot |
| 106 | +```js |
| 107 | +Plot.differenceY(aapl, Plot.shiftX("+1 year", {x: "Date", y: "Close"})).plot({y: {grid: true}}) |
| 108 | +``` |
| 109 | +::: |
| 110 | + |
| 111 | +For most of the covered time period, you would have <span style="border-bottom: solid #01ab63 3px;">made a profit</span> by holding Apple stock for a year; however, if you bought in 2015 and sold in 2016, you would likely have <span style="border-bottom: solid #4269d0 3px;">lost money</span>. |
| 112 | + |
| 113 | +## Difference options |
| 114 | + |
| 115 | +The following channels are required: |
| 116 | + |
| 117 | +* **x2** - the horizontal position of the metric; bound to the *x* scale |
| 118 | +* **y2** - the vertical position of the metric; bound to the *y* scale |
| 119 | + |
| 120 | +In addition to the [standard mark options](../features/marks.md#mark-options), the following optional channels are supported: |
| 121 | + |
| 122 | +* **x1** - the horizontal position of the comparison; bound to the *x* scale |
| 123 | +* **y1** - the vertical position of the comparison; bound to the *y* scale |
| 124 | + |
| 125 | +If **x1** is not specified, it defaults to **x2**. If **y1** is not specified, it defaults to 0 if **x1** and **x2** are equal, and to **y2** otherwise. These defaults facilitate sharing *x* or *y* coordinates between the metric and its comparison. |
| 126 | + |
| 127 | +The standard **fill** option is ignored; instead, there are separate channels based on the sign of the difference: |
| 128 | + |
| 129 | +* **positiveFill** - the color for when the metric is greater, defaults to <span style="border-bottom:solid #01ab63 3px;">green</span> |
| 130 | +* **negativeFill** - the color for when the comparison is greater, defaults to <span style="border-bottom:solid #4269d0 3px;">blue</span> |
| 131 | +* **fillOpacity** - the areas’ opacity, defaults to 1 |
| 132 | +* **positiveFillOpacity** - the positive area’s opacity, defaults to *opacity* |
| 133 | +* **negativeFillOpacity** - the negative area’s opacity, defaults to *opacity* |
| 134 | +* **stroke** - the metric line’s stroke color, defaults to currentColor |
| 135 | +* **strokeOpacity** - the metric line’s opacity, defaults to 1 |
| 136 | + |
| 137 | +These options are passed to the underlying area and line marks; in particular, when they are defined as a channel, the underlying marks are broken into contiguous overlapping segments when the values change. When any of these channels are used, setting an explicit **z** channel (possibly to null) is strongly recommended. |
| 138 | + |
| 139 | +## differenceY(*data*, *options*) {#differenceY} |
| 140 | + |
| 141 | +```js |
| 142 | +Plot.differenceY(gistemp, {x: "Date", y: "Anomaly"}) |
| 143 | +``` |
| 144 | + |
| 145 | +Returns a new difference with the given *data* and *options*. The mark is a composite of a positive area, negative area, and line. The positive area extends from the bottom of the frame to the line, and is clipped by the area extending from the comparison to the top of the frame. The negative area conversely extends from the top of the frame to the line, and is clipped by the area extending from the comparison to the bottom of the frame. |
0 commit comments