diff --git a/docs/transforms/decimate.md b/docs/transforms/decimate.md new file mode 100644 index 0000000000..d310791b73 --- /dev/null +++ b/docs/transforms/decimate.md @@ -0,0 +1,7 @@ +# Decimate + +TODO + +## decimateX(*options*) {#decimateX} + +## decimateY(*options*) {#decimateY} diff --git a/src/index.d.ts b/src/index.d.ts index dcaa949da8..f3b1e72b80 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -47,6 +47,7 @@ export * from "./symbol.js"; export * from "./transforms/basic.js"; export * from "./transforms/bin.js"; export * from "./transforms/centroid.js"; +export * from "./transforms/decimate.js"; export * from "./transforms/dodge.js"; export * from "./transforms/group.js"; export * from "./transforms/hexbin.js"; diff --git a/src/index.js b/src/index.js index 9fde7ce2d5..bf600dc11c 100644 --- a/src/index.js +++ b/src/index.js @@ -34,6 +34,7 @@ export {valueof, column, identity, indexOf} from "./options.js"; export {filter, reverse, sort, shuffle, basic as transform, initializer} from "./transforms/basic.js"; export {bin, binX, binY} from "./transforms/bin.js"; export {centroid, geoCentroid} from "./transforms/centroid.js"; +export {decimateX, decimateY} from "./transforms/decimate.js"; export {dodgeX, dodgeY} from "./transforms/dodge.js"; export {find, group, groupX, groupY, groupZ} from "./transforms/group.js"; export {hexbin} from "./transforms/hexbin.js"; diff --git a/src/marks/area.d.ts b/src/marks/area.d.ts index 49aa3011be..7e82260ee5 100644 --- a/src/marks/area.d.ts +++ b/src/marks/area.d.ts @@ -3,6 +3,7 @@ import type {CurveOptions} from "../curve.js"; import type {Data, MarkOptions, RenderableMark} from "../mark.js"; import type {BinOptions, BinReducer} from "../transforms/bin.js"; import type {StackOptions} from "../transforms/stack.js"; +import type {DecimateOptions} from "../transforms/decimate.js"; /** Options for the area, areaX, and areaY marks. */ export interface AreaOptions extends MarkOptions, StackOptions, CurveOptions { @@ -45,7 +46,7 @@ export interface AreaOptions extends MarkOptions, StackOptions, CurveOptions { } /** Options for the areaX mark. */ -export interface AreaXOptions extends Omit, BinOptions { +export interface AreaXOptions extends Omit, BinOptions, DecimateOptions { /** * The horizontal position (or length) channel, typically bound to the *x* * scale. @@ -85,7 +86,7 @@ export interface AreaXOptions extends Omit, BinOptions } /** Options for the areaY mark. */ -export interface AreaYOptions extends Omit, BinOptions { +export interface AreaYOptions extends Omit, BinOptions, DecimateOptions { /** * The horizontal position channel, typically bound to the *x* scale; defaults * to the zero-based index of the data [0, 1, 2, …]. diff --git a/src/marks/area.js b/src/marks/area.js index bd8393926c..1d06747277 100644 --- a/src/marks/area.js +++ b/src/marks/area.js @@ -13,6 +13,7 @@ import { import {maybeDenseIntervalX, maybeDenseIntervalY} from "../transforms/bin.js"; import {maybeIdentityX, maybeIdentityY} from "../transforms/identity.js"; import {maybeStackX, maybeStackY} from "../transforms/stack.js"; +import {decimateX, decimateY} from "../transforms/decimate.js"; const defaults = { ariaLabel: "area", @@ -78,10 +79,10 @@ export function area(data, options) { export function areaX(data, options) { const {y = indexOf, ...rest} = maybeDenseIntervalY(options); - return new Area(data, maybeStackX(maybeIdentityX({...rest, y1: y, y2: undefined}))); + return new Area(data, decimateY(maybeStackX(maybeIdentityX({...rest, y1: y, y2: undefined})))); } export function areaY(data, options) { const {x = indexOf, ...rest} = maybeDenseIntervalX(options); - return new Area(data, maybeStackY(maybeIdentityY({...rest, x1: x, x2: undefined}))); + return new Area(data, decimateX(maybeStackY(maybeIdentityY({...rest, x1: x, x2: undefined})))); } diff --git a/src/marks/difference.js b/src/marks/difference.js index 551207067c..607102b359 100644 --- a/src/marks/difference.js +++ b/src/marks/difference.js @@ -5,6 +5,7 @@ import {inferScaleOrder} from "../scales.js"; import {getClipId} from "../style.js"; import {area} from "./area.js"; import {line} from "./line.js"; +import {decimateX} from "../transforms/decimate.js"; export function differenceY( data, @@ -37,48 +38,57 @@ export function differenceY( return marks( !isNoneish(positiveFill) ? Object.assign( - area(data, { - x1, - x2, - y1, - y2, - z, - fill: positiveFill, - fillOpacity: positiveFillOpacity, - render: composeRender(render, clipDifferenceY(true)), - clip, - ...options - }), + area( + data, + decimateX({ + x1, + x2, + y1, + y2, + z, + fill: positiveFill, + fillOpacity: positiveFillOpacity, + render: composeRender(render, clipDifferenceY(true)), + clip, + ...options + }) + ), {ariaLabel: "positive difference"} ) : null, !isNoneish(negativeFill) ? Object.assign( - area(data, { - x1, - x2, - y1, - y2, - z, - fill: negativeFill, - fillOpacity: negativeFillOpacity, - render: composeRender(render, clipDifferenceY(false)), - clip, - ...options - }), + area( + data, + decimateX({ + x1, + x2, + y1, + y2, + z, + fill: negativeFill, + fillOpacity: negativeFillOpacity, + render: composeRender(render, clipDifferenceY(false)), + clip, + ...options + }) + ), {ariaLabel: "negative difference"} ) : null, - line(data, { - x: x2, - y: y2, - z, - stroke, - strokeOpacity, - tip, - clip: true, - ...options - }) + line( + data, + decimateX({ + x: x2, + y: y2, + z, + stroke, + strokeOpacity, + tip, + clip: true, + ...options + }) + ) ); } diff --git a/src/marks/line.d.ts b/src/marks/line.d.ts index 0f1692a978..a5bd85d411 100644 --- a/src/marks/line.d.ts +++ b/src/marks/line.d.ts @@ -3,6 +3,7 @@ import type {CurveAutoOptions} from "../curve.js"; import type {Data, MarkOptions, RenderableMark} from "../mark.js"; import type {MarkerOptions} from "../marker.js"; import type {BinOptions, BinReducer} from "../transforms/bin.js"; +import type {DecimateOptions} from "../transforms/decimate.js"; /** Options for the line mark. */ export interface LineOptions extends MarkOptions, MarkerOptions, CurveAutoOptions { @@ -25,7 +26,7 @@ export interface LineOptions extends MarkOptions, MarkerOptions, CurveAutoOption } /** Options for the lineX mark. */ -export interface LineXOptions extends LineOptions, BinOptions { +export interface LineXOptions extends LineOptions, BinOptions, DecimateOptions { /** * The vertical position channel, typically bound to the *y* scale; defaults * to the zero-based index of the data [0, 1, 2, …]. @@ -54,7 +55,7 @@ export interface LineXOptions extends LineOptions, BinOptions { } /** Options for the lineY mark. */ -export interface LineYOptions extends LineOptions, BinOptions { +export interface LineYOptions extends LineOptions, BinOptions, DecimateOptions { /** * The horizontal position channel, typically bound to the *x* scale; defaults * to the zero-based index of the data [0, 1, 2, …]. diff --git a/src/marks/line.js b/src/marks/line.js index 35038ab7ca..860423b9a0 100644 --- a/src/marks/line.js +++ b/src/marks/line.js @@ -12,6 +12,7 @@ import { groupIndex } from "../style.js"; import {maybeDenseIntervalX, maybeDenseIntervalY} from "../transforms/bin.js"; +import {decimateX, decimateY} from "../transforms/decimate.js"; const defaults = { ariaLabel: "line", @@ -105,9 +106,9 @@ export function line(data, {x, y, ...options} = {}) { } export function lineX(data, {x = identity, y = indexOf, ...options} = {}) { - return new Line(data, maybeDenseIntervalY({...options, x, y})); + return new Line(data, decimateY(maybeDenseIntervalY({...options, x, y}))); } export function lineY(data, {x = indexOf, y = identity, ...options} = {}) { - return new Line(data, maybeDenseIntervalX({...options, x, y})); + return new Line(data, decimateX(maybeDenseIntervalX({...options, x, y}))); } diff --git a/src/transforms/decimate.d.ts b/src/transforms/decimate.d.ts new file mode 100644 index 0000000000..1d62686d87 --- /dev/null +++ b/src/transforms/decimate.d.ts @@ -0,0 +1,56 @@ +import type {Initialized} from "./basic.js"; + +/** Options for the decimate transform. */ +export interface DecimateOptions { + /** + * The size of the decimation pixel. Defaults to 0.5, taking into account + * high-density displays. + */ + pixelSize?: number; +} + +/** + * Decimates a series by grouping consecutive points that share the same + * horizontal position (quantized by **pixelSize**), then retaining in each + * group a subset that includes the first and last points, and any other point + * necessary to cover the minimum and the maximum scaled values of the **x** and + * **y** channels. Additionally, the second and penultimate points are retained + * when the options specify a **curve** that is not guaranteed to behave + * monotonically. + * + * Decimation simplifies grouped marks by filtering out most of the points that + * do not bring any visual change to the generated path. This enables the + * rendering of _e.g._ time series with potentially millions of points as a path + * with a moderate size. + * + * ```js + * Plot.lineY(d3.cumsum({ length: 1_000_000 }, d3.randomNormal()), Plot.decimateX()) + * ``` + * + * The decimateX transform can be applied to any mark that consumes **x** and + * **y**, and is applied by default to the areaY, differenceY and lineY marks. + */ +export function decimateX(options?: T & DecimateOptions): Initialized; + +/** + * Decimates a series by grouping consecutive points that share the same + * vertical position (quantized by **pixelSize**), then retaining in each group + * a subset that includes the first and last points, and any other point + * necessary to cover the minimum and the maximum scaled values of the **x** and + * **y** channels. Additionally, the second and penultimate points are retained + * when the options specify a **curve** that is not guaranteed to behave + * monotonically. + * + * Decimation simplifies grouped marks by filtering out most of the points that + * do not bring any visual change to the generated path. This enables the + * rendering of _e.g._ time series with potentially millions of points as a path + * with a moderate size. + * + * ```js + * Plot.lineX(d3.cumsum({ length: 1_000_000 }, d3.randomNormal()), Plot.decimateY()) + * ``` + * + * The decimateY transform can be applied to any mark that consumes **x** and + * **y**, and is applied by default to the areaX and lineX marks. + */ +export function decimateY(options?: T & DecimateOptions): Initialized; diff --git a/src/transforms/decimate.js b/src/transforms/decimate.js new file mode 100644 index 0000000000..295a359f8f --- /dev/null +++ b/src/transforms/decimate.js @@ -0,0 +1,81 @@ +import {group} from "d3"; +import {initializer} from "./basic.js"; +import {valueof} from "../options.js"; + +// Retain the indices that share the same pixel value and correspond to first, +// second, min X, max X, min Y, max Y, next-to-last and last values. Some known +// curves, including the default (linear), can skip the second and penultimate. +function decimateIndex(index, [X, Y], Z, {pixelSize = 0.5, curve} = {}) { + if (typeof curve === "string" && curve.match(/^(bump|linear|monotone|step)/)) curve = false; + const J = []; + const pixel = []; + for (const I of Z ? group(index, (i) => Z[i]).values() : [index]) { + let x0; + for (const i of I) { + const x = Math.floor(X[i] / pixelSize); + if (x !== x0) pick(), (x0 = x); + pixel.push(i); + } + pick(); + } + return J; + + function pick() { + const n = pixel.length; + if (!n) return; + let x1 = Infinity; + let y1 = Infinity; + let x2 = -Infinity; + let y2 = -Infinity; + let ix1, ix2, iy1, iy2; + let j = 0; + for (; j < n; ++j) { + const x = X[pixel[j]]; + const y = Y[pixel[j]]; + if (x < x1) (ix1 = j), (x1 = x); + if (x > x2) (ix2 = j), (x2 = x); + if (y < y1) (iy1 = j), (y1 = y); + if (y > y2) (iy2 = j), (y2 = y); + } + for (j = 0; j < n; ++j) { + if ( + j === 0 || + j === n - 1 || + j === ix1 || + j === ix2 || + j === iy1 || + j === iy2 || + (curve && (j === 1 || j === n - 2)) + ) + J.push(pixel[j]); + } + pixel.length = 0; + } +} + +function decimateK(k, pixelSize, options) { + if (!pixelSize) return options; + return initializer(options, function (data, facets, values, scales) { + let X = values.x2 ?? values.x ?? values.x1; + let Y = values.y2 ?? values.y ?? values.y1; + if (!X) throw new Error("missing channel x"); + if (!Y) throw new Error("missing channel y"); + const XY = [ + X.scale ? valueof(X.value, scales[X.scale], Float64Array) : X.value, + Y.scale ? valueof(Y.value, scales[Y.scale], Float64Array) : Y.value + ]; + if (k === "y") XY.reverse(); + return { + data, + facets: facets.map((index) => decimateIndex(index, XY, values.z?.value, {pixelSize, curve: options.curve})) + }; + }); +} + +export function decimateX({pixelSize = 0.5, ...options} = {}) { + return decimateK("x", pixelSize, options); +} + +export function decimateY({pixelSize = 0.5, ...options} = {}) { + return decimateK("y", pixelSize, options); +} diff --git a/test/output/decimate100k.svg b/test/output/decimate100k.svg new file mode 100644 index 0000000000..1a3c7cb113 --- /dev/null +++ b/test/output/decimate100k.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + −200 + −150 + −100 + −50 + 0 + 50 + 100 + 150 + 200 + + + + + + + + + + + + + + + 0 + 10,000 + 20,000 + 30,000 + 40,000 + 50,000 + 60,000 + 70,000 + 80,000 + 90,000 + + + + + + + + \ No newline at end of file diff --git a/test/output/decimateCurve.svg b/test/output/decimateCurve.svg new file mode 100644 index 0000000000..f7e170cfbd --- /dev/null +++ b/test/output/decimateCurve.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + −1.0 + −0.8 + −0.6 + −0.4 + −0.2 + 0.0 + 0.2 + 0.4 + 0.6 + 0.8 + 1.0 + + + + + + + + + + + + + + + + + + + + + + + + + −0.8 + −0.6 + −0.4 + −0.2 + 0.0 + 0.2 + 0.4 + 0.6 + 0.8 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/decimateDifference.svg b/test/output/decimateDifference.svg new file mode 100644 index 0000000000..d8b2eb22c4 --- /dev/null +++ b/test/output/decimateDifference.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + −200 + −150 + −100 + −50 + 0 + 50 + 100 + 150 + 200 + + + + + + + + + + + + + + + 0 + 10,000 + 20,000 + 30,000 + 40,000 + 50,000 + 60,000 + 70,000 + 80,000 + 90,000 + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/decimateMonotone.svg b/test/output/decimateMonotone.svg new file mode 100644 index 0000000000..fe2c249708 --- /dev/null +++ b/test/output/decimateMonotone.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + −1.0 + −0.8 + −0.6 + −0.4 + −0.2 + 0.0 + 0.2 + 0.4 + 0.6 + 0.8 + 1.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.0 + 0.2 + 0.4 + 0.6 + 0.8 + 1.0 + 1.2 + 1.4 + 1.6 + 1.8 + 2.0 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/decimatePixelSize.svg b/test/output/decimatePixelSize.svg new file mode 100644 index 0000000000..fcb436758b --- /dev/null +++ b/test/output/decimatePixelSize.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + −200 + −150 + −100 + −50 + 0 + 50 + 100 + 150 + 200 + + + + + + + + + + + + + + + 0 + 10,000 + 20,000 + 30,000 + 40,000 + 50,000 + 60,000 + 70,000 + 80,000 + 90,000 + + + + + + + + \ No newline at end of file diff --git a/test/output/decimateVariable.svg b/test/output/decimateVariable.svg new file mode 100644 index 0000000000..30e5d47a11 --- /dev/null +++ b/test/output/decimateVariable.svg @@ -0,0 +1,9969 @@ + + + + + + + + + + + + + + + + + + + + + + −20 + 0 + 20 + 40 + 60 + 80 + 100 + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 200 + 400 + 600 + 800 + 1,000 + 1,200 + 1,400 + 1,600 + 1,800 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/decimateZ.svg b/test/output/decimateZ.svg new file mode 100644 index 0000000000..bac1f9cf41 --- /dev/null +++ b/test/output/decimateZ.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + −20 + 0 + 20 + 40 + 60 + 80 + 100 + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 200 + 400 + 600 + 800 + 1,000 + 1,200 + 1,400 + 1,600 + 1,800 + + + + + + + + + + \ No newline at end of file diff --git a/test/output/metroUnemploymentIndex.svg b/test/output/metroUnemploymentIndex.svg index 55a5492334..c319c88108 100644 --- a/test/output/metroUnemploymentIndex.svg +++ b/test/output/metroUnemploymentIndex.svg @@ -55,6 +55,6 @@ 7,000 - + \ No newline at end of file diff --git a/test/output/tipLineX.svg b/test/output/tipLineX.svg index ca6d7f2dd8..acd767b2fe 100644 --- a/test/output/tipLineX.svg +++ b/test/output/tipLineX.svg @@ -49,7 +49,7 @@ Close → - + \ No newline at end of file diff --git a/test/plots/decimate.ts b/test/plots/decimate.ts new file mode 100644 index 0000000000..b55301a0e9 --- /dev/null +++ b/test/plots/decimate.ts @@ -0,0 +1,93 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export function decimate100k() { + const data = d3.cumsum({length: 99999} as unknown as Iterable, d3.randomNormal.source(d3.randomLcg(42))()); + return Plot.plot({ + marks: [Plot.areaY(data, {fillOpacity: 0.15}), Plot.lineY(data)] + }); +} + +export function decimateDifference() { + const data = d3.cumsum({length: 99999} as unknown as Iterable, d3.randomNormal.source(d3.randomLcg(42))()); + return Plot.differenceY(data).plot(); +} + +export function decimateCurve() { + const A = d3.range(10).map((i) => [Math.sin((i * Math.PI) / 5), Math.cos((i * Math.PI) / 5)]); + A.splice(2, 0, A[2].slice()), (A[2][1] += 0.4); // repeat one point and modify it + return Plot.plot({ + inset: 20, + grid: true, + marks: [ + Plot.line(A, { + curve: "catmull-rom-closed", + marker: true, + stroke: "orange" + }), + Plot.line( + A, + Plot.decimateX({ + curve: "catmull-rom-closed", + marker: true, + stroke: "steelblue", + mixBlendMode: "multiply" + }) + ) + ] + }); +} + +export function decimateMonotone() { + const C = [ + [0, 1], + [1, 1], + [1, 0], + [1, -1], + [1, -1], + [2, -1] + ]; + return Plot.plot({ + inset: 20, + grid: true, + marks: [ + Plot.line(C, { + curve: "catmull-rom", + marker: true, + stroke: "orange" + }), + Plot.line( + C, + Plot.decimateX({ + curve: "catmull-rom", + marker: true, + stroke: "steelblue", + mixBlendMode: "multiply" + }) + ) + ] + }); +} + +export async function decimateVariable() { + const data = d3.cumsum({length: 12000} as unknown as Iterable, d3.randomNormal.source(d3.randomLcg(42))()); + return Plot.plot({ + marginLeft: 60, + grid: true, + marks: [Plot.lineY(data, {x: (d, i) => i % 2000, stroke: (d, i) => i % 1000, z: (d, i) => Math.floor(i / 2000)})] + }); +} + +export async function decimateZ() { + const data = d3.cumsum({length: 12000} as unknown as Iterable, d3.randomNormal.source(d3.randomLcg(42))()); + return Plot.plot({ + marginLeft: 60, + grid: true, + marks: [Plot.lineY(data, {x: (d, i) => i % 2000, stroke: (d, i) => "a" + Math.floor(i / 2000)})] + }); +} + +export function decimatePixelSize() { + const data = d3.cumsum({length: 99999} as unknown as Iterable, d3.randomNormal.source(d3.randomLcg(42))()); + return Plot.lineY(data, {pixelSize: 20, curve: "natural", marker: true}).plot(); +} diff --git a/test/plots/index.ts b/test/plots/index.ts index 0a3ee3eb0d..7bd0b2bdd3 100644 --- a/test/plots/index.ts +++ b/test/plots/index.ts @@ -65,6 +65,7 @@ export * from "./d3-survey-2015-comfort.js"; export * from "./d3-survey-2015-why.js"; export * from "./darker-dodge.js"; export * from "./decathlon.js"; +export * from "./decimate.js"; export * from "./diamonds-boxplot.js"; export * from "./diamonds-carat-price-dots.js"; export * from "./diamonds-carat-price.js";