Skip to content

Commit

Permalink
differenceX and shiftY (#1922)
Browse files Browse the repository at this point in the history
* differenceX

* shiftY

* anchor

* consolidate DifferenceOptions

* update docs

* update test snapshots

* Update src/marks/difference.js

---------

Co-authored-by: Mike Bostock <mbostock@gmail.com>
  • Loading branch information
Fil and mbostock committed Jun 14, 2024
1 parent 199b0da commit 7c8a465
Show file tree
Hide file tree
Showing 11 changed files with 1,537 additions and 21 deletions.
10 changes: 9 additions & 1 deletion docs/marks/difference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*) <VersionBadge pr="1922" /> {#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.
14 changes: 12 additions & 2 deletions docs/transforms/shift.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*) <VersionBadge pr="1922" /> {#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.
4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down
16 changes: 15 additions & 1 deletion src/marks/difference.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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).
Expand Down
47 changes: 32 additions & 15 deletions src/marks/difference.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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(
Expand All @@ -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
}),
Expand All @@ -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
}),
Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 7 additions & 0 deletions src/transforms/shift.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,10 @@ import type {Transformed} from "./basic.js";
* *x* channel according to the specified *interval*.
*/
export function shiftX<T>(interval: Interval, options?: T): Transformed<T>;

/**
* 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<T>(interval: Interval, options?: T): Transformed<T>;
4 changes: 4 additions & 0 deletions src/transforms/shift.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 7c8a465

Please sign in to comment.