From 7c8a465d6492a7e637e30331072362cd8bf0ac7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 14 Jun 2024 07:11:19 +0200 Subject: [PATCH] differenceX and shiftY (#1922) * differenceX * shiftY * anchor * consolidate DifferenceOptions * update docs * update test snapshots * Update src/marks/difference.js --------- Co-authored-by: Mike Bostock --- docs/marks/difference.md | 10 +- docs/transforms/shift.md | 14 +- src/index.js | 4 +- src/marks/difference.d.ts | 16 +- src/marks/difference.js | 47 +- src/transforms/shift.d.ts | 7 + src/transforms/shift.js | 4 + test/output/differenceX.svg | 102 +++ test/output/shiftY.svg | 1341 +++++++++++++++++++++++++++++++++++ test/plots/difference.ts | 8 + test/plots/shift.ts | 5 + 11 files changed, 1537 insertions(+), 21 deletions(-) create mode 100644 test/output/differenceX.svg create mode 100644 test/output/shiftY.svg diff --git a/docs/marks/difference.md b/docs/marks/difference.md index f551e2d7ea..4ebde0a96c 100644 --- a/docs/marks/difference.md +++ b/docs/marks/difference.md @@ -142,4 +142,12 @@ These options are passed to the underlying area and line marks; in particular, w Plot.differenceY(gistemp, {x: "Date", y: "Anomaly"}) ``` -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. +Returns a new vertical 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. + +## differenceX(*data*, *options*) {#differenceX} + +```js +Plot.differenceX(gistemp, {y: "Date", x: "Anomaly"}) +``` + +Returns a new horizontal difference with the given *data* and *options*. See [differenceY](#differenceY) for more. diff --git a/docs/transforms/shift.md b/docs/transforms/shift.md index 297dbeb8b5..ae01113720 100644 --- a/docs/transforms/shift.md +++ b/docs/transforms/shift.md @@ -43,6 +43,16 @@ When looking at year-over-year growth, the chart is mostly green, implying that Plot.shiftX("7 days", {x: "Date", y: "Close"}) ``` -Derives an **x1** channel from the input **x** channel by shifting values by the given *interval*. The *interval* may be specified as: a name (*second*, *minute*, *hour*, *day*, *week*, *month*, *quarter*, *half*, *year*, *monday*, *tuesday*, *wednesday*, *thursday*, *friday*, *saturday*, *sunday*) with an optional number and sign (*e.g.*, *+3 days* or *-1 year*); or as a number; or as an implementation — such as d3.utcMonth — with *interval*.floor(*value*), *interval*.offset(*value*), and *interval*.range(*start*, *stop*) methods. +Derives an **x1** channel from the input **x** channel by shifting values by the given [*interval*](../features/intervals.md). The *interval* may be specified as: a name (*second*, *minute*, *hour*, *day*, *week*, *month*, *quarter*, *half*, *year*, *monday*, *tuesday*, *wednesday*, *thursday*, *friday*, *saturday*, *sunday*) with an optional number and sign (*e.g.*, *+3 days* or *-1 year*); or as a number; or as an implementation — such as d3.utcMonth — with *interval*.floor(*value*), *interval*.offset(*value*), and *interval*.range(*start*, *stop*) methods. -The shiftX also transform aliases the **x** channel to **x2** and applies a domain hint to the **x2** channel such that by default the plot shows only the intersection of **x1** and **x2**. For example, if the interval is *+1 year*, the first year of the data is not shown. +The shiftX transform also aliases the **x** channel to **x2** and applies a domain hint to the **x2** channel such that by default the plot shows only the intersection of **x1** and **x2**. For example, if the interval is *+1 year*, the first year of the data is not shown. + +## shiftY(*interval*, *options*) {#shiftY} + +```js +Plot.shiftY("7 days", {y: "Date", x: "Close"}) +``` + +Derives a **y1** channel from the input **y** channel by shifting values by the given [*interval*](../features/intervals.md). See [shiftX](#shiftX) for more. + +The shiftY transform also aliases the **y** channel to **y2** and applies a domain hint to the **y2** channel such that by default the plot shows only the intersection of **y1** and **y2**. For example, if the interval is *+1 year*, the first year of the data is not shown. diff --git a/src/index.js b/src/index.js index df33d998d4..4cca38b84f 100644 --- a/src/index.js +++ b/src/index.js @@ -20,7 +20,7 @@ export {Contour, contour} from "./marks/contour.js"; export {crosshair, crosshairX, crosshairY} from "./marks/crosshair.js"; export {delaunayLink, delaunayMesh, hull, voronoi, voronoiMesh} from "./marks/delaunay.js"; export {Density, density} from "./marks/density.js"; -export {differenceY} from "./marks/difference.js"; +export {differenceX, differenceY} from "./marks/difference.js"; export {Dot, dot, dotX, dotY, circle, hexagon} from "./marks/dot.js"; export {Frame, frame} from "./marks/frame.js"; export {Geo, geo, sphere, graticule} from "./marks/geo.js"; @@ -47,7 +47,7 @@ export {find, group, groupX, groupY, groupZ} from "./transforms/group.js"; export {hexbin} from "./transforms/hexbin.js"; export {normalize, normalizeX, normalizeY} from "./transforms/normalize.js"; export {map, mapX, mapY} from "./transforms/map.js"; -export {shiftX} from "./transforms/shift.js"; +export {shiftX, shiftY} from "./transforms/shift.js"; export {window, windowX, windowY} from "./transforms/window.js"; export {select, selectFirst, selectLast, selectMaxX, selectMaxY, selectMinX, selectMinY} from "./transforms/select.js"; export {stackX, stackX1, stackX2, stackY, stackY1, stackY2} from "./transforms/stack.js"; diff --git a/src/marks/difference.d.ts b/src/marks/difference.d.ts index 3e4b634cbb..eceda57bfd 100644 --- a/src/marks/difference.d.ts +++ b/src/marks/difference.d.ts @@ -6,7 +6,8 @@ import type {Data, MarkOptions, RenderableMark} from "../mark.js"; export interface DifferenceOptions extends MarkOptions, CurveOptions { /** * The comparison horizontal position channel, typically bound to the *x* - * scale; if not specified, **x** is used. + * scale; if not specified, **x** is used. For differenceX, defaults to zero + * if only one *x* and *y* channel is specified. */ x1?: ChannelValueSpec; @@ -69,6 +70,19 @@ export interface DifferenceOptions extends MarkOptions, CurveOptions { z?: ChannelValue; } +/** + * Returns a new horizontal difference mark for the given the specified *data* + * and *options*, as in a time-series chart where time goes down↓ (or up↑). + * + * The mark is a composite of a positive area, negative area, and line. The + * positive area extends from the left of the frame to the line, and is clipped + * by the area extending from the comparison to the right of the frame. The + * negative area conversely extends from the right of the frame to the line, and + * is clipped by the area extending from the comparison to the left of the + * frame. + */ +export function differenceX(data?: Data, options?: DifferenceOptions): Difference; + /** * Returns a new vertical difference mark for the given the specified *data* and * *options*, as in a time-series chart where time goes right→ (or ←left). diff --git a/src/marks/difference.js b/src/marks/difference.js index 551207067c..9e8bdb42c3 100644 --- a/src/marks/difference.js +++ b/src/marks/difference.js @@ -6,15 +6,24 @@ import {getClipId} from "../style.js"; import {area} from "./area.js"; import {line} from "./line.js"; -export function differenceY( +export function differenceX(data, options) { + return differenceK("x", data, options); +} + +export function differenceY(data, options) { + return differenceK("y", data, options); +} + +function differenceK( + k, data, { x1, x2, y1, y2, - x = x1 === undefined && x2 === undefined ? indexOf : undefined, - y = y1 === undefined && y2 === undefined ? identity : undefined, + x = x1 === undefined && x2 === undefined ? (k === "y" ? indexOf : identity) : undefined, + y = y1 === undefined && y2 === undefined ? (k === "x" ? indexOf : identity) : undefined, fill, // ignored positiveFill = "#3ca951", negativeFill = "#4269d0", @@ -32,8 +41,11 @@ export function differenceY( ) { [x1, x2] = memoTuple(x, x1, x2); [y1, y2] = memoTuple(y, y1, y2); - if (x1 === x2 && y1 === y2) y1 = memo(0); - ({tip} = withTip({tip}, "x")); + if (x1 === x2 && y1 === y2) { + if (k === "y") y1 = memo(0); + else x1 = memo(0); + } + ({tip} = withTip({tip}, k === "y" ? "x" : "y")); return marks( !isNoneish(positiveFill) ? Object.assign( @@ -45,7 +57,7 @@ export function differenceY( z, fill: positiveFill, fillOpacity: positiveFillOpacity, - render: composeRender(render, clipDifferenceY(true)), + render: composeRender(render, clipDifference(k, true)), clip, ...options }), @@ -62,7 +74,7 @@ export function differenceY( z, fill: negativeFill, fillOpacity: negativeFillOpacity, - render: composeRender(render, clipDifferenceY(false)), + render: composeRender(render, clipDifference(k, false)), clip, ...options }), @@ -110,15 +122,20 @@ function memo(v) { return {transform: (data) => V || (V = valueof(data, value)), label}; } -function clipDifferenceY(positive) { +function clipDifference(k, positive) { + const f = k === "x" ? "y" : "x"; // f is the flipped dimension + const f1 = `${f}1`; + const f2 = `${f}2`; + const k1 = `${k}1`; + const k2 = `${k}2`; return (index, scales, channels, dimensions, context, next) => { - const {x1, x2} = channels; - const {height} = dimensions; - const y1 = new Float32Array(x1.length); - const y2 = new Float32Array(x2.length); - (positive === inferScaleOrder(scales.y) < 0 ? y1 : y2).fill(height); - const oc = next(index, scales, {...channels, x2: x1, y2}, dimensions, context); - const og = next(index, scales, {...channels, x1: x2, y1}, dimensions, context); + const {[f1]: F1, [f2]: F2} = channels; + const K1 = new Float32Array(F1.length); + const K2 = new Float32Array(F2.length); + const m = dimensions[k === "y" ? "height" : "width"]; + (positive === inferScaleOrder(scales[k]) < 0 ? K1 : K2).fill(m); + const oc = next(index, scales, {...channels, [f2]: F1, [k2]: K2}, dimensions, context); + const og = next(index, scales, {...channels, [f1]: F2, [k1]: K1}, dimensions, context); const c = oc.querySelector("g") ?? oc; // applyClip const g = og.querySelector("g") ?? og; // applyClip for (let i = 0; c.firstChild; i += 2) { diff --git a/src/transforms/shift.d.ts b/src/transforms/shift.d.ts index 14327aadee..82848e7967 100644 --- a/src/transforms/shift.d.ts +++ b/src/transforms/shift.d.ts @@ -7,3 +7,10 @@ import type {Transformed} from "./basic.js"; * *x* channel according to the specified *interval*. */ export function shiftX(interval: Interval, options?: T): Transformed; + +/** + * Groups data into series using the first channel of *z*, *fill*, or *stroke* + * (if any), then derives *y1* and *y2* output channels by shifting the input + * *y* channel according to the specified *interval*. + */ +export function shiftY(interval: Interval, options?: T): Transformed; diff --git a/src/transforms/shift.js b/src/transforms/shift.js index 03f1c1171e..3027542404 100644 --- a/src/transforms/shift.js +++ b/src/transforms/shift.js @@ -7,6 +7,10 @@ export function shiftX(interval, options) { return shiftK("x", interval, options); } +export function shiftY(interval, options) { + return shiftK("y", interval, options); +} + function shiftK(x, interval, options = {}) { let offset; let k = 1; diff --git a/test/output/differenceX.svg b/test/output/differenceX.svg new file mode 100644 index 0000000000..a6fe21f02c --- /dev/null +++ b/test/output/differenceX.svg @@ -0,0 +1,102 @@ + + + + + 95 + 90 + 85 + 80 + 75 + 70 + 65 + 60 + 55 + 50 + 45 + 40 + 35 + 30 + 25 + 20 + 15 + 10 + 5 + 0 + + + + −10 + −8 + −6 + −4 + −2 + 0 + 2 + 4 + 6 + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/shiftY.svg b/test/output/shiftY.svg new file mode 100644 index 0000000000..d339505e4d --- /dev/null +++ b/test/output/shiftY.svg @@ -0,0 +1,1341 @@ + + + + + Oct2013 + Jan2014 + Apr + Jul + Oct + Jan2015 + Apr + Jul + Oct + Jan2016 + Apr + Jul + Oct + Jan2017 + Apr + Jul + Oct + Jan2018 + Apr + + + + 60 + 80 + 100 + 120 + 140 + 160 + 180 + + + Close → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/difference.ts b/test/plots/difference.ts index 5ee4ea3690..b2e4df40ba 100644 --- a/test/plots/difference.ts +++ b/test/plots/difference.ts @@ -189,3 +189,11 @@ export async function differenceFilterY2() { const y2 = aapl.map((d, i, data) => d.Close / data[0].Close); return Plot.differenceY(aapl, {x, y1, y2}).plot(); } + +export async function differenceX() { + const random = d3.randomNormal.source(d3.randomLcg(22))(); + return Plot.differenceX({length: 100}, Plot.mapX("cumsum", {x1: random, x2: random, curve: "basis"})).plot({ + height: 600, + y: {reverse: true} + }); +} diff --git a/test/plots/shift.ts b/test/plots/shift.ts index 3fb1bc61cc..97d6f920bb 100644 --- a/test/plots/shift.ts +++ b/test/plots/shift.ts @@ -5,3 +5,8 @@ export async function shiftX() { const aapl = await d3.csv("data/aapl.csv", d3.autoType); return Plot.arrow(aapl, Plot.shiftX("quarter", {x: "Date", y: "Close", bend: true})).plot(); } + +export async function shiftY() { + const aapl = await d3.csv("data/aapl.csv", d3.autoType); + return Plot.arrow(aapl, Plot.shiftY("quarter", {y: "Date", x: "Close", bend: true})).plot(); +}