Skip to content

Commit 7c8a465

Browse files
Filmbostock
andauthored
differenceX and shiftY (#1922)
* differenceX * shiftY * anchor * consolidate DifferenceOptions * update docs * update test snapshots * Update src/marks/difference.js --------- Co-authored-by: Mike Bostock <[email protected]>
1 parent 199b0da commit 7c8a465

File tree

11 files changed

+1537
-21
lines changed

11 files changed

+1537
-21
lines changed

Diff for: docs/marks/difference.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,12 @@ These options are passed to the underlying area and line marks; in particular, w
142142
Plot.differenceY(gistemp, {x: "Date", y: "Anomaly"})
143143
```
144144

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.
145+
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.
146+
147+
## differenceX(*data*, *options*) <VersionBadge pr="1922" /> {#differenceX}
148+
149+
```js
150+
Plot.differenceX(gistemp, {y: "Date", x: "Anomaly"})
151+
```
152+
153+
Returns a new horizontal difference with the given *data* and *options*. See [differenceY](#differenceY) for more.

Diff for: docs/transforms/shift.md

+12-2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ When looking at year-over-year growth, the chart is mostly green, implying that
4343
Plot.shiftX("7 days", {x: "Date", y: "Close"})
4444
```
4545

46-
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.
46+
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.
4747

48-
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.
48+
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.
49+
50+
## shiftY(*interval*, *options*) <VersionBadge pr="1922" /> {#shiftY}
51+
52+
```js
53+
Plot.shiftY("7 days", {y: "Date", x: "Close"})
54+
```
55+
56+
Derives a **y1** channel from the input **y** channel by shifting values by the given [*interval*](../features/intervals.md). See [shiftX](#shiftX) for more.
57+
58+
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 for: src/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export {Contour, contour} from "./marks/contour.js";
2020
export {crosshair, crosshairX, crosshairY} from "./marks/crosshair.js";
2121
export {delaunayLink, delaunayMesh, hull, voronoi, voronoiMesh} from "./marks/delaunay.js";
2222
export {Density, density} from "./marks/density.js";
23-
export {differenceY} from "./marks/difference.js";
23+
export {differenceX, differenceY} from "./marks/difference.js";
2424
export {Dot, dot, dotX, dotY, circle, hexagon} from "./marks/dot.js";
2525
export {Frame, frame} from "./marks/frame.js";
2626
export {Geo, geo, sphere, graticule} from "./marks/geo.js";
@@ -47,7 +47,7 @@ export {find, group, groupX, groupY, groupZ} from "./transforms/group.js";
4747
export {hexbin} from "./transforms/hexbin.js";
4848
export {normalize, normalizeX, normalizeY} from "./transforms/normalize.js";
4949
export {map, mapX, mapY} from "./transforms/map.js";
50-
export {shiftX} from "./transforms/shift.js";
50+
export {shiftX, shiftY} from "./transforms/shift.js";
5151
export {window, windowX, windowY} from "./transforms/window.js";
5252
export {select, selectFirst, selectLast, selectMaxX, selectMaxY, selectMinX, selectMinY} from "./transforms/select.js";
5353
export {stackX, stackX1, stackX2, stackY, stackY1, stackY2} from "./transforms/stack.js";

Diff for: src/marks/difference.d.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import type {Data, MarkOptions, RenderableMark} from "../mark.js";
66
export interface DifferenceOptions extends MarkOptions, CurveOptions {
77
/**
88
* The comparison horizontal position channel, typically bound to the *x*
9-
* scale; if not specified, **x** is used.
9+
* scale; if not specified, **x** is used. For differenceX, defaults to zero
10+
* if only one *x* and *y* channel is specified.
1011
*/
1112
x1?: ChannelValueSpec;
1213

@@ -69,6 +70,19 @@ export interface DifferenceOptions extends MarkOptions, CurveOptions {
6970
z?: ChannelValue;
7071
}
7172

73+
/**
74+
* Returns a new horizontal difference mark for the given the specified *data*
75+
* and *options*, as in a time-series chart where time goes down↓ (or up↑).
76+
*
77+
* The mark is a composite of a positive area, negative area, and line. The
78+
* positive area extends from the left of the frame to the line, and is clipped
79+
* by the area extending from the comparison to the right of the frame. The
80+
* negative area conversely extends from the right of the frame to the line, and
81+
* is clipped by the area extending from the comparison to the left of the
82+
* frame.
83+
*/
84+
export function differenceX(data?: Data, options?: DifferenceOptions): Difference;
85+
7286
/**
7387
* Returns a new vertical difference mark for the given the specified *data* and
7488
* *options*, as in a time-series chart where time goes right→ (or ←left).

Diff for: src/marks/difference.js

+32-15
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,24 @@ import {getClipId} from "../style.js";
66
import {area} from "./area.js";
77
import {line} from "./line.js";
88

9-
export function differenceY(
9+
export function differenceX(data, options) {
10+
return differenceK("x", data, options);
11+
}
12+
13+
export function differenceY(data, options) {
14+
return differenceK("y", data, options);
15+
}
16+
17+
function differenceK(
18+
k,
1019
data,
1120
{
1221
x1,
1322
x2,
1423
y1,
1524
y2,
16-
x = x1 === undefined && x2 === undefined ? indexOf : undefined,
17-
y = y1 === undefined && y2 === undefined ? identity : undefined,
25+
x = x1 === undefined && x2 === undefined ? (k === "y" ? indexOf : identity) : undefined,
26+
y = y1 === undefined && y2 === undefined ? (k === "x" ? indexOf : identity) : undefined,
1827
fill, // ignored
1928
positiveFill = "#3ca951",
2029
negativeFill = "#4269d0",
@@ -32,8 +41,11 @@ export function differenceY(
3241
) {
3342
[x1, x2] = memoTuple(x, x1, x2);
3443
[y1, y2] = memoTuple(y, y1, y2);
35-
if (x1 === x2 && y1 === y2) y1 = memo(0);
36-
({tip} = withTip({tip}, "x"));
44+
if (x1 === x2 && y1 === y2) {
45+
if (k === "y") y1 = memo(0);
46+
else x1 = memo(0);
47+
}
48+
({tip} = withTip({tip}, k === "y" ? "x" : "y"));
3749
return marks(
3850
!isNoneish(positiveFill)
3951
? Object.assign(
@@ -45,7 +57,7 @@ export function differenceY(
4557
z,
4658
fill: positiveFill,
4759
fillOpacity: positiveFillOpacity,
48-
render: composeRender(render, clipDifferenceY(true)),
60+
render: composeRender(render, clipDifference(k, true)),
4961
clip,
5062
...options
5163
}),
@@ -62,7 +74,7 @@ export function differenceY(
6274
z,
6375
fill: negativeFill,
6476
fillOpacity: negativeFillOpacity,
65-
render: composeRender(render, clipDifferenceY(false)),
77+
render: composeRender(render, clipDifference(k, false)),
6678
clip,
6779
...options
6880
}),
@@ -110,15 +122,20 @@ function memo(v) {
110122
return {transform: (data) => V || (V = valueof(data, value)), label};
111123
}
112124

113-
function clipDifferenceY(positive) {
125+
function clipDifference(k, positive) {
126+
const f = k === "x" ? "y" : "x"; // f is the flipped dimension
127+
const f1 = `${f}1`;
128+
const f2 = `${f}2`;
129+
const k1 = `${k}1`;
130+
const k2 = `${k}2`;
114131
return (index, scales, channels, dimensions, context, next) => {
115-
const {x1, x2} = channels;
116-
const {height} = dimensions;
117-
const y1 = new Float32Array(x1.length);
118-
const y2 = new Float32Array(x2.length);
119-
(positive === inferScaleOrder(scales.y) < 0 ? y1 : y2).fill(height);
120-
const oc = next(index, scales, {...channels, x2: x1, y2}, dimensions, context);
121-
const og = next(index, scales, {...channels, x1: x2, y1}, dimensions, context);
132+
const {[f1]: F1, [f2]: F2} = channels;
133+
const K1 = new Float32Array(F1.length);
134+
const K2 = new Float32Array(F2.length);
135+
const m = dimensions[k === "y" ? "height" : "width"];
136+
(positive === inferScaleOrder(scales[k]) < 0 ? K1 : K2).fill(m);
137+
const oc = next(index, scales, {...channels, [f2]: F1, [k2]: K2}, dimensions, context);
138+
const og = next(index, scales, {...channels, [f1]: F2, [k1]: K1}, dimensions, context);
122139
const c = oc.querySelector("g") ?? oc; // applyClip
123140
const g = og.querySelector("g") ?? og; // applyClip
124141
for (let i = 0; c.firstChild; i += 2) {

Diff for: src/transforms/shift.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,10 @@ import type {Transformed} from "./basic.js";
77
* *x* channel according to the specified *interval*.
88
*/
99
export function shiftX<T>(interval: Interval, options?: T): Transformed<T>;
10+
11+
/**
12+
* Groups data into series using the first channel of *z*, *fill*, or *stroke*
13+
* (if any), then derives *y1* and *y2* output channels by shifting the input
14+
* *y* channel according to the specified *interval*.
15+
*/
16+
export function shiftY<T>(interval: Interval, options?: T): Transformed<T>;

Diff for: src/transforms/shift.js

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ export function shiftX(interval, options) {
77
return shiftK("x", interval, options);
88
}
99

10+
export function shiftY(interval, options) {
11+
return shiftK("y", interval, options);
12+
}
13+
1014
function shiftK(x, interval, options = {}) {
1115
let offset;
1216
let k = 1;

0 commit comments

Comments
 (0)