From 487eaf327a52c8a46a0590dc74c347e563564577 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 30 Sep 2022 10:21:08 -0700 Subject: [PATCH 01/18] checkpoint facet expansion --- README.md | 32 ++++----- src/facet.js | 48 ++++++++++++++ src/marks/area.js | 4 +- src/marks/bar.js | 4 +- src/marks/rect.js | 4 +- src/transforms/stack.js | 125 ++++++++++++++++++++++++----------- test/output/stackExclude.svg | 68 +++++++++++++++++++ test/plots/index.js | 1 + test/plots/stack-exclude.js | 26 ++++++++ 9 files changed, 252 insertions(+), 60 deletions(-) create mode 100644 src/facet.js create mode 100644 test/output/stackExclude.svg create mode 100644 test/plots/stack-exclude.js diff --git a/README.md b/README.md index 16c0e4b641..da09b8574d 100644 --- a/README.md +++ b/README.md @@ -1038,7 +1038,7 @@ used when the baseline and topline share *y* values, as in a time-series area chart where time goes up↑. If neither the **x1** nor **x2** option is specified, the **x** option may be specified as shorthand to apply an implicit [stackX -transform](#plotstackxstack-options); +transform](#plotstackxstackoptions-options); this is the typical configuration for an area chart with a baseline at *x* = 0. If the **x** option is not specified, it defaults to the identity function. The **y** option specifies the **y1** channel; and the **y1** and @@ -1072,7 +1072,7 @@ used when the baseline and topline share *x* values, as in a time-series area chart where time goes right→. If neither the **y1** nor **y2** option is specified, the **y** option may be specified as shorthand to apply an implicit [stackY -transform](#plotstackystack-options); +transform](#plotstackystackoptions-options); this is the typical configuration for an area chart with a baseline at *y* = 0. If the **y** option is not specified, it defaults to the identity function. The **x** option specifies the **x1** channel; and the **x1** and @@ -1157,7 +1157,7 @@ following channels are required: If neither the **x1** nor **x2** option is specified, the **x** option may be specified as shorthand to apply an implicit [stackX -transform](#plotstackxstack-options); +transform](#plotstackxstackoptions-options); this is the typical configuration for a horizontal bar chart with bars aligned at *x* = 0. If the **x** option is not specified, it defaults to the identity function. If *options* is undefined, then it defaults to **x2** as @@ -1195,7 +1195,7 @@ following channels are required: If neither the **y1** nor **y2** option is specified, the **y** option may be specified as shorthand to apply an implicit [stackY -transform](#plotstackystack-options); +transform](#plotstackystackoptions-options); this is the typical configuration for a vertical bar chart with bars aligned at *y* = 0. If the **y** option is not specified, it defaults to the identity function. If *options* is undefined, then it defaults to **y2** as the @@ -1797,7 +1797,7 @@ Equivalent to [Plot.rect](#plotrectdata-options), except that if neither the **x1** nor **x2** option is specified, the **x** option may be specified as shorthand to apply an implicit [stackX -transform](#plotstackxstack-options); +transform](#plotstackxstackoptions-options); this is the typical configuration for a histogram with rects aligned at *x* = 0. If the **x** option is not specified, it defaults to the identity function. @@ -1814,7 +1814,7 @@ Equivalent to [Plot.rect](#plotrectdata-options), except that if neither the **y1** nor **y2** option is specified, the **y** option may be specified as shorthand to apply an implicit [stackY -transform](#plotstackystack-options); +transform](#plotstackystackoptions-options); this is the typical configuration for a histogram with rects aligned at *y* = 0. If the **y** option is not specified, it defaults to the identity function. @@ -2789,7 +2789,7 @@ If two arguments are passed to the stack transform functions below, the stack-sp -#### Plot.stackY(*stack*, *options*) +#### Plot.stackY(*stackOptions*, *options*) ```js Plot.stackY({x: "year", y: "revenue", z: "format", fill: "group"}) @@ -2806,35 +2806,35 @@ the only argument, or as a separate *stack* options argument. -#### Plot.stackY1(*stack*, *options*) +#### Plot.stackY1(*stackOptions*, *options*) ```js Plot.stackY1({x: "year", y: "revenue", z: "format", fill: "group"}) ``` Equivalent to -[Plot.stackY](#plotstackystack-options), +[Plot.stackY](#plotstackystackoptions-options), except that the **y1** channel is returned as the **y** channel. This can be used, for example, to draw a line at the bottom of each stacked area. -#### Plot.stackX(*stack*, *options*) +#### Plot.stackX(*stackOptions*, *options*) ```js Plot.stackX({y: "year", x: "revenue", z: "format", fill: "group"}) @@ -2846,28 +2846,28 @@ index, *x1*, *x2* and *x* as the output channels. -#### Plot.stackX1(*stack*, *options*) +#### Plot.stackX1(*stackOptions*, *options*) ```js Plot.stackX1({y: "year", x: "revenue", z: "format", fill: "group"}) ``` Equivalent to -[Plot.stackX](#plotstackxstack-options), +[Plot.stackX](#plotstackxstackoptions-options), except that the **x1** channel is returned as the **x** channel. This can be used, for example, to draw a line at the left edge of each stacked area. -#### Plot.stackX2(*stack*, *options*) +#### Plot.stackX2(*stackOptions*, *options*) ```js Plot.stackX2({y: "year", x: "revenue", z: "format", fill: "group"}) ``` Equivalent to -[Plot.stackX](#plotstackxstack-options), +[Plot.stackX](#plotstackxstackoptions-options), except that the **x2** channel is returned as the **x** channel. This can be used, for example, to draw a line at the right edge of each stacked area. diff --git a/src/facet.js b/src/facet.js new file mode 100644 index 0000000000..4cab8b5bd6 --- /dev/null +++ b/src/facet.js @@ -0,0 +1,48 @@ +import {slice} from "./options.js"; + +export function facetReindex(facets, n) { + const overlap = new Uint8Array(n); + let count = 0; + let plan; + + // Count the number of overlapping indexes across facets. + for (const facet of facets) { + for (const i of facet) { + if (overlap[i]) ++count; + overlap[i] = 1; + } + } + + // For each overlapping index (duplicate number), assign a new unique index at + // the end of the existing array. For example, [[0, 1, 2], [2, 1, 3]] would + // become [[0, 1, 2], [4, 5, 3]]. Attach a plan to the facets array, to be + // able to read the values associated with the old index in unaffected + // channels. + if (count > 0) { + facets = facets.map((facet) => slice(facet, Uint32Array)); + plan = new Uint32Array(n + count); + let j = 0; + for (; j < n; ++j) plan[j] = j; + overlap.fill(0); + for (const facet of facets) { + for (let k = 0; k < facet.length; ++k) { + const i = facet[k]; + if (overlap[i]) { + plan[j] = i; + facet[k] = j; + j++; + } + overlap[i] = 1; + } + } + } + + return {facets, plan}; +} + +export function maybeExpand(X, plan) { + if (!X || !plan) return X; + const V = new X.constructor(plan.length); + for (let i = 0; i < plan.length; ++i) V[i] = X[plan[i]]; + return V; +} diff --git a/src/marks/area.js b/src/marks/area.js index 1234b16e2c..be0bbc60af 100644 --- a/src/marks/area.js +++ b/src/marks/area.js @@ -101,7 +101,7 @@ export function area(data, options) { * chart where time goes up↑. If neither the **x1** nor **x2** option is * specified, the **x** option may be specified as shorthand to apply an * implicit [stackX - * transform](https://github.com/observablehq/plot/blob/main/README.md#plotstackxstack-options); + * transform](https://github.com/observablehq/plot/blob/main/README.md#plotstackxstackoptions-options); * this is the typical configuration for an area chart with a baseline at *x* = * 0. If the **x** option is not specified, it defaults to the identity * function. The **y** option specifies the **y1** channel; and the **y1** and @@ -137,7 +137,7 @@ export function areaX(data, options) { * chart where time goes right→. If neither the **y1** nor **y2** option is * specified, the **y** option may be specified as shorthand to apply an * implicit [stackY - * transform](https://github.com/observablehq/plot/blob/main/README.md#plotstackystack-options); + * transform](https://github.com/observablehq/plot/blob/main/README.md#plotstackystackoptions-options); * this is the typical configuration for an area chart with a baseline at *y* = * 0. If the **y** option is not specified, it defaults to the identity * function. The **x** option specifies the **x1** channel; and the **x1** and diff --git a/src/marks/bar.js b/src/marks/bar.js index 0a3874f981..46f83ece77 100644 --- a/src/marks/bar.js +++ b/src/marks/bar.js @@ -142,7 +142,7 @@ export class BarY extends AbstractBar { * * If neither the **x1** nor **x2** option is specified, the **x** option may be * specified as shorthand to apply an implicit [stackX - * transform](https://github.com/observablehq/plot/blob/main/README.md#plotstackxstack-options); + * transform](https://github.com/observablehq/plot/blob/main/README.md#plotstackxstackoptions-options); * this is the typical configuration for a horizontal bar chart with bars * aligned at *x* = 0. If the **x** option is not specified, it defaults to the * identity function. If *options* is undefined, then it defaults to **x2** as @@ -181,7 +181,7 @@ export function barX(data, options = {y: indexOf, x2: identity}) { * * If neither the **y1** nor **y2** option is specified, the **y** option may be * specified as shorthand to apply an implicit [stackY - * transform](https://github.com/observablehq/plot/blob/main/README.md#plotstackystack-options); + * transform](https://github.com/observablehq/plot/blob/main/README.md#plotstackystackoptions-options); * this is the typical configuration for a vertical bar chart with bars aligned * at *y* = 0. If the **y** option is not specified, it defaults to the identity * function. If *options* is undefined, then it defaults to **y2** as the diff --git a/src/marks/rect.js b/src/marks/rect.js index b7a18b0192..48acd7131f 100644 --- a/src/marks/rect.js +++ b/src/marks/rect.js @@ -108,7 +108,7 @@ export function rect(data, options) { * [Plot.rect](https://github.com/observablehq/plot/blob/main/README.md#plotrectdata-options), * except that if neither the **x1** nor **x2** option is specified, the **x** * option may be specified as shorthand to apply an implicit [stackX - * transform](https://github.com/observablehq/plot/blob/main/README.md#plotstackxstack-options); + * transform](https://github.com/observablehq/plot/blob/main/README.md#plotstackxstackoptions-options); * this is the typical configuration for a histogram with rects aligned at *x* = * 0. If the **x** option is not specified, it defaults to the identity * function. @@ -126,7 +126,7 @@ export function rectX(data, options = {y: indexOf, interval: 1, x2: identity}) { * [Plot.rect](https://github.com/observablehq/plot/blob/main/README.md#plotrectdata-options), * except that if neither the **y1** nor **y2** option is specified, the **y** * option may be specified as shorthand to apply an implicit [stackY - * transform](https://github.com/observablehq/plot/blob/main/README.md#plotstackystack-options); + * transform](https://github.com/observablehq/plot/blob/main/README.md#plotstackystackoptions-options); * this is the typical configuration for a histogram with rects aligned at *y* = * 0. If the **y** option is not specified, it defaults to the identity * function. diff --git a/src/transforms/stack.js b/src/transforms/stack.js index 7018ff5ce9..20c5a43d87 100644 --- a/src/transforms/stack.js +++ b/src/transforms/stack.js @@ -1,6 +1,8 @@ import {InternMap, cumsum, group, groupSort, greatest, max, min, rollup, sum} from "d3"; import {ascendingDefined} from "../defined.js"; -import {field, column, maybeColumn, maybeZ, mid, range, valueof, maybeZero, one} from "../options.js"; +import {maybeExpand, facetReindex} from "../facet.js"; +import {field, column, mid, range, valueof, one} from "../options.js"; +import {maybeColumn, maybeColorChannel, maybeZ, maybeZero} from "../options.js"; import {basic} from "./basic.js"; /** @@ -13,11 +15,11 @@ import {basic} from "./basic.js"; * * @link https://github.com/observablehq/plot/blob/main/README.md#stack */ -export function stackX(stack = {}, options = {}) { - if (arguments.length === 1) [stack, options] = mergeOptions(stack); +export function stackX(stackOptions = {}, options = {}) { + if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions); const {y1, y = y1, x, ...rest} = options; // note: consumes x! - const [transform, Y, x1, x2] = stackAlias(y, x, "x", stack, rest); - return {...transform, y1, y: Y, x1, x2, x: mid(x1, x2)}; + const [transform, Y, x1, x2, other] = stack(y, x, "x", stackOptions, rest); + return {...transform, y1, y: Y, x1, x2, ...other, x: mid(x1, x2)}; } /** @@ -26,17 +28,17 @@ export function stackX(stack = {}, options = {}) { * ``` * * Equivalent to - * [Plot.stackX](https://github.com/observablehq/plot/blob/main/README.md#plotstackxstack-options), + * [Plot.stackX](https://github.com/observablehq/plot/blob/main/README.md#plotstackxstackoptions-options), * except that the **x1** channel is returned as the **x** channel. This can be * used, for example, to draw a line at the left edge of each stacked area. * * @link https://github.com/observablehq/plot/blob/main/README.md#stack */ -export function stackX1(stack = {}, options = {}) { - if (arguments.length === 1) [stack, options] = mergeOptions(stack); +export function stackX1(stackOptions = {}, options = {}) { + if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions); const {y1, y = y1, x} = options; - const [transform, Y, X] = stackAlias(y, x, "x", stack, options); - return {...transform, y1, y: Y, x: X}; + const [transform, Y, X, , other] = stack(y, x, "x", stackOptions, options); + return {...transform, y1, y: Y, x: X, ...other}; } /** @@ -45,17 +47,17 @@ export function stackX1(stack = {}, options = {}) { * ``` * * Equivalent to - * [Plot.stackX](https://github.com/observablehq/plot/blob/main/README.md#plotstackxstack-options), + * [Plot.stackX](https://github.com/observablehq/plot/blob/main/README.md#plotstackxstackoptions-options), * except that the **x2** channel is returned as the **x** channel. This can be * used, for example, to draw a line at the right edge of each stacked area. * * @link https://github.com/observablehq/plot/blob/main/README.md#stack */ -export function stackX2(stack = {}, options = {}) { - if (arguments.length === 1) [stack, options] = mergeOptions(stack); +export function stackX2(stackOptions = {}, options = {}) { + if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions); const {y1, y = y1, x} = options; - const [transform, Y, , X] = stackAlias(y, x, "x", stack, options); - return {...transform, y1, y: Y, x: X}; + const [transform, Y, , X, other] = stack(y, x, "x", stackOptions, options); + return {...transform, y1, y: Y, x: X, ...other}; } /** @@ -73,11 +75,11 @@ export function stackX2(stack = {}, options = {}) { * * @link https://github.com/observablehq/plot/blob/main/README.md#stack */ -export function stackY(stack = {}, options = {}) { - if (arguments.length === 1) [stack, options] = mergeOptions(stack); +export function stackY(stackOptions = {}, options = {}) { + if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions); const {x1, x = x1, y, ...rest} = options; // note: consumes y! - const [transform, X, y1, y2] = stackAlias(x, y, "y", stack, rest); - return {...transform, x1, x: X, y1, y2, y: mid(y1, y2)}; + const [transform, X, y1, y2, other] = stack(x, y, "y", stackOptions, rest); + return {...transform, x1, x: X, y1, y2, y: mid(y1, y2), ...other}; } /** @@ -86,17 +88,17 @@ export function stackY(stack = {}, options = {}) { * ``` * * Equivalent to - * [Plot.stackY](https://github.com/observablehq/plot/blob/main/README.md#plotstackystack-options), + * [Plot.stackY](https://github.com/observablehq/plot/blob/main/README.md#plotstackystackoptions-options), * except that the **y1** channel is returned as the **y** channel. This can be * used, for example, to draw a line at the bottom of each stacked area. * * @link https://github.com/observablehq/plot/blob/main/README.md#stack */ -export function stackY1(stack = {}, options = {}) { - if (arguments.length === 1) [stack, options] = mergeOptions(stack); +export function stackY1(stackOptions = {}, options = {}) { + if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions); const {x1, x = x1, y} = options; - const [transform, X, Y] = stackAlias(x, y, "y", stack, options); - return {...transform, x1, x: X, y: Y}; + const [transform, X, Y, , other] = stack(x, y, "y", stackOptions, options); + return {...transform, x1, x: X, y: Y, ...other}; } /** @@ -105,17 +107,17 @@ export function stackY1(stack = {}, options = {}) { * ``` * * Equivalent to - * [Plot.stackY](https://github.com/observablehq/plot/blob/main/README.md#plotstackystack-options), + * [Plot.stackY](https://github.com/observablehq/plot/blob/main/README.md#plotstackystackoptions-options), * except that the **y2** channel is returned as the **y** channel. This can be * used, for example, to draw a line at the top of each stacked area. * * @link https://github.com/observablehq/plot/blob/main/README.md#stack */ -export function stackY2(stack = {}, options = {}) { - if (arguments.length === 1) [stack, options] = mergeOptions(stack); +export function stackY2(stackOptions = {}, options = {}) { + if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions); const {x1, x = x1, y} = options; - const [transform, X, , Y] = stackAlias(x, y, "y", stack, options); - return {...transform, x1, x: X, y: Y}; + const [transform, X, , Y, other] = stack(x, y, "y", stackOptions, options); + return {...transform, x1, x: X, y: Y, ...other}; } export function maybeStackX({x, x1, x2, ...options} = {}) { @@ -145,13 +147,62 @@ function stack(x, y = one, ky, {offset, order, reverse}, options) { const [Y2, setY2] = column(y); offset = maybeOffset(offset); order = maybeOrder(order, offset, ky); + + // TODO Here we will need to iterate over the options and pull out any that + // represent channels (columns of values). The list of possible channels for + // all Marks: + // - fill (color) + // - fillOpacity (opacity/number) + // - stroke (color) + // - strokeOpacity (opacity/number) + // - strokeWidth (number) + // - opacity (opacity/number) + // - title + // - href + // - ariaLabel + // - z? + // TODO + // - text (text) + // - rotate (text, dot) + // - fontSize (text) + // - symbol (dot) + // - r (dot) + // - length (vector) + // - width (image) + // - height (image) + // - src (image) + // - weight (density) + + const knownChannels = [ + ["fill", (value) => maybeColorChannel(value)[0]], + ["stroke", (value) => maybeColorChannel(value)[0]], + ["text"] + ]; + + const other = {}; + const outputs = []; + for (const [name, test = (value) => value] of knownChannels) { + const value = test(options[name]); + if (value) { + const [V, setV] = column(value); + other[name] = V; + outputs.push((data, plan) => setV(maybeExpand(valueof(data, value), plan))); + } + } + return [ basic(options, (data, facets) => { - const X = x == null ? undefined : setX(valueof(data, x)); - const Y = valueof(data, y, Float64Array); - const Z = valueof(data, z); - const O = order && order(data, X, Y, Z); - const n = data.length; + let plan; + ({facets, plan} = facetReindex(facets, data.length)); // make facets exclusive + for (const o of outputs) o(data, plan); // expand any extra channels + const XS = x == null ? undefined : valueof(data, x); + const YS = valueof(data, y, Float64Array); + const ZS = valueof(data, z); + const X = XS && setX(maybeExpand(XS, plan)); + const Y = maybeExpand(YS, plan); + const Z = maybeExpand(ZS, plan); + const O = order && maybeExpand(order(data, XS, YS, ZS), plan); + const n = plan ? plan.length : data.length; const Y1 = setY1(new Float64Array(n)); const Y2 = setY2(new Float64Array(n)); const facetstacks = []; @@ -176,13 +227,11 @@ function stack(x, y = one, ky, {offset, order, reverse}, options) { }), X, Y1, - Y2 + Y2, + other ]; } -// This is used internally so we can use `stack` as an argument name. -const stackAlias = stack; - function maybeOffset(offset) { if (offset == null) return; if (typeof offset === "function") return offset; diff --git a/test/output/stackExclude.svg b/test/output/stackExclude.svg new file mode 100644 index 0000000000..1631981e99 --- /dev/null +++ b/test/output/stackExclude.svg @@ -0,0 +1,68 @@ + + + + + 0 + + + 1 + + + 2 + + + 3 + + + 4 + + + 5 + + + + + a + + + b + + + c + + + + + + + + 12 + + + + + + + 02 + + + + + + + 01 + + \ No newline at end of file diff --git a/test/plots/index.js b/test/plots/index.js index 9a94cecfee..d04a95d329 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -209,6 +209,7 @@ export {default as softwareVersions} from "./software-versions.js"; export {default as sparseCell} from "./sparse-cell.js"; export {default as stackedBar} from "./stacked-bar.js"; export {default as stackedRect} from "./stacked-rect.js"; +export {default as stackExclude} from "./stack-exclude.js"; export {default as stargazers} from "./stargazers.js"; export {default as stargazersBinned} from "./stargazers-binned.js"; export {default as stargazersHourly} from "./stargazers-hourly.js"; diff --git a/test/plots/stack-exclude.js b/test/plots/stack-exclude.js new file mode 100644 index 0000000000..e2f2afdc81 --- /dev/null +++ b/test/plots/stack-exclude.js @@ -0,0 +1,26 @@ +import * as Plot from "@observablehq/plot"; + +export default async function () { + const data = Float64Array.of(1, 2, 3); + const facets = ["a", "b", "c"]; + return Plot.plot({ + height: 180, + facet: {data, x: facets}, + marks: [ + Plot.barY(data, { + stroke: (d) => d, // channel as accessor + fill: data, // channel as array + fillOpacity: 0.5, + facet: "exclude" + }), + Plot.textY( + data, + Plot.stackY({ + y: data, + text: (d, i) => i, // the original index + facet: "exclude" + }) + ) + ] + }); +} From 171e97b3c21f517fea6bc4a367226049b335c83c Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 30 Sep 2022 10:36:47 -0700 Subject: [PATCH 02/18] fix a test --- test/marks/bar-test.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/marks/bar-test.js b/test/marks/bar-test.js index 031228532f..e2a39ad82e 100644 --- a/test/marks/bar-test.js +++ b/test/marks/bar-test.js @@ -58,11 +58,12 @@ it("barX(data, {fill}) allows fill to be null", () => { }); it("barX(data, {fill}) allows fill to be a variable color", () => { - const bar = Plot.barX(undefined, {fill: "x"}); + const data = [{x: 1}, {x: 2}]; + const bar = Plot.barX(data, {fill: "x"}); assert.strictEqual(bar.fill, undefined); - const {fill} = bar.channels; - assert.strictEqual(fill.value, "x"); - assert.strictEqual(fill.scale, "color"); + const {channels} = bar.initialize([[0, 1]]); + assert.deepStrictEqual(channels.fill.value, [1, 2]); + assert.strictEqual(channels.fill.scale, "color"); }); it("barX(data, {stroke}) allows stroke to be a constant color", () => { From 301de0d59952126381b555a241baf312b914a10d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Tue, 4 Oct 2022 13:08:02 +0200 Subject: [PATCH 03/18] facet expansion --- src/facet.js | 50 +- src/marks/image.js | 2 +- src/marks/text.js | 2 +- src/transforms/bin.js | 16 +- src/transforms/dodge.js | 17 +- src/transforms/map.js | 12 +- src/transforms/stack.js | 62 +- test/marks/bar-test.js | 40 +- test/output/generativeRoses.svg | 296 + test/output/hexbinExclude.svg | 401 + test/output/musicRevenueBars.svg | 9986 +++++++++++++++++++++++++ test/output/musicRevenueBin.svg | 2318 ++++++ test/output/musicRevenueGroup.svg | 484 ++ test/output/musicRevenueWiggle.svg | 412 + test/output/penguinCumsumExclude.svg | 806 ++ test/output/penguinDodgeReindexed.svg | 746 ++ test/plots/generative-roses.js | 43 + test/plots/hexbin-exclude.js | 45 + test/plots/index.js | 12 +- test/plots/music-revenue-bars.js | 49 + test/plots/music-revenue-bin.js | 48 + test/plots/music-revenue-group.js | 35 + test/plots/music-revenue-wiggle.js | 40 + test/plots/penguin-cumsum-exclude.js | 37 + test/plots/penguin-dodge-reindexed.js | 11 + 25 files changed, 15881 insertions(+), 89 deletions(-) create mode 100644 test/output/generativeRoses.svg create mode 100644 test/output/hexbinExclude.svg create mode 100644 test/output/musicRevenueBars.svg create mode 100644 test/output/musicRevenueBin.svg create mode 100644 test/output/musicRevenueGroup.svg create mode 100644 test/output/musicRevenueWiggle.svg create mode 100644 test/output/penguinCumsumExclude.svg create mode 100644 test/output/penguinDodgeReindexed.svg create mode 100644 test/plots/generative-roses.js create mode 100644 test/plots/hexbin-exclude.js create mode 100644 test/plots/music-revenue-bars.js create mode 100644 test/plots/music-revenue-bin.js create mode 100644 test/plots/music-revenue-group.js create mode 100644 test/plots/music-revenue-wiggle.js create mode 100644 test/plots/penguin-cumsum-exclude.js create mode 100644 test/plots/penguin-dodge-reindexed.js diff --git a/src/facet.js b/src/facet.js index 4cab8b5bd6..930bd08809 100644 --- a/src/facet.js +++ b/src/facet.js @@ -1,4 +1,7 @@ -import {slice} from "./options.js"; +import {column, maybeColorChannel, maybeNumberChannel, slice, valueof} from "./options.js"; +import {maybeSymbolChannel} from "./symbols.js"; +import {maybeFontSizeChannel} from "./marks/text.js"; +import {maybePathChannel} from "./marks/image.js"; export function facetReindex(facets, n) { const overlap = new Uint8Array(n); @@ -8,6 +11,7 @@ export function facetReindex(facets, n) { // Count the number of overlapping indexes across facets. for (const facet of facets) { for (const i of facet) { + if (i >= n) return {facets}; // already dedup'ed! if (overlap[i]) ++count; overlap[i] = 1; } @@ -46,3 +50,47 @@ export function maybeExpand(X, plan) { for (let i = 0; i < plan.length; ++i) V[i] = X[plan[i]]; return V; } + +// Iterate over the options and pull out any that represent columns of values. +const knownChannels = [ + ["x"], + ["x1"], + ["x2"], + ["y"], + ["y1"], + ["y2"], + ["z"], + ["ariaLabel"], + ["href"], + ["title"], + ["fill", (value) => maybeColorChannel(value)[0]], + ["stroke", (value) => maybeColorChannel(value)[0]], + ["fillOpacity", (value) => maybeNumberChannel(value)[0]], + ["strokeOpacity", (value) => maybeNumberChannel(value)[0]], + ["opacity", (value) => maybeNumberChannel(value)[0]], + ["strokeWidth", (value) => maybeNumberChannel(value)[0]], + ["symbol", (value) => maybeSymbolChannel(value)[0]], // dot + ["r", (value) => maybeNumberChannel(value)[0]], // dot + ["rotate", (value) => maybeNumberChannel(value)[0]], // dot, text + ["fontSize", (value) => maybeFontSizeChannel(value)[0]], // text + ["text"], // text + ["length", (value) => maybeNumberChannel(value)[0]], // vector + ["width", (value) => maybeNumberChannel(value)[0]], // image + ["height", (value) => maybeNumberChannel(value)[0]], // image + ["src", (value) => maybePathChannel(value)[0]], // image + ["weight", (value) => maybeNumberChannel(value)[0]] // density +]; + +export function maybeExpandOutputs(options) { + const other = {}; + const outputs = []; + for (const [name, test = (value) => value] of knownChannels) { + const value = test(options[name]); + if (value != null) { + const [V, setV] = column(value); + other[name] = V; + outputs.push((data, plan) => setV(maybeExpand(valueof(data, value), plan))); + } + } + return [other, outputs]; +} diff --git a/src/marks/image.js b/src/marks/image.js index efe203a40f..3eaef552d3 100644 --- a/src/marks/image.js +++ b/src/marks/image.js @@ -34,7 +34,7 @@ function isUrl(string) { // Disambiguates a constant src definition from a channel. A path or URL string // is assumed to be a constant; any other string is assumed to be a field name. -function maybePathChannel(value) { +export function maybePathChannel(value) { return typeof value === "string" && (isPath(value) || isUrl(value)) ? [undefined, value] : [value, undefined]; } diff --git a/src/marks/text.js b/src/marks/text.js index 344e89c68f..ccd5ec7fa7 100644 --- a/src/marks/text.js +++ b/src/marks/text.js @@ -241,7 +241,7 @@ const fontSizes = new Set([ // - string : e.g., "12px" // - string : e.g., "80%" // Anything else is assumed to be a channel definition. -function maybeFontSizeChannel(fontSize) { +export function maybeFontSizeChannel(fontSize) { if (fontSize == null || typeof fontSize === "number") return [undefined, fontSize]; if (typeof fontSize !== "string") return [fontSize, undefined]; fontSize = fontSize.trim().toLowerCase(); diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 91ab1666d5..c5fd317729 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -1,7 +1,6 @@ import {bin as binner, extent, thresholdFreedmanDiaconis, thresholdScott, thresholdSturges, utcTickInterval} from "d3"; import { valueof, - range, identity, maybeColumn, maybeTuple, @@ -161,6 +160,7 @@ function binn( ...("fill" in inputs && {fill: GF || fill}), ...("stroke" in inputs && {stroke: GS || stroke}), ...basic(options, (data, facets) => { + const cover = (bx || by) && union(facets); const K = valueof(data, k); const Z = valueof(data, z); const F = valueof(data, vfill); @@ -172,8 +172,8 @@ function binn( const GZ = Z && setGZ([]); const GF = F && setGF([]); const GS = S && setGS([]); - const BX = bx ? bx(data) : [[, , (I) => I]]; - const BY = by ? by(data) : [[, , (I) => I]]; + const BX = bx ? bx(data, cover) : [[, , (I) => I]]; + const BY = by ? by(data, cover) : [[, , (I) => I]]; const BX1 = bx && setBX1([]); const BX2 = bx && setBX2([]); const BY1 = by && setBY1([]); @@ -248,7 +248,7 @@ function maybeBinValueTuple(options) { function maybeBin(options) { if (options == null) return; const {value, cumulative, domain = extent, thresholds} = options; - const bin = (data) => { + const bin = (data, cover) => { let V = valueof(data, value, Array); // d3.bin prefers Array input const bin = binner().value((i) => V[i]); if (isTemporal(V) || isTimeThresholds(thresholds)) { @@ -279,7 +279,7 @@ function maybeBin(options) { } bin.thresholds(t).domain(d); } - let bins = bin(range(data)).map(binset); + let bins = bin(cover).map(binset); if (cumulative) bins = (cumulative < 0 ? bins.reverse() : bins).map(bincumset); return bins.map(binfilter); }; @@ -365,3 +365,9 @@ function binfilter([{x0, x1}, set]) { function binempty() { return new Uint32Array(0); } + +function union(facets) { + const U = new Set(); + for (const f of facets) for (const i of f) U.add(i); + return U; +} diff --git a/src/transforms/dodge.js b/src/transforms/dodge.js index 513a7b1efa..1d28d3deda 100644 --- a/src/transforms/dodge.js +++ b/src/transforms/dodge.js @@ -3,6 +3,7 @@ import {finite, positive} from "../defined.js"; import {identity, maybeNamed, number, valueof} from "../options.js"; import {coerceNumbers} from "../scales.js"; import {initializer} from "./basic.js"; +import {maybeExpand, facetReindex} from "../facet.js"; const anchorXLeft = ({marginLeft}) => [1, marginLeft]; const anchorXRight = ({width, marginRight}) => [-1, width - marginRight]; @@ -87,11 +88,15 @@ function dodge(y, x, anchor, padding, options) { options = {...options, channels: {r: {value: r, scale: "r"}, ...maybeNamed(channels)}}; if (sort === undefined && reverse === undefined) options.sort = {channel: "r", order: "descending"}; } - return initializer(options, function (data, facets, {[x]: X, r: R}, scales, dimensions) { + return initializer(options, function (data, facets, {[x]: X, r: R, ...channels}, scales, dimensions) { if (!X) throw new Error(`missing channel: ${x}`); - X = coerceNumbers(valueof(X.value, scales[X.scale] || identity)); + + let plan; + ({facets, plan} = facetReindex(facets, data.length)); // make facets exclusive + X = maybeExpand(coerceNumbers(valueof(X.value, scales[X.scale] || identity)), plan); + const r = R ? undefined : this.r !== undefined ? this.r : options.r !== undefined ? number(options.r) : 3; - if (R) R = coerceNumbers(valueof(R.value, scales[R.scale] || identity)); + if (R) R = maybeExpand(coerceNumbers(valueof(R.value, scales[R.scale] || identity))); let [ky, ty] = anchor(dimensions); const compare = ky ? compareAscending : compareSymmetric; const Y = new Float64Array(X.length); @@ -144,13 +149,17 @@ function dodge(y, x, anchor, padding, options) { Y[i] = Y[i] * ky + ty; } } + for (const key in channels) { + channels[key].value = maybeExpand(channels[key].value, plan); + } return { data, facets, channels: { [x]: {value: X}, [y]: {value: Y}, - ...(R && {r: {value: R}}) + ...(R && {r: {value: R}}), + ...channels } }; }); diff --git a/src/transforms/map.js b/src/transforms/map.js index b62ea564c0..1cf2bc21a0 100644 --- a/src/transforms/map.js +++ b/src/transforms/map.js @@ -1,6 +1,7 @@ import {count, group, rank} from "d3"; import {maybeZ, take, valueof, maybeInput, column} from "../options.js"; import {basic} from "./basic.js"; +import {maybeExpand, facetReindex, maybeExpandOutputs} from "../facet.js"; /** * ```js @@ -56,11 +57,15 @@ export function map(outputs = {}, options = {}) { const [output, setOutput] = column(input); return {key, input, output, setOutput, map: maybeMap(map)}; }); + const [other, facetOutputs] = maybeExpandOutputs(options); // TODO wrap outputs in facetReindex return { ...basic(options, (data, facets) => { - const Z = valueof(data, z); - const X = channels.map(({input}) => valueof(data, input)); - const MX = channels.map(({setOutput}) => setOutput(new Array(data.length))); + let plan; + ({facets, plan} = facetReindex(facets, data.length)); // make facets exclusive + const Z = maybeExpand(valueof(data, z), plan); + const X = channels.map(({input}) => maybeExpand(valueof(data, input), plan)); + const MX = channels.map(({setOutput}) => setOutput(new Array(plan ? plan.length : data.length))); + for (const o of facetOutputs) o(data, plan); // expand any extra channels for (const facet of facets) { for (const I of Z ? group(facet, (i) => Z[i]).values() : [facet]) { channels.forEach(({map}, i) => map.map(I, X[i], MX[i])); @@ -68,6 +73,7 @@ export function map(outputs = {}, options = {}) { } return {data, facets}; }), + ...other, ...Object.fromEntries(channels.map(({key, output}) => [key, output])) }; } diff --git a/src/transforms/stack.js b/src/transforms/stack.js index 20c5a43d87..1059d625c4 100644 --- a/src/transforms/stack.js +++ b/src/transforms/stack.js @@ -1,8 +1,8 @@ import {InternMap, cumsum, group, groupSort, greatest, max, min, rollup, sum} from "d3"; import {ascendingDefined} from "../defined.js"; -import {maybeExpand, facetReindex} from "../facet.js"; +import {maybeExpand, facetReindex, maybeExpandOutputs} from "../facet.js"; import {field, column, mid, range, valueof, one} from "../options.js"; -import {maybeColumn, maybeColorChannel, maybeZ, maybeZero} from "../options.js"; +import {maybeColumn, maybeZ, maybeZero} from "../options.js"; import {basic} from "./basic.js"; /** @@ -19,7 +19,7 @@ export function stackX(stackOptions = {}, options = {}) { if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions); const {y1, y = y1, x, ...rest} = options; // note: consumes x! const [transform, Y, x1, x2, other] = stack(y, x, "x", stackOptions, rest); - return {...transform, y1, y: Y, x1, x2, ...other, x: mid(x1, x2)}; + return {...transform, ...other, y1, y: Y, x1, x2, x: mid(x1, x2)}; } /** @@ -38,7 +38,7 @@ export function stackX1(stackOptions = {}, options = {}) { if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions); const {y1, y = y1, x} = options; const [transform, Y, X, , other] = stack(y, x, "x", stackOptions, options); - return {...transform, y1, y: Y, x: X, ...other}; + return {...transform, ...other, y1, y: Y, x: X}; } /** @@ -57,7 +57,7 @@ export function stackX2(stackOptions = {}, options = {}) { if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions); const {y1, y = y1, x} = options; const [transform, Y, , X, other] = stack(y, x, "x", stackOptions, options); - return {...transform, y1, y: Y, x: X, ...other}; + return {...transform, ...other, y1, y: Y, x: X}; } /** @@ -79,7 +79,7 @@ export function stackY(stackOptions = {}, options = {}) { if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions); const {x1, x = x1, y, ...rest} = options; // note: consumes y! const [transform, X, y1, y2, other] = stack(x, y, "y", stackOptions, rest); - return {...transform, x1, x: X, y1, y2, y: mid(y1, y2), ...other}; + return {...transform, ...other, x1, x: X, y1, y2, y: mid(y1, y2)}; } /** @@ -98,7 +98,7 @@ export function stackY1(stackOptions = {}, options = {}) { if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions); const {x1, x = x1, y} = options; const [transform, X, Y, , other] = stack(x, y, "y", stackOptions, options); - return {...transform, x1, x: X, y: Y, ...other}; + return {...transform, ...other, x1, x: X, y: Y}; } /** @@ -117,7 +117,7 @@ export function stackY2(stackOptions = {}, options = {}) { if (arguments.length === 1) [stackOptions, options] = mergeOptions(stackOptions); const {x1, x = x1, y} = options; const [transform, X, , Y, other] = stack(x, y, "y", stackOptions, options); - return {...transform, x1, x: X, y: Y, ...other}; + return {...transform, ...other, x1, x: X, y: Y}; } export function maybeStackX({x, x1, x2, ...options} = {}) { @@ -147,54 +147,11 @@ function stack(x, y = one, ky, {offset, order, reverse}, options) { const [Y2, setY2] = column(y); offset = maybeOffset(offset); order = maybeOrder(order, offset, ky); - - // TODO Here we will need to iterate over the options and pull out any that - // represent channels (columns of values). The list of possible channels for - // all Marks: - // - fill (color) - // - fillOpacity (opacity/number) - // - stroke (color) - // - strokeOpacity (opacity/number) - // - strokeWidth (number) - // - opacity (opacity/number) - // - title - // - href - // - ariaLabel - // - z? - // TODO - // - text (text) - // - rotate (text, dot) - // - fontSize (text) - // - symbol (dot) - // - r (dot) - // - length (vector) - // - width (image) - // - height (image) - // - src (image) - // - weight (density) - - const knownChannels = [ - ["fill", (value) => maybeColorChannel(value)[0]], - ["stroke", (value) => maybeColorChannel(value)[0]], - ["text"] - ]; - - const other = {}; - const outputs = []; - for (const [name, test = (value) => value] of knownChannels) { - const value = test(options[name]); - if (value) { - const [V, setV] = column(value); - other[name] = V; - outputs.push((data, plan) => setV(maybeExpand(valueof(data, value), plan))); - } - } - + const [other, outputs] = maybeExpandOutputs(options); // TODO wrap outputs in facetReindex return [ basic(options, (data, facets) => { let plan; ({facets, plan} = facetReindex(facets, data.length)); // make facets exclusive - for (const o of outputs) o(data, plan); // expand any extra channels const XS = x == null ? undefined : valueof(data, x); const YS = valueof(data, y, Float64Array); const ZS = valueof(data, z); @@ -203,6 +160,7 @@ function stack(x, y = one, ky, {offset, order, reverse}, options) { const Z = maybeExpand(ZS, plan); const O = order && maybeExpand(order(data, XS, YS, ZS), plan); const n = plan ? plan.length : data.length; + for (const o of outputs) o(data, plan); // expand any extra channels const Y1 = setY1(new Float64Array(n)); const Y2 = setY2(new Float64Array(n)); const facetstacks = []; diff --git a/test/marks/bar-test.js b/test/marks/bar-test.js index e2a39ad82e..a6751a0209 100644 --- a/test/marks/bar-test.js +++ b/test/marks/bar-test.js @@ -41,10 +41,10 @@ it("barX(data, {y}) uses a band scale", () => { }); it("barX(data, {title}) specifies an optional title channel", () => { - const bar = Plot.barX(undefined, {title: "x"}); - const {title} = bar.channels; - assert.strictEqual(title.value, "x"); - assert.strictEqual(title.scale, undefined); + const bar = Plot.barX([{x: 1}, {x: "b"}], {title: "x"}); + const {channels} = bar.initialize(); + assert.deepStrictEqual(channels.title.value, [1, "b"]); + assert.strictEqual(channels.title.scale, undefined); }); it("barX(data, {fill}) allows fill to be a constant color", () => { @@ -77,11 +77,11 @@ it("barX(data, {stroke}) allows stroke to be null", () => { }); it("barX(data, {stroke}) allows stroke to be a variable color", () => { - const bar = Plot.barX(undefined, {stroke: "x"}); + const bar = Plot.barX([{x: 1}, {x: 2}], {stroke: "x"}); assert.strictEqual(bar.stroke, undefined); - const {stroke} = bar.channels; - assert.strictEqual(stroke.value, "x"); - assert.strictEqual(stroke.scale, "color"); + const {channels} = bar.initialize(); + assert.deepStrictEqual(channels.stroke.value, [1, 2]); + assert.strictEqual(channels.stroke.scale, "color"); }); it("barX(data, {x, y}) defaults x1 to zero and x2 to x", () => { @@ -142,10 +142,10 @@ it("barY(data, {x}) uses a band scale", () => { }); it("barY(data, {title}) specifies an optional title channel", () => { - const bar = Plot.barY(undefined, {title: "x"}); - const {title} = bar.channels; - assert.strictEqual(title.value, "x"); - assert.strictEqual(title.scale, undefined); + const bar = Plot.barY([{x: 1}, {x: 2}], {title: "x"}); + const {channels} = bar.initialize(); + assert.deepStrictEqual(channels.title.value, [1, 2]); + assert.strictEqual(channels.title.scale, undefined); }); it("barY(data, {fill}) allows fill to be a constant color", () => { @@ -159,11 +159,11 @@ it("barY(data, {fill}) allows fill to be null", () => { }); it("barY(data, {fill}) allows fill to be a variable color", () => { - const bar = Plot.barY(undefined, {fill: "x"}); + const bar = Plot.barY([{x: 1}, {x: 2}], {fill: "x"}); assert.strictEqual(bar.fill, undefined); - const {fill} = bar.channels; - assert.strictEqual(fill.value, "x"); - assert.strictEqual(fill.scale, "color"); + const {channels} = bar.initialize(); + assert.deepStrictEqual(channels.fill.value, [1, 2]); + assert.strictEqual(channels.fill.scale, "color"); }); it("barY(data, {stroke}) allows stroke to be a constant color", () => { @@ -177,11 +177,11 @@ it("barY(data, {stroke}) allows stroke to be null", () => { }); it("barY(data, {stroke}) allows stroke to be a variable color", () => { - const bar = Plot.barY(undefined, {stroke: "x"}); + const bar = Plot.barY([{x: 1}, {x: 2}], {stroke: "x"}); assert.strictEqual(bar.stroke, undefined); - const {stroke} = bar.channels; - assert.strictEqual(stroke.value, "x"); - assert.strictEqual(stroke.scale, "color"); + const {channels} = bar.initialize(); + assert.deepStrictEqual(channels.stroke.value, [1, 2]); + assert.strictEqual(channels.stroke.scale, "color"); }); it("barY(data, {x, y}) defaults y1 to zero and y2 to y", () => { diff --git a/test/output/generativeRoses.svg b/test/output/generativeRoses.svg new file mode 100644 index 0000000000..e4a7205c79 --- /dev/null +++ b/test/output/generativeRoses.svg @@ -0,0 +1,296 @@ + + + + + + + + + First + + + + + Last + + + + + MaxX + + + + + MinX + + + + + MaxY + + + + + MinY + + + + + + + + + + First + + + + + Last + + + + + MaxX + + + + + MinX + + + + + MaxY + + + + + MinY + + + + + + + + + + First + + + + + Last + + + + + MaxX + + + + + MinX + + + + + MaxY + + + + + MinY + + + + + + + + + + First + + + + + Last + + + + + MaxX + + + + + MinX + + + + + MaxY + + + + + MinY + + + + + + + + + + First + + + + + Last + + + + + MaxX + + + + + MinX + + + + + MaxY + + + + + MinY + + + + + + + + + + First + + + + + Last + + + + + MaxX + + + + + MinX + + + + + MaxY + + + + + MinY + + + + + + + + + + First + + + + + Last + + + + + MaxX + + + + + MinX + + + + + MaxY + + + + + MinY + + + + + + + + + + First + + + + + Last + + + + + MaxX + + + + + MinX + + + + + MaxY + + + + + MinY + + + + \ No newline at end of file diff --git a/test/output/hexbinExclude.svg b/test/output/hexbinExclude.svg new file mode 100644 index 0000000000..ca4edb5e0f --- /dev/null +++ b/test/output/hexbinExclude.svg @@ -0,0 +1,401 @@ + + + + + 35 + + + 40 + + + 45 + + + 50 + + + 55 + ↑ culmen_length_mm + + + + Adelie + + + Chinstrap + + + Gentoo + species + + + + 14 + + + 16 + + + 18 + + + 20 + + + + + 14 + + + 16 + + + 18 + + + 20 + + + + + 14 + + + 16 + + + 18 + + + 20 + culmen_depth_mm → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/musicRevenueBars.svg b/test/output/musicRevenueBars.svg new file mode 100644 index 0000000000..67d0421b08 --- /dev/null +++ b/test/output/musicRevenueBars.svg @@ -0,0 +1,9986 @@ + + + + + Disc + + + Download + + + Other + + + Streaming + + + Tape + + + Vinyl + group + + + + 1975 + + + 1980 + + + 1985 + + + 1990 + + + 1995 + + + 2000 + + + 2005 + + + 2010 + + + 2015 + + + + + + −20 + + + + −10 + + + + 0 + + + + 10 + + + + 20 + ↑ Annual revenue (billions, adj.) + + + + + −20 + + + + −10 + + + + 0 + + + + 10 + + + + 20 + + + + + + −20 + + + + −10 + + + + 0 + + + + 10 + + + + 20 + + + + + + −20 + + + + −10 + + + + 0 + + + + 10 + + + + 20 + + + + + + −20 + + + + −10 + + + + 0 + + + + 10 + + + + 20 + + + + + + −20 + + + + −10 + + + + 0 + + + + 10 + + + + 20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + CD Single + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + SACD + Disc (1) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Album + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Music Video + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Download Single + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Other Digital + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + Ringtones & Ringbacks + Download (1) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + DVD Audio + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Kiosk + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Music Video (Physical) + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + Synchronization + Other (1) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + Limited Tier Paid Subscription + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + On-Demand Streaming (Ad-Supported) + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Other Ad-Supported Streaming + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + Paid Subscription + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + SoundExchange Distributions + Streaming (1) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + 8 - Track + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Cassette Single + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + Other Tapes + Tape (1) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + LP/EP + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + Vinyl Single + Vinyl (1) + + + + + + + \ No newline at end of file diff --git a/test/output/musicRevenueBin.svg b/test/output/musicRevenueBin.svg new file mode 100644 index 0000000000..a55a967481 --- /dev/null +++ b/test/output/musicRevenueBin.svg @@ -0,0 +1,2318 @@ + + + + + Disc + + + Download + + + Other + + + Streaming + + + Tape + + + Vinyl + group + + + + 1970 + + + 1980 + + + 1990 + + + 2000 + + + 2010 + + + 2020 + + + + + + −100 + + + + −50 + + + + 0 + + + + 50 + + + + 100 + ↑ Annual revenue (billions, adj.) + + + + + −100 + + + + −50 + + + + 0 + + + + 50 + + + + 100 + + + + + + −100 + + + + −50 + + + + 0 + + + + 50 + + + + 100 + + + + + + −100 + + + + −50 + + + + 0 + + + + 50 + + + + 100 + + + + + + −100 + + + + −50 + + + + 0 + + + + 50 + + + + 100 + + + + + + −100 + + + + −50 + + + + 0 + + + + 50 + + + + 100 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CD + Disc (2) + + + CD + Disc (5) + + + CD + Disc (5) + + + CD + Disc (5) + + + CD + Disc (5) + + + CD + Disc (5) + + + CD + Disc (5) + + + CD + Disc (5) + + + CD + Disc (5) + + + CD + Disc (5) + + + CD Single + Disc (2) + + + CD Single + Disc (5) + + + CD Single + Disc (5) + + + CD Single + Disc (5) + + + CD Single + Disc (5) + + + CD Single + Disc (5) + + + CD Single + Disc (5) + + + CD Single + Disc (5) + + + CD Single + Disc (5) + + + CD Single + Disc (5) + + + SACD + Disc (2) + + + SACD + Disc (5) + + + SACD + Disc (5) + + + SACD + Disc (5) + + + SACD + Disc (5) + + + SACD + Disc (5) + + + SACD + Disc (5) + + + SACD + Disc (5) + + + SACD + Disc (5) + + + SACD + Disc (5) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Download Album + Download (2) + + + Download Album + Download (5) + + + Download Album + Download (5) + + + Download Album + Download (5) + + + Download Album + Download (5) + + + Download Album + Download (5) + + + Download Album + Download (5) + + + Download Album + Download (5) + + + Download Album + Download (5) + + + Download Album + Download (5) + + + Download Music Video + Download (2) + + + Download Music Video + Download (5) + + + Download Music Video + Download (5) + + + Download Music Video + Download (5) + + + Download Music Video + Download (5) + + + Download Music Video + Download (5) + + + Download Music Video + Download (5) + + + Download Music Video + Download (5) + + + Download Music Video + Download (5) + + + Download Music Video + Download (5) + + + Download Single + Download (2) + + + Download Single + Download (5) + + + Download Single + Download (5) + + + Download Single + Download (5) + + + Download Single + Download (5) + + + Download Single + Download (5) + + + Download Single + Download (5) + + + Download Single + Download (5) + + + Download Single + Download (5) + + + Download Single + Download (5) + + + Other Digital + Download (2) + + + Other Digital + Download (5) + + + Other Digital + Download (5) + + + Other Digital + Download (5) + + + Other Digital + Download (5) + + + Other Digital + Download (5) + + + Other Digital + Download (5) + + + Other Digital + Download (5) + + + Other Digital + Download (5) + + + Other Digital + Download (5) + + + Ringtones & Ringbacks + Download (2) + + + Ringtones & Ringbacks + Download (5) + + + Ringtones & Ringbacks + Download (5) + + + Ringtones & Ringbacks + Download (5) + + + Ringtones & Ringbacks + Download (5) + + + Ringtones & Ringbacks + Download (5) + + + Ringtones & Ringbacks + Download (5) + + + Ringtones & Ringbacks + Download (5) + + + Ringtones & Ringbacks + Download (5) + + + Ringtones & Ringbacks + Download (5) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DVD Audio + Other (2) + + + DVD Audio + Other (5) + + + DVD Audio + Other (5) + + + DVD Audio + Other (5) + + + DVD Audio + Other (5) + + + DVD Audio + Other (5) + + + DVD Audio + Other (5) + + + DVD Audio + Other (5) + + + DVD Audio + Other (5) + + + DVD Audio + Other (5) + + + Kiosk + Other (2) + + + Kiosk + Other (5) + + + Kiosk + Other (5) + + + Kiosk + Other (5) + + + Kiosk + Other (5) + + + Kiosk + Other (5) + + + Kiosk + Other (5) + + + Kiosk + Other (5) + + + Kiosk + Other (5) + + + Kiosk + Other (5) + + + Music Video (Physical) + Other (2) + + + Music Video (Physical) + Other (5) + + + Music Video (Physical) + Other (5) + + + Music Video (Physical) + Other (5) + + + Music Video (Physical) + Other (5) + + + Music Video (Physical) + Other (5) + + + Music Video (Physical) + Other (5) + + + Music Video (Physical) + Other (5) + + + Music Video (Physical) + Other (5) + + + Music Video (Physical) + Other (5) + + + Synchronization + Other (2) + + + Synchronization + Other (5) + + + Synchronization + Other (5) + + + Synchronization + Other (5) + + + Synchronization + Other (5) + + + Synchronization + Other (5) + + + Synchronization + Other (5) + + + Synchronization + Other (5) + + + Synchronization + Other (5) + + + Synchronization + Other (5) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Limited Tier Paid Subscription + Streaming (2) + + + Limited Tier Paid Subscription + Streaming (5) + + + Limited Tier Paid Subscription + Streaming (5) + + + Limited Tier Paid Subscription + Streaming (5) + + + Limited Tier Paid Subscription + Streaming (5) + + + Limited Tier Paid Subscription + Streaming (5) + + + Limited Tier Paid Subscription + Streaming (5) + + + Limited Tier Paid Subscription + Streaming (5) + + + Limited Tier Paid Subscription + Streaming (5) + + + Limited Tier Paid Subscription + Streaming (5) + + + On-Demand Streaming (Ad-Supported) + Streaming (2) + + + On-Demand Streaming (Ad-Supported) + Streaming (5) + + + On-Demand Streaming (Ad-Supported) + Streaming (5) + + + On-Demand Streaming (Ad-Supported) + Streaming (5) + + + On-Demand Streaming (Ad-Supported) + Streaming (5) + + + On-Demand Streaming (Ad-Supported) + Streaming (5) + + + On-Demand Streaming (Ad-Supported) + Streaming (5) + + + On-Demand Streaming (Ad-Supported) + Streaming (5) + + + On-Demand Streaming (Ad-Supported) + Streaming (5) + + + On-Demand Streaming (Ad-Supported) + Streaming (5) + + + Other Ad-Supported Streaming + Streaming (2) + + + Other Ad-Supported Streaming + Streaming (5) + + + Other Ad-Supported Streaming + Streaming (5) + + + Other Ad-Supported Streaming + Streaming (5) + + + Other Ad-Supported Streaming + Streaming (5) + + + Other Ad-Supported Streaming + Streaming (5) + + + Other Ad-Supported Streaming + Streaming (5) + + + Other Ad-Supported Streaming + Streaming (5) + + + Other Ad-Supported Streaming + Streaming (5) + + + Other Ad-Supported Streaming + Streaming (5) + + + Paid Subscription + Streaming (2) + + + Paid Subscription + Streaming (5) + + + Paid Subscription + Streaming (5) + + + Paid Subscription + Streaming (5) + + + Paid Subscription + Streaming (5) + + + Paid Subscription + Streaming (5) + + + Paid Subscription + Streaming (5) + + + Paid Subscription + Streaming (5) + + + Paid Subscription + Streaming (5) + + + Paid Subscription + Streaming (5) + + + SoundExchange Distributions + Streaming (2) + + + SoundExchange Distributions + Streaming (5) + + + SoundExchange Distributions + Streaming (5) + + + SoundExchange Distributions + Streaming (5) + + + SoundExchange Distributions + Streaming (5) + + + SoundExchange Distributions + Streaming (5) + + + SoundExchange Distributions + Streaming (5) + + + SoundExchange Distributions + Streaming (5) + + + SoundExchange Distributions + Streaming (5) + + + SoundExchange Distributions + Streaming (5) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 8 - Track + Tape (2) + + + 8 - Track + Tape (5) + + + 8 - Track + Tape (5) + + + 8 - Track + Tape (5) + + + 8 - Track + Tape (5) + + + 8 - Track + Tape (5) + + + 8 - Track + Tape (5) + + + 8 - Track + Tape (5) + + + 8 - Track + Tape (5) + + + 8 - Track + Tape (5) + + + Cassette + Tape (2) + + + Cassette + Tape (5) + + + Cassette + Tape (5) + + + Cassette + Tape (5) + + + Cassette + Tape (5) + + + Cassette + Tape (5) + + + Cassette + Tape (5) + + + Cassette + Tape (5) + + + Cassette + Tape (5) + + + Cassette + Tape (5) + + + Cassette Single + Tape (2) + + + Cassette Single + Tape (5) + + + Cassette Single + Tape (5) + + + Cassette Single + Tape (5) + + + Cassette Single + Tape (5) + + + Cassette Single + Tape (5) + + + Cassette Single + Tape (5) + + + Cassette Single + Tape (5) + + + Cassette Single + Tape (5) + + + Cassette Single + Tape (5) + + + Other Tapes + Tape (2) + + + Other Tapes + Tape (5) + + + Other Tapes + Tape (5) + + + Other Tapes + Tape (5) + + + Other Tapes + Tape (5) + + + Other Tapes + Tape (5) + + + Other Tapes + Tape (5) + + + Other Tapes + Tape (5) + + + Other Tapes + Tape (5) + + + Other Tapes + Tape (5) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LP/EP + Vinyl (2) + + + LP/EP + Vinyl (5) + + + LP/EP + Vinyl (5) + + + LP/EP + Vinyl (5) + + + LP/EP + Vinyl (5) + + + LP/EP + Vinyl (5) + + + LP/EP + Vinyl (5) + + + LP/EP + Vinyl (5) + + + LP/EP + Vinyl (5) + + + LP/EP + Vinyl (5) + + + Vinyl Single + Vinyl (2) + + + Vinyl Single + Vinyl (5) + + + Vinyl Single + Vinyl (5) + + + Vinyl Single + Vinyl (5) + + + Vinyl Single + Vinyl (5) + + + Vinyl Single + Vinyl (5) + + + Vinyl Single + Vinyl (5) + + + Vinyl Single + Vinyl (5) + + + Vinyl Single + Vinyl (5) + + + Vinyl Single + Vinyl (5) + + + + + + + \ No newline at end of file diff --git a/test/output/musicRevenueGroup.svg b/test/output/musicRevenueGroup.svg new file mode 100644 index 0000000000..de3afad1ad --- /dev/null +++ b/test/output/musicRevenueGroup.svg @@ -0,0 +1,484 @@ + + + + + Disc + + + Download + + + Other + + + Streaming + + + Tape + + + Vinyl + group + + + + 1980 + + + 1990 + + + 2000 + + + 2010 + + + + + + −20 + + + + −10 + + + + 0 + + + + 10 + + + + 20 + ↑ Annual revenue (billions, adj.) + + + + + −20 + + + + −10 + + + + 0 + + + + 10 + + + + 20 + + + + + + −20 + + + + −10 + + + + 0 + + + + 10 + + + + 20 + + + + + + −20 + + + + −10 + + + + 0 + + + + 10 + + + + 20 + + + + + + −20 + + + + −10 + + + + 0 + + + + 10 + + + + 20 + + + + + + −20 + + + + −10 + + + + 0 + + + + 10 + + + + 20 + + + + + + CD + Disc + + + CD Single + Disc + + + SACD + Disc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Download Album + Download + + + Download Music Video + Download + + + Download Single + Download + + + Other Digital + Download + + + Ringtones & Ringbacks + Download + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DVD Audio + Other + + + Kiosk + Other + + + Music Video (Physical) + Other + + + Synchronization + Other + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Limited Tier Paid Subscription + Streaming + + + On-Demand Streaming (Ad-Supported) + Streaming + + + Other Ad-Supported Streaming + Streaming + + + Paid Subscription + Streaming + + + SoundExchange Distributions + Streaming + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 8 - Track + Tape + + + Cassette + Tape + + + Cassette Single + Tape + + + Other Tapes + Tape + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LP/EP + Vinyl + + + Vinyl Single + Vinyl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/musicRevenueWiggle.svg b/test/output/musicRevenueWiggle.svg new file mode 100644 index 0000000000..f446964356 --- /dev/null +++ b/test/output/musicRevenueWiggle.svg @@ -0,0 +1,412 @@ + + + + + Disc + + + Download + + + Other + + + Streaming + + + Tape + + + Vinyl + group + + + + 1980 + + + 1990 + + + 2000 + + + 2010 + + + + + + 0 + + + + 10 + ↑ Annual revenue (billions, adj.) + + + + + 0 + + + + 10 + + + + + + 0 + + + + 10 + + + + + + 0 + + + + 10 + + + + + + 0 + + + + 10 + + + + + + 0 + + + + 10 + + + + + + + + + + + + + + + + + + + + + + + + + + + + CD + Disc + + + CD Single + Disc + + + SACD + Disc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Download Album + Download + + + Download Music Video + Download + + + Download Single + Download + + + Other Digital + Download + + + Ringtones & Ringbacks + Download + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DVD Audio + Other + + + Kiosk + Other + + + Music Video (Physical) + Other + + + Synchronization + Other + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Limited Tier Paid Subscription + Streaming + + + On-Demand Streaming (Ad-Supported) + Streaming + + + Other Ad-Supported Streaming + Streaming + + + Paid Subscription + Streaming + + + SoundExchange Distributions + Streaming + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 8 - Track + Tape + + + Cassette + Tape + + + Cassette Single + Tape + + + Other Tapes + Tape + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LP/EP + Vinyl + + + Vinyl Single + Vinyl + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/penguinCumsumExclude.svg b/test/output/penguinCumsumExclude.svg new file mode 100644 index 0000000000..3e0fb57005 --- /dev/null +++ b/test/output/penguinCumsumExclude.svg @@ -0,0 +1,806 @@ + + + + + −300 + + + −200 + + + −100 + + + 0 + + + 100 + + + 200 + + + + + Biscoe + + + Dream + + + Torgersen + island + + + + 3,000 + + + 4,000 + + + 5,000 + + + 6,000 + + + + + 3,000 + + + 4,000 + + + 5,000 + + + 6,000 + + + + + 3,000 + + + 4,000 + + + 5,000 + + + 6,000 + body_mass_g → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/penguinDodgeReindexed.svg b/test/output/penguinDodgeReindexed.svg new file mode 100644 index 0000000000..c6a90c6451 --- /dev/null +++ b/test/output/penguinDodgeReindexed.svg @@ -0,0 +1,746 @@ + + + + + Biscoe + + + Dream + + + Torgersen + island + + + + 3,000 + + + 3,500 + + + 4,000 + + + 4,500 + + + 5,000 + + + 5,500 + + + 6,000 + body_mass_g → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/generative-roses.js b/test/plots/generative-roses.js new file mode 100644 index 0000000000..7899ef1da0 --- /dev/null +++ b/test/plots/generative-roses.js @@ -0,0 +1,43 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +// Generate roses from a cumulative sum of vectors with various angles. The +// twist is that each facet selects a subset of these angles to ignore (with +// facet: "exclude"). +export default async function () { + const data = d3.range(0, 48, 0.7); + const mapped = (p) => + Plot.mapY( + "cumsum", + Plot.mapX("cumsum", { + facet: "exclude", + x: Math.sin, + y: Math.cos, + ...(p && { + fill: () => p, + title: () => p + }) + }) + ); + return Plot.plot({ + facet: { + data, + x: (d, i) => (i % 8) % 3, + y: (d, i) => Math.floor((i % 8) / 3), + marginRight: 80 + }, + axis: null, + marks: [ + Plot.line(data, {...mapped(null), curve: "natural"}), + ["First", "Last", "MaxX", "MinX", "MaxY", "MinY"].map((p) => + Plot.dot( + data, + Plot[`select${p}`]({ + ...mapped(p), + r: 6 + }) + ) + ) + ] + }); +} diff --git a/test/plots/hexbin-exclude.js b/test/plots/hexbin-exclude.js new file mode 100644 index 0000000000..c862a43cf0 --- /dev/null +++ b/test/plots/hexbin-exclude.js @@ -0,0 +1,45 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function () { + const penguins = await d3.csv("data/penguins.csv", d3.autoType); + const noise = d3.randomNormal.source(d3.randomLcg(42))(0, 0.1); + return Plot.plot({ + width: 960, + height: 320, + inset: 14, + facet: { + data: penguins, + x: "species", + marginRight: 80 + }, + marks: [ + Plot.frame(), + Plot.dot( + penguins, + Plot.hexbin( + {fillOpacity: "count"}, + Plot.map( + { + x: (X) => X.map((d) => d + noise()), + y: (Y) => Y.map((d) => d + noise()) + }, + { + x: "culmen_depth_mm", + y: "culmen_length_mm", + fill: "species", + facet: "exclude" + } + ) + ) + ), + Plot.dot( + penguins, + Plot.hexbin( + {fillOpacity: "count"}, + {x: "culmen_depth_mm", y: "culmen_length_mm", fill: "species", stroke: "species"} + ) + ) + ] + }); +} diff --git a/test/plots/index.js b/test/plots/index.js index d04a95d329..dbd041ec4c 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -75,6 +75,7 @@ export {default as footballCoverage} from "./football-coverage.js"; export {default as frameCorners} from "./frame-corners.js"; export {default as fruitSales} from "./fruit-sales.js"; export {default as fruitSalesDate} from "./fruit-sales-date.js"; +export {default as generativeRoses} from "./generative-roses.js"; export {default as gistempAnomaly} from "./gistemp-anomaly.js"; export {default as gistempAnomalyMoving} from "./gistemp-anomaly-moving.js"; export {default as gistempAnomalyTransform} from "./gistemp-anomaly-transform.js"; @@ -85,6 +86,7 @@ export {default as gridChoroplethDx} from "./grid-choropleth-dx.js"; export {default as groupedRects} from "./grouped-rects.js"; export {default as hadcrutWarmingStripes} from "./hadcrut-warming-stripes.js"; export {default as hexbin} from "./hexbin.js"; +export {default as hexbinExclude} from "./hexbin-exclude.js"; export {default as hexbinOranges} from "./hexbin-oranges.js"; export {default as hexbinR} from "./hexbin-r.js"; export {default as hexbinSymbol} from "./hexbin-symbol.js"; @@ -134,6 +136,10 @@ export {default as mobyDickLetterRelativeFrequency} from "./moby-dick-letter-rel export {default as morleyBoxplot} from "./morley-boxplot.js"; export {default as moviesProfitByGenre} from "./movies-profit-by-genre.js"; export {default as musicRevenue} from "./music-revenue.js"; +export {default as musicRevenueBars} from "./music-revenue-bars.js"; +export {default as musicRevenueBin} from "./music-revenue-bin.js"; +export {default as musicRevenueGroup} from "./music-revenue-group.js"; +export {default as musicRevenueWiggle} from "./music-revenue-wiggle.js"; export {default as ordinalBar} from "./ordinal-bar.js"; export {default as penguinAnnotated} from "./penguin-annotated.js"; export {default as penguinCulmen} from "./penguin-culmen.js"; @@ -142,12 +148,13 @@ export {default as penguinCulmenDelaunay} from "./penguin-culmen-delaunay.js"; export {default as penguinCulmenDelaunayMesh} from "./penguin-culmen-delaunay-mesh.js"; export {default as penguinCulmenDelaunaySpecies} from "./penguin-culmen-delaunay-species.js"; export {default as penguinCulmenVoronoi} from "./penguin-culmen-voronoi.js"; -export {default as penguinVoronoi1D} from "./penguin-voronoi-1d.js"; +export {default as penguinCumsumExclude} from "./penguin-cumsum-exclude.js"; export {default as penguinDensity} from "./penguin-density.js"; export {default as penguinDensityFill} from "./penguin-density-fill.js"; export {default as penguinDensityZ} from "./penguin-density-z.js"; export {default as penguinDodge} from "./penguin-dodge.js"; export {default as penguinDodgeHexbin} from "./penguin-dodge-hexbin.js"; +export {default as penguinDodgeReindexed} from "./penguin-dodge-reindexed.js"; export {default as penguinDodgeVoronoi} from "./penguin-dodge-voronoi.js"; export {default as penguinFacetDodge} from "./penguin-facet-dodge.js"; export {default as penguinFacetDodgeIdentity} from "./penguin-facet-dodge-identity.js"; @@ -167,6 +174,7 @@ export {default as penguinSpeciesGroup} from "./penguin-species-group.js"; export {default as penguinSpeciesIsland} from "./penguin-species-island.js"; export {default as penguinSpeciesIslandRelative} from "./penguin-species-island-relative.js"; export {default as penguinSpeciesIslandSex} from "./penguin-species-island-sex.js"; +export {default as penguinVoronoi1D} from "./penguin-voronoi-1d.js"; export {default as polylinear} from "./polylinear.js"; export {default as randomBins} from "./random-bins.js"; export {default as randomBinsXY} from "./random-bins-xy.js"; @@ -207,9 +215,9 @@ export {default as singleValueBar} from "./single-value-bar.js"; export {default as singleValueBin} from "./single-value-bin.js"; export {default as softwareVersions} from "./software-versions.js"; export {default as sparseCell} from "./sparse-cell.js"; +export {default as stackExclude} from "./stack-exclude.js"; export {default as stackedBar} from "./stacked-bar.js"; export {default as stackedRect} from "./stacked-rect.js"; -export {default as stackExclude} from "./stack-exclude.js"; export {default as stargazers} from "./stargazers.js"; export {default as stargazersBinned} from "./stargazers-binned.js"; export {default as stargazersHourly} from "./stargazers-hourly.js"; diff --git a/test/plots/music-revenue-bars.js b/test/plots/music-revenue-bars.js new file mode 100644 index 0000000000..e29eda1572 --- /dev/null +++ b/test/plots/music-revenue-bars.js @@ -0,0 +1,49 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function () { + const data = await d3.csv("data/riaa-us-revenue.csv", d3.autoType); + const stack = {x: (d) => d["year"].getFullYear(), y: "revenue", z: "format", order: "value", reverse: true}; + return Plot.plot({ + marginRight: 90, + marginBottom: 35, + facet: {data, y: "group", marginRight: 90}, + x: {ticks: d3.range(1975, 2020, 5), tickFormat: ""}, + y: { + grid: true, + label: "↑ Annual revenue (billions, adj.)", + transform: (d) => d / 1000, + nice: true + }, + marks: [ + Plot.frame(), + Plot.barY( + data, + Plot.groupX( + {y: "sum"}, + Plot.windowY({ + ...stack, + k: 3, + y: (d) => -d.revenue, + fill: "group", + facet: "exclude", + order: "sum" + }) + ) + ), + Plot.barY( + data, + Plot.groupX( + {y: "sum"}, + Plot.windowY({ + ...stack, + k: 3, + fill: "group", + title: (d) => `${d.format}\n${d.group}` + }) + ) + ), + Plot.ruleY([0]) + ] + }); +} diff --git a/test/plots/music-revenue-bin.js b/test/plots/music-revenue-bin.js new file mode 100644 index 0000000000..adfa085c44 --- /dev/null +++ b/test/plots/music-revenue-bin.js @@ -0,0 +1,48 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function () { + const data = await d3.csv("data/riaa-us-revenue.csv", d3.autoType); + const stack = {x: "year", y: "revenue", z: "format", order: "value", reverse: true}; + return Plot.plot({ + marginRight: 90, + facet: {data, y: "group", marginRight: 90}, + y: { + grid: true, + label: "↑ Annual revenue (billions, adj.)", + transform: (d) => d / 1000, + nice: true + }, + marks: [ + Plot.frame(), + Plot.rectY( + data, + Plot.binX( + {y: "sum"}, + Plot.windowY({ + ...stack, + k: 7, + interval: d3.utcYear.every(5), + y: (d) => -d.revenue, + fill: "#eee", + facet: "exclude" + }) + ) + ), + Plot.rectY( + data, + Plot.binX( + {y: "sum"}, + Plot.windowY({ + ...stack, + k: 7, + interval: d3.utcYear.every(5), + fill: "group", + title: (d) => `${d.format}\n${d.group}` + }) + ) + ), + Plot.ruleY([0]) + ] + }); +} diff --git a/test/plots/music-revenue-group.js b/test/plots/music-revenue-group.js new file mode 100644 index 0000000000..5f0bcf9ac0 --- /dev/null +++ b/test/plots/music-revenue-group.js @@ -0,0 +1,35 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function () { + const data = await d3.csv("data/riaa-us-revenue.csv", d3.autoType); + const stack = {x: "year", y: "revenue", z: "format", order: "appearance", reverse: true}; + return Plot.plot({ + marginRight: 90, + facet: {data, y: "group", marginRight: 90}, + y: { + grid: true, + label: "↑ Annual revenue (billions, adj.)", + transform: (d) => d / 1000 + }, + marks: [ + Plot.areaY(data, Plot.stackY({...stack, fill: "group", title: (d) => `${d.format}\n${d.group}`})), + Plot.areaY( + data, + Plot.mapY( + (Y) => Y.map((d) => -d), + Plot.stackY({ + ...stack, + y: "revenue", + fill: "#eee", + stroke: "#fff", + facet: "exclude" + }) + ) + ), + Plot.lineY(data, Plot.stackY2({...stack, stroke: "white", strokeWidth: 1})), + Plot.ruleY([0]), + Plot.frame() + ] + }); +} diff --git a/test/plots/music-revenue-wiggle.js b/test/plots/music-revenue-wiggle.js new file mode 100644 index 0000000000..0bee1b535c --- /dev/null +++ b/test/plots/music-revenue-wiggle.js @@ -0,0 +1,40 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function () { + const data = await d3.csv("data/riaa-us-revenue.csv", d3.autoType); + const stack = { + x: "year", + y: "revenue", + z: "format", + // order and offset are used to test that these options follow facet reindexation + order: ["Cassette", "Paid Subscription"], + offset: "wiggle", + reverse: true + }; + return Plot.plot({ + marginRight: 90, + facet: {data, y: "group", marginRight: 90}, + y: { + grid: true, + label: "↑ Annual revenue (billions, adj.)", + transform: (d) => d / 1000 + }, + marks: [ + Plot.areaY( + data, + Plot.stackY({ + ...stack, + y: (d) => -1 - d.revenue, + fill: "#eee", + stroke: "#fff", + facet: "exclude" + }) + ), + Plot.areaY(data, Plot.stackY({...stack, fill: "group", title: (d) => `${d.format}\n${d.group}`})), + Plot.lineY(data, Plot.stackY2({...stack, stroke: "white", strokeWidth: 1})), + Plot.ruleY([0]), + Plot.frame() + ] + }); +} diff --git a/test/plots/penguin-cumsum-exclude.js b/test/plots/penguin-cumsum-exclude.js new file mode 100644 index 0000000000..d3f665a6be --- /dev/null +++ b/test/plots/penguin-cumsum-exclude.js @@ -0,0 +1,37 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function () { + const penguins = await d3.csv("data/penguins.csv", d3.autoType); + return Plot.plot({ + facet: {data: penguins, x: "island"}, + width: 860, + height: 300, + y: {nice: true}, + marks: [ + Plot.frame(), + Plot.ruleY([0]), + Plot.dot( + penguins, + Plot.mapY( + "cumsum", + Plot.sort("body_mass_g", {x: "body_mass_g", y: -1, fill: "island", facet: "exclude", z: null, r: 1}) + ) + ), + Plot.lineY( + penguins, + Plot.mapY( + "cumsum", + Plot.sort("body_mass_g", { + x: "body_mass_g", + y: 1, + strokeWidth: 2, + stroke: "island", + facet: "include", + z: null + }) + ) + ) + ] + }); +} diff --git a/test/plots/penguin-dodge-reindexed.js b/test/plots/penguin-dodge-reindexed.js new file mode 100644 index 0000000000..e4d7b76480 --- /dev/null +++ b/test/plots/penguin-dodge-reindexed.js @@ -0,0 +1,11 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function () { + const penguins = await d3.csv("data/penguins.csv", d3.autoType); + return Plot.plot({ + facet: {data: penguins, y: "island"}, + height: 400, + marks: [Plot.dot(penguins, Plot.dodgeY({x: "body_mass_g", facet: "exclude", fill: "island"}))] + }); +} From fc378275c1965dcd7e7227215efee3d83ee51f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 5 Oct 2022 07:24:28 +0200 Subject: [PATCH 04/18] optimize for the common case of 1 facet --- src/facet.js | 1 + src/transforms/bin.js | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/facet.js b/src/facet.js index 930bd08809..fc26316aa8 100644 --- a/src/facet.js +++ b/src/facet.js @@ -4,6 +4,7 @@ import {maybeFontSizeChannel} from "./marks/text.js"; import {maybePathChannel} from "./marks/image.js"; export function facetReindex(facets, n) { + if (facets.length === 1) return {facets}; const overlap = new Uint8Array(n); let count = 0; let plan; diff --git a/src/transforms/bin.js b/src/transforms/bin.js index c5fd317729..f55865221e 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -1,4 +1,12 @@ -import {bin as binner, extent, thresholdFreedmanDiaconis, thresholdScott, thresholdSturges, utcTickInterval} from "d3"; +import { + bin as binner, + extent, + sum, + thresholdFreedmanDiaconis, + thresholdScott, + thresholdSturges, + utcTickInterval +} from "d3"; import { valueof, identity, @@ -160,7 +168,7 @@ function binn( ...("fill" in inputs && {fill: GF || fill}), ...("stroke" in inputs && {stroke: GS || stroke}), ...basic(options, (data, facets) => { - const cover = (bx || by) && union(facets); + const cover = (bx || by) && merge(facets); const K = valueof(data, k); const Z = valueof(data, z); const F = valueof(data, vfill); @@ -366,8 +374,10 @@ function binempty() { return new Uint32Array(0); } -function union(facets) { - const U = new Set(); - for (const f of facets) for (const i of f) U.add(i); +function merge(facets) { + if (facets.length === 1) return facets[0]; + const U = new Uint32Array(sum(facets, (f) => f.length)); + let k = 0; + for (const f of facets) for (const i of f) U[k++] = i; return U; } From b969392d4c0a79f076c199d7cda930780362cb75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 6 Oct 2022 17:22:03 +0200 Subject: [PATCH 05/18] creates a central registry of channels, with their associated scales and definitions wraps the complexity of facet reindexing in a more expressive API offers an option to expand channels that are not listed as knownChannels --- src/channel.js | 90 +++++++++++++++++++++++++++++++++++++++++ src/facet.js | 74 ++++++++++++++------------------- src/marks/image.js | 21 +--------- src/marks/text.js | 37 +---------------- src/plot.js | 21 ++-------- src/transforms/dodge.js | 8 ++-- src/transforms/map.js | 13 +++--- src/transforms/stack.js | 12 +++--- 8 files changed, 141 insertions(+), 135 deletions(-) diff --git a/src/channel.js b/src/channel.js index afcd3992e5..6bc44c53bf 100644 --- a/src/channel.js +++ b/src/channel.js @@ -2,6 +2,40 @@ import {ascending, descending, rollup, sort} from "d3"; import {first, isIterable, labelof, map, maybeValue, range, valueof} from "./options.js"; import {registry} from "./scales/index.js"; import {maybeReduce} from "./transforms/group.js"; +import {maybeColorChannel, maybeNumberChannel} from "./options.js"; +import {maybeSymbolChannel} from "./symbols.js"; + +// An array of known channels, with an associated scale name, and a definition +// that returns [variable, undefined] if variable, or [undefined, constant] if +// constant (such as "#eee" for the color channel) +export const knownChannels = { + x: {scale: "x"}, + x1: {scale: "x"}, + x2: {scale: "x"}, + y: {scale: "y"}, + y1: {scale: "y"}, + y2: {scale: "y"}, + z: {}, + ariaLabel: {}, + href: {}, + title: {}, + fill: {scale: "color", definition: maybeColorChannel}, + stroke: {scale: "color", definition: maybeColorChannel}, + fillOpacity: {scale: "opacity", definition: maybeNumberChannel}, + strokeOpacity: {scale: "opacity", definition: maybeNumberChannel}, + opacity: {scale: "opacity", definition: maybeNumberChannel}, + strokeWidth: {definition: maybeNumberChannel}, + symbol: {scale: "symbol", definition: maybeSymbolChannel}, // dot + r: {scale: "r", definition: maybeNumberChannel}, // dot + rotate: {definition: maybeNumberChannel}, // dot, text + fontSize: {definition: maybeFontSizeChannel}, // text + text: {}, // text + length: {definition: maybeNumberChannel}, // vector + width: {definition: maybeNumberChannel}, // image + height: {definition: maybeNumberChannel}, // image + src: {definition: maybePathChannel}, // image + weight: {definition: maybeNumberChannel} // density +}; // TODO Type coercion? export function Channel(data, {scale, type, value, filter, hint}) { @@ -105,3 +139,59 @@ function ascendingGroup([ak, av], [bk, bv]) { function descendingGroup([ak, av], [bk, bv]) { return descending(av, bv) || ascending(ak, bk); } + +// https://developer.mozilla.org/en-US/docs/Web/CSS/font-size +const fontSizes = new Set([ + // global keywords + "inherit", + "initial", + "revert", + "unset", + // absolute keywords + "xx-small", + "x-small", + "small", + "medium", + "large", + "x-large", + "xx-large", + "xxx-large", + // relative keywords + "larger", + "smaller" +]); + +// The font size may be expressed as a constant in the following forms: +// - number in pixels +// - string keyword: see above +// - string : e.g., "12px" +// - string : e.g., "80%" +// Anything else is assumed to be a channel definition. +export function maybeFontSizeChannel(fontSize) { + if (fontSize == null || typeof fontSize === "number") return [undefined, fontSize]; + if (typeof fontSize !== "string") return [fontSize, undefined]; + fontSize = fontSize.trim().toLowerCase(); + return fontSizes.has(fontSize) || /^[+-]?\d*\.?\d+(e[+-]?\d+)?(\w*|%)$/.test(fontSize) + ? [undefined, fontSize] + : [fontSize, undefined]; +} + +// Tests if the given string is a path: does it start with a dot-slash +// (./foo.png), dot-dot-slash (../foo.png), or slash (/foo.png)? +function isPath(string) { + return /^\.*\//.test(string); +} + +// Tests if the given string is a URL (e.g., https://placekitten.com/200/300). +// The allowed protocols is overly restrictive, but we don’t want to allow any +// scheme here because it would increase the likelihood of a false positive with +// a field name that happens to contain a colon. +function isUrl(string) { + return /^(blob|data|file|http|https):/i.test(string); +} + +// Disambiguates a constant src definition from a channel. A path or URL string +// is assumed to be a constant; any other string is assumed to be a field name. +export function maybePathChannel(value) { + return typeof value === "string" && (isPath(value) || isUrl(value)) ? [undefined, value] : [value, undefined]; +} diff --git a/src/facet.js b/src/facet.js index fc26316aa8..aba6e59afc 100644 --- a/src/facet.js +++ b/src/facet.js @@ -1,9 +1,7 @@ -import {column, maybeColorChannel, maybeNumberChannel, slice, valueof} from "./options.js"; -import {maybeSymbolChannel} from "./symbols.js"; -import {maybeFontSizeChannel} from "./marks/text.js"; -import {maybePathChannel} from "./marks/image.js"; +import {isIterable, labelof, slice, valueof} from "./options.js"; +import {knownChannels} from "./channel.js"; -export function facetReindex(facets, n) { +function facetReindex(facets, n) { if (facets.length === 1) return {facets}; const overlap = new Uint8Array(n); let count = 0; @@ -53,45 +51,35 @@ export function maybeExpand(X, plan) { } // Iterate over the options and pull out any that represent columns of values. -const knownChannels = [ - ["x"], - ["x1"], - ["x2"], - ["y"], - ["y1"], - ["y2"], - ["z"], - ["ariaLabel"], - ["href"], - ["title"], - ["fill", (value) => maybeColorChannel(value)[0]], - ["stroke", (value) => maybeColorChannel(value)[0]], - ["fillOpacity", (value) => maybeNumberChannel(value)[0]], - ["strokeOpacity", (value) => maybeNumberChannel(value)[0]], - ["opacity", (value) => maybeNumberChannel(value)[0]], - ["strokeWidth", (value) => maybeNumberChannel(value)[0]], - ["symbol", (value) => maybeSymbolChannel(value)[0]], // dot - ["r", (value) => maybeNumberChannel(value)[0]], // dot - ["rotate", (value) => maybeNumberChannel(value)[0]], // dot, text - ["fontSize", (value) => maybeFontSizeChannel(value)[0]], // text - ["text"], // text - ["length", (value) => maybeNumberChannel(value)[0]], // vector - ["width", (value) => maybeNumberChannel(value)[0]], // image - ["height", (value) => maybeNumberChannel(value)[0]], // image - ["src", (value) => maybePathChannel(value)[0]], // image - ["weight", (value) => maybeNumberChannel(value)[0]] // density -]; - -export function maybeExpandOutputs(options) { - const other = {}; - const outputs = []; - for (const [name, test = (value) => value] of knownChannels) { - const value = test(options[name]); +function maybeExpandChannels({expandChannels, ...options}) { + if (expandChannels == null) { + expandChannels = Object.entries(knownChannels) + .filter(([name, {definition = (value) => [value]}]) => definition(options[name])[0] != null) + .map(([name]) => name); + } else if (!isIterable(expandChannels)) throw new Error(`the expandChannels option is not iterable`); + const channels = {}; + let data, plan; + for (const name of expandChannels) { + const value = options[name]; if (value != null) { - const [V, setV] = column(value); - other[name] = V; - outputs.push((data, plan) => setV(maybeExpand(valueof(data, value), plan))); + channels[name] = { + transform: () => maybeExpand(valueof(data, value), plan), + label: labelof(value) + }; } } - return [other, outputs]; + return [channels, (v) => ({data, plan} = v)]; +} + +export function exclusiveFacets(options) { + const [other, setPlan] = maybeExpandChannels(options); + return [ + other, + (facets, data) => { + let plan; + ({facets, plan} = facetReindex(facets, data.length)); + setPlan({data, plan}); + return {facets, plan, n: plan ? plan.length : data.length}; + } + ]; } diff --git a/src/marks/image.js b/src/marks/image.js index 3eaef552d3..39ba3ff667 100644 --- a/src/marks/image.js +++ b/src/marks/image.js @@ -11,6 +11,7 @@ import { impliedString, applyFrameAnchor } from "../style.js"; +import {maybePathChannel} from "../channel.js"; const defaults = { ariaLabel: "image", @@ -18,26 +19,6 @@ const defaults = { stroke: null }; -// Tests if the given string is a path: does it start with a dot-slash -// (./foo.png), dot-dot-slash (../foo.png), or slash (/foo.png)? -function isPath(string) { - return /^\.*\//.test(string); -} - -// Tests if the given string is a URL (e.g., https://placekitten.com/200/300). -// The allowed protocols is overly restrictive, but we don’t want to allow any -// scheme here because it would increase the likelihood of a false positive with -// a field name that happens to contain a colon. -function isUrl(string) { - return /^(blob|data|file|http|https):/i.test(string); -} - -// Disambiguates a constant src definition from a channel. A path or URL string -// is assumed to be a constant; any other string is assumed to be a field name. -export function maybePathChannel(value) { - return typeof value === "string" && (isPath(value) || isUrl(value)) ? [undefined, value] : [value, undefined]; -} - export class Image extends Mark { constructor(data, options = {}) { let {x, y, width, height, src, preserveAspectRatio, crossOrigin, frameAnchor} = options; diff --git a/src/marks/text.js b/src/marks/text.js index ccd5ec7fa7..9edc294bfe 100644 --- a/src/marks/text.js +++ b/src/marks/text.js @@ -27,6 +27,7 @@ import { applyFrameAnchor } from "../style.js"; import {maybeIntervalMidX, maybeIntervalMidY} from "../transforms/interval.js"; +import {maybeFontSizeChannel} from "../channel.js"; const defaults = { ariaLabel: "text", @@ -214,42 +215,6 @@ function applyIndirectTextStyles(selection, mark, T) { applyAttr(selection, "font-weight", mark.fontWeight); } -// https://developer.mozilla.org/en-US/docs/Web/CSS/font-size -const fontSizes = new Set([ - // global keywords - "inherit", - "initial", - "revert", - "unset", - // absolute keywords - "xx-small", - "x-small", - "small", - "medium", - "large", - "x-large", - "xx-large", - "xxx-large", - // relative keywords - "larger", - "smaller" -]); - -// The font size may be expressed as a constant in the following forms: -// - number in pixels -// - string keyword: see above -// - string : e.g., "12px" -// - string : e.g., "80%" -// Anything else is assumed to be a channel definition. -export function maybeFontSizeChannel(fontSize) { - if (fontSize == null || typeof fontSize === "number") return [undefined, fontSize]; - if (typeof fontSize !== "string") return [fontSize, undefined]; - fontSize = fontSize.trim().toLowerCase(); - return fontSizes.has(fontSize) || /^[+-]?\d*\.?\d+(e[+-]?\d+)?(\w*|%)$/.test(fontSize) - ? [undefined, fontSize] - : [fontSize, undefined]; -} - // This is a greedy algorithm for line wrapping. It would be better to use the // Knuth–Plass line breaking algorithm (but that would be much more complex). // https://en.wikipedia.org/wiki/Line_wrap_and_word_wrap diff --git a/src/plot.js b/src/plot.js index 0e53efdce0..239438d95c 100644 --- a/src/plot.js +++ b/src/plot.js @@ -21,6 +21,7 @@ import {Scales, ScaleFunctions, autoScaleRange, exposeScales} from "./scales.js" import {position, registry as scaleRegistry} from "./scales/index.js"; import {applyInlineStyles, maybeClassName, maybeClip, styles} from "./style.js"; import {basic, initializer} from "./transforms/basic.js"; +import {knownChannels} from "./channel.js"; import {maybeInterval} from "./transforms/interval.js"; import {consumeWarnings, warn} from "./warnings.js"; @@ -756,24 +757,8 @@ function applyScaleTransforms(channels, options) { function inferChannelScale(channels) { for (const name in channels) { const channel = channels[name]; - let {scale} = channel; - if (scale === true) { - switch (name) { - case "fill": - case "stroke": - scale = "color"; - break; - case "fillOpacity": - case "strokeOpacity": - case "opacity": - scale = "opacity"; - break; - default: - scale = scaleRegistry.has(name) ? name : null; - break; - } - channel.scale = scale; - } + const {scale} = channel; + if (scale === true) channel.scale = knownChannels[name].scale; } } diff --git a/src/transforms/dodge.js b/src/transforms/dodge.js index 1d28d3deda..3232574934 100644 --- a/src/transforms/dodge.js +++ b/src/transforms/dodge.js @@ -3,8 +3,7 @@ import {finite, positive} from "../defined.js"; import {identity, maybeNamed, number, valueof} from "../options.js"; import {coerceNumbers} from "../scales.js"; import {initializer} from "./basic.js"; -import {maybeExpand, facetReindex} from "../facet.js"; - +import {exclusiveFacets, maybeExpand} from "../facet.js"; const anchorXLeft = ({marginLeft}) => [1, marginLeft]; const anchorXRight = ({width, marginRight}) => [-1, width - marginRight]; const anchorXMiddle = ({width, marginLeft, marginRight}) => [0, (marginLeft + width - marginRight) / 2]; @@ -88,15 +87,16 @@ function dodge(y, x, anchor, padding, options) { options = {...options, channels: {r: {value: r, scale: "r"}, ...maybeNamed(channels)}}; if (sort === undefined && reverse === undefined) options.sort = {channel: "r", order: "descending"}; } + const [, maybeReindex] = exclusiveFacets(options); return initializer(options, function (data, facets, {[x]: X, r: R, ...channels}, scales, dimensions) { if (!X) throw new Error(`missing channel: ${x}`); let plan; - ({facets, plan} = facetReindex(facets, data.length)); // make facets exclusive + ({facets, plan} = maybeReindex(facets, data)); X = maybeExpand(coerceNumbers(valueof(X.value, scales[X.scale] || identity)), plan); const r = R ? undefined : this.r !== undefined ? this.r : options.r !== undefined ? number(options.r) : 3; - if (R) R = maybeExpand(coerceNumbers(valueof(R.value, scales[R.scale] || identity))); + if (R) R = maybeExpand(coerceNumbers(valueof(R.value, scales[R.scale] || identity)), plan); let [ky, ty] = anchor(dimensions); const compare = ky ? compareAscending : compareSymmetric; const Y = new Float64Array(X.length); diff --git a/src/transforms/map.js b/src/transforms/map.js index 1cf2bc21a0..5c75f6bc7d 100644 --- a/src/transforms/map.js +++ b/src/transforms/map.js @@ -1,7 +1,7 @@ import {count, group, rank} from "d3"; import {maybeZ, take, valueof, maybeInput, column} from "../options.js"; import {basic} from "./basic.js"; -import {maybeExpand, facetReindex, maybeExpandOutputs} from "../facet.js"; +import {exclusiveFacets, maybeExpand} from "../facet.js"; /** * ```js @@ -57,15 +57,14 @@ export function map(outputs = {}, options = {}) { const [output, setOutput] = column(input); return {key, input, output, setOutput, map: maybeMap(map)}; }); - const [other, facetOutputs] = maybeExpandOutputs(options); // TODO wrap outputs in facetReindex + const [otherChannels, maybeReindex] = exclusiveFacets(options); return { ...basic(options, (data, facets) => { - let plan; - ({facets, plan} = facetReindex(facets, data.length)); // make facets exclusive + let plan, n; + ({facets, plan, n} = maybeReindex(facets, data)); const Z = maybeExpand(valueof(data, z), plan); const X = channels.map(({input}) => maybeExpand(valueof(data, input), plan)); - const MX = channels.map(({setOutput}) => setOutput(new Array(plan ? plan.length : data.length))); - for (const o of facetOutputs) o(data, plan); // expand any extra channels + const MX = channels.map(({setOutput}) => setOutput(new Array(n))); for (const facet of facets) { for (const I of Z ? group(facet, (i) => Z[i]).values() : [facet]) { channels.forEach(({map}, i) => map.map(I, X[i], MX[i])); @@ -73,7 +72,7 @@ export function map(outputs = {}, options = {}) { } return {data, facets}; }), - ...other, + ...otherChannels, ...Object.fromEntries(channels.map(({key, output}) => [key, output])) }; } diff --git a/src/transforms/stack.js b/src/transforms/stack.js index 1059d625c4..5f2e7168f1 100644 --- a/src/transforms/stack.js +++ b/src/transforms/stack.js @@ -1,6 +1,6 @@ import {InternMap, cumsum, group, groupSort, greatest, max, min, rollup, sum} from "d3"; import {ascendingDefined} from "../defined.js"; -import {maybeExpand, facetReindex, maybeExpandOutputs} from "../facet.js"; +import {exclusiveFacets, maybeExpand} from "../facet.js"; import {field, column, mid, range, valueof, one} from "../options.js"; import {maybeColumn, maybeZ, maybeZero} from "../options.js"; import {basic} from "./basic.js"; @@ -147,11 +147,11 @@ function stack(x, y = one, ky, {offset, order, reverse}, options) { const [Y2, setY2] = column(y); offset = maybeOffset(offset); order = maybeOrder(order, offset, ky); - const [other, outputs] = maybeExpandOutputs(options); // TODO wrap outputs in facetReindex + const [channels, maybeReindex] = exclusiveFacets(options); return [ basic(options, (data, facets) => { - let plan; - ({facets, plan} = facetReindex(facets, data.length)); // make facets exclusive + let plan, n; + ({facets, plan, n} = maybeReindex(facets, data)); const XS = x == null ? undefined : valueof(data, x); const YS = valueof(data, y, Float64Array); const ZS = valueof(data, z); @@ -159,8 +159,6 @@ function stack(x, y = one, ky, {offset, order, reverse}, options) { const Y = maybeExpand(YS, plan); const Z = maybeExpand(ZS, plan); const O = order && maybeExpand(order(data, XS, YS, ZS), plan); - const n = plan ? plan.length : data.length; - for (const o of outputs) o(data, plan); // expand any extra channels const Y1 = setY1(new Float64Array(n)); const Y2 = setY2(new Float64Array(n)); const facetstacks = []; @@ -186,7 +184,7 @@ function stack(x, y = one, ky, {offset, order, reverse}, options) { X, Y1, Y2, - other + channels ]; } From 218cd707821de901aaef5424ee9524973f4d6e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 6 Oct 2022 17:45:26 +0200 Subject: [PATCH 06/18] centralize channel definitions --- src/channel.js | 5 +++++ src/legends/swatches.js | 13 +++++++------ src/marks/cell.js | 7 ++++--- src/marks/dot.js | 10 +++++----- src/marks/image.js | 10 +++++----- src/marks/text.js | 7 +++---- src/marks/vector.js | 7 ++++--- src/style.js | 15 ++++++++------- src/transforms/bin.js | 6 +++--- src/transforms/group.js | 6 +++--- 10 files changed, 47 insertions(+), 39 deletions(-) diff --git a/src/channel.js b/src/channel.js index 6bc44c53bf..2e0b5132fc 100644 --- a/src/channel.js +++ b/src/channel.js @@ -37,6 +37,11 @@ export const knownChannels = { weight: {definition: maybeNumberChannel} // density }; +export function definition(name, value, defaultValue) { + const {definition} = knownChannels[name]; + return definition ? definition(value, defaultValue) : value === undefined ? [undefined, defaultValue] : [value]; +} + // TODO Type coercion? export function Channel(data, {scale, type, value, filter, hint}) { return { diff --git a/src/legends/swatches.js b/src/legends/swatches.js index ac093651b1..758fdc11fe 100644 --- a/src/legends/swatches.js +++ b/src/legends/swatches.js @@ -2,7 +2,8 @@ import {path} from "d3"; import {inferFontVariant} from "../axes.js"; import {maybeAutoTickFormat} from "../axis.js"; import {Context, create} from "../context.js"; -import {isNoneish, maybeColorChannel, maybeNumberChannel} from "../options.js"; +import {isNoneish} from "../options.js"; +import {definition} from "../channel.js"; import {isOrdinalScale, isThresholdScale} from "../scales.js"; import {applyInlineStyles, impliedString, maybeClassName} from "../style.js"; @@ -42,14 +43,14 @@ export function legendSymbols( } = {}, scale ) { - const [vf, cf] = maybeColorChannel(fill); - const [vs, cs] = maybeColorChannel(stroke); + const [vf, cf] = definition("fill", fill); + const [vs, cs] = definition("stroke", stroke); const sf = maybeScale(scale, vf); const ss = maybeScale(scale, vs); const size = r * r * Math.PI; - fillOpacity = maybeNumberChannel(fillOpacity)[1]; - strokeOpacity = maybeNumberChannel(strokeOpacity)[1]; - strokeWidth = maybeNumberChannel(strokeWidth)[1]; + fillOpacity = definition("fillOpacity", fillOpacity)[1]; + strokeOpacity = definition("strokeOpacity", strokeOpacity)[1]; + strokeWidth = definition("strokeWidth", strokeWidth)[1]; return legendItems( symbol, options, diff --git a/src/marks/cell.js b/src/marks/cell.js index 6df7557432..38f10d8ceb 100644 --- a/src/marks/cell.js +++ b/src/marks/cell.js @@ -1,4 +1,5 @@ -import {identity, indexOf, maybeColorChannel, maybeTuple} from "../options.js"; +import {identity, indexOf, maybeTuple} from "../options.js"; +import {definition} from "../channel.js"; import {applyTransform} from "../style.js"; import {AbstractBar} from "./bar.js"; @@ -54,7 +55,7 @@ export function cell(data, options = {}) { */ export function cellX(data, options = {}) { let {x = indexOf, fill, stroke, ...remainingOptions} = options; - if (fill === undefined && maybeColorChannel(stroke)[0] === undefined) fill = identity; + if (fill === undefined && definition("stroke", stroke)[0] === undefined) fill = identity; return new Cell(data, {...remainingOptions, x, fill, stroke}); } @@ -72,6 +73,6 @@ export function cellX(data, options = {}) { */ export function cellY(data, options = {}) { let {y = indexOf, fill, stroke, ...remainingOptions} = options; - if (fill === undefined && maybeColorChannel(stroke)[0] === undefined) fill = identity; + if (fill === undefined && definition("stroke", stroke)[0] === undefined) fill = identity; return new Cell(data, {...remainingOptions, y, fill, stroke}); } diff --git a/src/marks/dot.js b/src/marks/dot.js index 6b3e380cc5..077858f32c 100644 --- a/src/marks/dot.js +++ b/src/marks/dot.js @@ -1,7 +1,8 @@ import {path, symbolCircle} from "d3"; import {create} from "../context.js"; import {positive} from "../defined.js"; -import {identity, maybeFrameAnchor, maybeNumberChannel, maybeTuple} from "../options.js"; +import {identity, maybeFrameAnchor, maybeTuple} from "../options.js"; +import {definition} from "../channel.js"; import {Mark} from "../plot.js"; import { applyChannelStyles, @@ -10,7 +11,6 @@ import { applyIndirectStyles, applyTransform } from "../style.js"; -import {maybeSymbolChannel} from "../symbols.js"; import {sort} from "../transforms/basic.js"; import {maybeIntervalMidX, maybeIntervalMidY} from "../transforms/interval.js"; @@ -24,9 +24,9 @@ const defaults = { export class Dot extends Mark { constructor(data, options = {}) { const {x, y, r, rotate, symbol = symbolCircle, frameAnchor} = options; - const [vrotate, crotate] = maybeNumberChannel(rotate, 0); - const [vsymbol, csymbol] = maybeSymbolChannel(symbol); - const [vr, cr] = maybeNumberChannel(r, vsymbol == null ? 3 : 4.5); + const [vrotate, crotate] = definition("rotate", rotate, 0); + const [vsymbol, csymbol] = definition("symbol", symbol); + const [vr, cr] = definition("r", r, vsymbol == null ? 3 : 4.5); super( data, { diff --git a/src/marks/image.js b/src/marks/image.js index 39ba3ff667..cb1c159f31 100644 --- a/src/marks/image.js +++ b/src/marks/image.js @@ -1,6 +1,6 @@ import {create} from "../context.js"; import {positive} from "../defined.js"; -import {maybeFrameAnchor, maybeNumberChannel, maybeTuple, string} from "../options.js"; +import {maybeFrameAnchor, maybeTuple, string} from "../options.js"; import {Mark} from "../plot.js"; import { applyChannelStyles, @@ -11,7 +11,7 @@ import { impliedString, applyFrameAnchor } from "../style.js"; -import {maybePathChannel} from "../channel.js"; +import {definition} from "../channel.js"; const defaults = { ariaLabel: "image", @@ -24,9 +24,9 @@ export class Image extends Mark { let {x, y, width, height, src, preserveAspectRatio, crossOrigin, frameAnchor} = options; if (width === undefined && height !== undefined) width = height; else if (height === undefined && width !== undefined) height = width; - const [vs, cs] = maybePathChannel(src); - const [vw, cw] = maybeNumberChannel(width, 16); - const [vh, ch] = maybeNumberChannel(height, 16); + const [vs, cs] = definition("src", src); + const [vw, cw] = definition("width", width, 16); + const [vh, ch] = definition("height", height, 16); super( data, { diff --git a/src/marks/text.js b/src/marks/text.js index 9edc294bfe..df36594dfa 100644 --- a/src/marks/text.js +++ b/src/marks/text.js @@ -6,7 +6,6 @@ import { indexOf, identity, string, - maybeNumberChannel, maybeTuple, numberChannel, isNumeric, @@ -27,7 +26,7 @@ import { applyFrameAnchor } from "../style.js"; import {maybeIntervalMidX, maybeIntervalMidY} from "../transforms/interval.js"; -import {maybeFontSizeChannel} from "../channel.js"; +import {definition} from "../channel.js"; const defaults = { ariaLabel: "text", @@ -55,8 +54,8 @@ export class Text extends Mark { fontWeight, rotate } = options; - const [vrotate, crotate] = maybeNumberChannel(rotate, 0); - const [vfontSize, cfontSize] = maybeFontSizeChannel(fontSize); + const [vrotate, crotate] = definition("rotate", rotate, 0); + const [vfontSize, cfontSize] = definition("fontSize", fontSize); super( data, { diff --git a/src/marks/vector.js b/src/marks/vector.js index 7dc00e5700..e64b970d7f 100644 --- a/src/marks/vector.js +++ b/src/marks/vector.js @@ -1,6 +1,7 @@ import {create} from "../context.js"; import {radians} from "../math.js"; -import {maybeFrameAnchor, maybeNumberChannel, maybeTuple, keyword, identity} from "../options.js"; +import {maybeFrameAnchor, maybeTuple, keyword, identity} from "../options.js"; +import {definition} from "../channel.js"; import {Mark} from "../plot.js"; import { applyChannelStyles, @@ -21,8 +22,8 @@ const defaults = { export class Vector extends Mark { constructor(data, options = {}) { const {x, y, length, rotate, anchor = "middle", frameAnchor} = options; - const [vl, cl] = maybeNumberChannel(length, 12); - const [vr, cr] = maybeNumberChannel(rotate, 0); + const [vl, cl] = definition("length", length, 12); + const [vr, cr] = definition("rotate", rotate, 0); super( data, { diff --git a/src/style.js b/src/style.js index 3547c50f1e..42e114944d 100644 --- a/src/style.js +++ b/src/style.js @@ -1,8 +1,9 @@ import {group, namespaces} from "d3"; import {defined, nonempty} from "./defined.js"; import {formatDefault} from "./format.js"; -import {string, number, maybeColorChannel, maybeNumberChannel, isNoneish, isNone, isRound, keyof} from "./options.js"; +import {string, number, isNoneish, isNone, isRound, keyof} from "./options.js"; import {warn} from "./warnings.js"; +import {definition} from "./channel.js"; export const offset = typeof window !== "undefined" && window.devicePixelRatio > 1 ? 0 : 0.5; @@ -70,11 +71,11 @@ export function styles( if (isNoneish(defaultStroke) && !isNoneish(stroke)) defaultFill = "none"; } - const [vfill, cfill] = maybeColorChannel(fill, defaultFill); - const [vfillOpacity, cfillOpacity] = maybeNumberChannel(fillOpacity, defaultFillOpacity); - const [vstroke, cstroke] = maybeColorChannel(stroke, defaultStroke); - const [vstrokeOpacity, cstrokeOpacity] = maybeNumberChannel(strokeOpacity, defaultStrokeOpacity); - const [vopacity, copacity] = maybeNumberChannel(opacity); + const [vfill, cfill] = definition("fill", fill, defaultFill); + const [vfillOpacity, cfillOpacity] = definition("fillOpacity", fillOpacity, defaultFillOpacity); + const [vstroke, cstroke] = definition("stroke", stroke, defaultStroke); + const [vstrokeOpacity, cstrokeOpacity] = definition("strokeOpacity", strokeOpacity, defaultStrokeOpacity); + const [vopacity, copacity] = definition("opacity", opacity); // For styles that have no effect if there is no stroke, only apply the // defaults if the stroke is not the constant none. (If stroke is a channel, @@ -94,7 +95,7 @@ export function styles( if (!isNone(cfill) && paintOrder === undefined) paintOrder = defaultPaintOrder; } - const [vstrokeWidth, cstrokeWidth] = maybeNumberChannel(strokeWidth); + const [vstrokeWidth, cstrokeWidth] = definition("strokeWidth", strokeWidth); // Some marks don’t support fill (e.g., tick and rule). if (defaultFill !== null) { diff --git a/src/transforms/bin.js b/src/transforms/bin.js index f55865221e..8a877f8bfe 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -7,12 +7,12 @@ import { thresholdSturges, utcTickInterval } from "d3"; +import {definition} from "../channel.js"; import { valueof, identity, maybeColumn, maybeTuple, - maybeColorChannel, maybeValue, mid, labelof, @@ -158,8 +158,8 @@ function binn( ...options } = inputs; const [GZ, setGZ] = maybeColumn(z); - const [vfill] = maybeColorChannel(fill); - const [vstroke] = maybeColorChannel(stroke); + const [vfill] = definition("fill", fill); + const [vstroke] = definition("stroke", stroke); const [GF, setGF] = maybeColumn(vfill); const [GS, setGS] = maybeColumn(vstroke); diff --git a/src/transforms/group.js b/src/transforms/group.js index 5aa82be38a..aa66e2053d 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -17,7 +17,6 @@ import { import {ascendingDefined} from "../defined.js"; import { valueof, - maybeColorChannel, maybeInput, maybeTuple, maybeColumn, @@ -30,6 +29,7 @@ import { second, percentile } from "../options.js"; +import {definition} from "../channel.js"; import {basic} from "./basic.js"; /** @@ -135,8 +135,8 @@ function groupn( ...options } = inputs; const [GZ, setGZ] = maybeColumn(z); - const [vfill] = maybeColorChannel(fill); - const [vstroke] = maybeColorChannel(stroke); + const [vfill] = definition("fill", fill); + const [vstroke] = definition("stroke", stroke); const [GF, setGF] = maybeColumn(vfill); const [GS, setGS] = maybeColumn(vstroke); From e733bd0c3b200ef93cb4aa236017114ed43ac590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 6 Oct 2022 17:56:55 +0200 Subject: [PATCH 07/18] centralize channel scale definitions --- src/channel.js | 2 +- src/marks/area.js | 8 ++++---- src/marks/arrow.js | 8 ++++---- src/marks/bar.js | 12 ++++++------ src/marks/cell.js | 4 ++-- src/marks/delaunay.js | 12 ++++++------ src/marks/density.js | 4 ++-- src/marks/dot.js | 8 ++++---- src/marks/image.js | 4 ++-- src/marks/line.js | 4 ++-- src/marks/linearRegression.js | 4 ++-- src/marks/link.js | 8 ++++---- src/marks/rect.js | 8 ++++---- src/marks/rule.js | 12 ++++++------ src/marks/text.js | 4 ++-- src/marks/tick.js | 8 ++++---- src/marks/vector.js | 6 +++--- src/plot.js | 12 +++++++----- 18 files changed, 65 insertions(+), 63 deletions(-) diff --git a/src/channel.js b/src/channel.js index 2e0b5132fc..910329f39a 100644 --- a/src/channel.js +++ b/src/channel.js @@ -30,7 +30,7 @@ export const knownChannels = { rotate: {definition: maybeNumberChannel}, // dot, text fontSize: {definition: maybeFontSizeChannel}, // text text: {}, // text - length: {definition: maybeNumberChannel}, // vector + length: {scale: "length", definition: maybeNumberChannel}, // vector width: {definition: maybeNumberChannel}, // image height: {definition: maybeNumberChannel}, // image src: {definition: maybePathChannel}, // image diff --git a/src/marks/area.js b/src/marks/area.js index be0bbc60af..cbfa326640 100644 --- a/src/marks/area.js +++ b/src/marks/area.js @@ -28,10 +28,10 @@ export class Area extends Mark { super( data, { - x1: {value: x1, scale: "x"}, - y1: {value: y1, scale: "y"}, - x2: {value: x2, scale: "x", optional: true}, - y2: {value: y2, scale: "y", optional: true}, + x1: {value: x1}, + y1: {value: y1}, + x2: {value: x2, optional: true}, + y2: {value: y2, optional: true}, z: {value: maybeZ(options), optional: true} }, options, diff --git a/src/marks/arrow.js b/src/marks/arrow.js index 79fbefa392..65304acceb 100644 --- a/src/marks/arrow.js +++ b/src/marks/arrow.js @@ -31,10 +31,10 @@ export class Arrow extends Mark { super( data, { - x1: {value: x1, scale: "x"}, - y1: {value: y1, scale: "y"}, - x2: {value: x2, scale: "x", optional: true}, - y2: {value: y2, scale: "y", optional: true} + x1: {value: x1}, + y1: {value: y1}, + x2: {value: x2, optional: true}, + y2: {value: y2, optional: true} }, options, defaults diff --git a/src/marks/bar.js b/src/marks/bar.js index 46f83ece77..bdd534c947 100644 --- a/src/marks/bar.js +++ b/src/marks/bar.js @@ -77,9 +77,9 @@ export class BarX extends AbstractBar { super( data, { - x1: {value: x1, scale: "x"}, - x2: {value: x2, scale: "x"}, - y: {value: y, scale: "y", type: "band", optional: true} + x1: {value: x1}, + x2: {value: x2}, + y: {value: y, type: "band", optional: true} }, options, defaults @@ -106,9 +106,9 @@ export class BarY extends AbstractBar { super( data, { - y1: {value: y1, scale: "y"}, - y2: {value: y2, scale: "y"}, - x: {value: x, scale: "x", type: "band", optional: true} + y1: {value: y1}, + y2: {value: y2}, + x: {value: x, type: "band", optional: true} }, options, defaults diff --git a/src/marks/cell.js b/src/marks/cell.js index 38f10d8ceb..d2b4ba7991 100644 --- a/src/marks/cell.js +++ b/src/marks/cell.js @@ -12,8 +12,8 @@ export class Cell extends AbstractBar { super( data, { - x: {value: x, scale: "x", type: "band", optional: true}, - y: {value: y, scale: "y", type: "band", optional: true} + x: {value: x, type: "band", optional: true}, + y: {value: y, type: "band", optional: true} }, options, defaults diff --git a/src/marks/delaunay.js b/src/marks/delaunay.js index e80603db4f..9c49f51462 100644 --- a/src/marks/delaunay.js +++ b/src/marks/delaunay.js @@ -54,8 +54,8 @@ class DelaunayLink extends Mark { super( data, { - x: {value: x, scale: "x", optional: true}, - y: {value: y, scale: "y", optional: true}, + x: {value: x, optional: true}, + y: {value: y, optional: true}, z: {value: z, optional: true} }, options, @@ -146,8 +146,8 @@ class AbstractDelaunayMark extends Mark { super( data, { - x: {value: x, scale: "x", optional: true}, - y: {value: y, scale: "y", optional: true}, + x: {value: x, optional: true}, + y: {value: y, optional: true}, z: {value: zof(options), optional: true} }, options, @@ -214,8 +214,8 @@ class Voronoi extends Mark { super( data, { - x: {value: x, scale: "x", optional: true}, - y: {value: y, scale: "y", optional: true}, + x: {value: x, optional: true}, + y: {value: y, optional: true}, z: {value: z, optional: true} }, options, diff --git a/src/marks/density.js b/src/marks/density.js index a8d9cf2d93..b6ce5f5780 100644 --- a/src/marks/density.js +++ b/src/marks/density.js @@ -30,8 +30,8 @@ export class Density extends Mark { super( data, { - x: {value: x, scale: "x", optional: true}, - y: {value: y, scale: "y", optional: true}, + x: {value: x, optional: true}, + y: {value: y, optional: true}, z: {value: maybeZ({z, fill, stroke}), optional: true}, weight: {value: weight, optional: true} }, diff --git a/src/marks/dot.js b/src/marks/dot.js index 077858f32c..46c810468a 100644 --- a/src/marks/dot.js +++ b/src/marks/dot.js @@ -30,11 +30,11 @@ export class Dot extends Mark { super( data, { - x: {value: x, scale: "x", optional: true}, - y: {value: y, scale: "y", optional: true}, - r: {value: vr, scale: "r", filter: positive, optional: true}, + x: {value: x, optional: true}, + y: {value: y, optional: true}, + r: {value: vr, filter: positive, optional: true}, rotate: {value: vrotate, optional: true}, - symbol: {value: vsymbol, scale: "symbol", optional: true} + symbol: {value: vsymbol, optional: true} }, options.sort === undefined && options.reverse === undefined ? sort({channel: "r", order: "descending"}, options) diff --git a/src/marks/image.js b/src/marks/image.js index cb1c159f31..c98542e585 100644 --- a/src/marks/image.js +++ b/src/marks/image.js @@ -30,8 +30,8 @@ export class Image extends Mark { super( data, { - x: {value: x, scale: "x", optional: true}, - y: {value: y, scale: "y", optional: true}, + x: {value: x, optional: true}, + y: {value: y, optional: true}, width: {value: vw, filter: positive, optional: true}, height: {value: vh, filter: positive, optional: true}, src: {value: vs, optional: true} diff --git a/src/marks/line.js b/src/marks/line.js index a4db931266..c690c060ba 100644 --- a/src/marks/line.js +++ b/src/marks/line.js @@ -29,8 +29,8 @@ export class Line extends Mark { super( data, { - x: {value: x, scale: "x"}, - y: {value: y, scale: "y"}, + x: {value: x}, + y: {value: y}, z: {value: maybeZ(options), optional: true} }, options, diff --git a/src/marks/linearRegression.js b/src/marks/linearRegression.js index 7cdd44902d..b3f4e93bc3 100644 --- a/src/marks/linearRegression.js +++ b/src/marks/linearRegression.js @@ -23,8 +23,8 @@ class LinearRegression extends Mark { super( data, { - x: {value: x, scale: "x"}, - y: {value: y, scale: "y"}, + x: {value: x}, + y: {value: y}, z: {value: maybeZ(options), optional: true} }, options, diff --git a/src/marks/link.js b/src/marks/link.js index b4d9529c7d..1b5c3f66e9 100644 --- a/src/marks/link.js +++ b/src/marks/link.js @@ -18,10 +18,10 @@ export class Link extends Mark { super( data, { - x1: {value: x1, scale: "x"}, - y1: {value: y1, scale: "y"}, - x2: {value: x2, scale: "x", optional: true}, - y2: {value: y2, scale: "y", optional: true} + x1: {value: x1}, + y1: {value: y1}, + x2: {value: x2, optional: true}, + y2: {value: y2, optional: true} }, options, defaults diff --git a/src/marks/rect.js b/src/marks/rect.js index 48acd7131f..38f81f7808 100644 --- a/src/marks/rect.js +++ b/src/marks/rect.js @@ -36,10 +36,10 @@ export class Rect extends Mark { super( data, { - x1: {value: x1, scale: "x", optional: true}, - y1: {value: y1, scale: "y", optional: true}, - x2: {value: x2, scale: "x", optional: true}, - y2: {value: y2, scale: "y", optional: true} + x1: {value: x1, optional: true}, + y1: {value: y1, optional: true}, + x2: {value: x2, optional: true}, + y2: {value: y2, optional: true} }, options, defaults diff --git a/src/marks/rule.js b/src/marks/rule.js index caedd29d89..4de2936ec1 100644 --- a/src/marks/rule.js +++ b/src/marks/rule.js @@ -17,9 +17,9 @@ export class RuleX extends Mark { super( data, { - x: {value: x, scale: "x", optional: true}, - y1: {value: y1, scale: "y", optional: true}, - y2: {value: y2, scale: "y", optional: true} + x: {value: x, optional: true}, + y1: {value: y1, optional: true}, + y2: {value: y2, optional: true} }, options, defaults @@ -65,9 +65,9 @@ export class RuleY extends Mark { super( data, { - y: {value: y, scale: "y", optional: true}, - x1: {value: x1, scale: "x", optional: true}, - x2: {value: x2, scale: "x", optional: true} + y: {value: y, optional: true}, + x1: {value: x1, optional: true}, + x2: {value: x2, optional: true} }, options, defaults diff --git a/src/marks/text.js b/src/marks/text.js index df36594dfa..2b480fd2d4 100644 --- a/src/marks/text.js +++ b/src/marks/text.js @@ -59,8 +59,8 @@ export class Text extends Mark { super( data, { - x: {value: x, scale: "x", optional: true}, - y: {value: y, scale: "y", optional: true}, + x: {value: x, optional: true}, + y: {value: y, optional: true}, fontSize: {value: vfontSize, optional: true}, rotate: {value: numberChannel(vrotate), optional: true}, text: {value: text, filter: nonempty} diff --git a/src/marks/tick.js b/src/marks/tick.js index 9303b7c46d..daf4670488 100644 --- a/src/marks/tick.js +++ b/src/marks/tick.js @@ -40,8 +40,8 @@ export class TickX extends AbstractTick { super( data, { - x: {value: x, scale: "x"}, - y: {value: y, scale: "y", type: "band", optional: true} + x: {value: x}, + y: {value: y, type: "band", optional: true} }, options ); @@ -73,8 +73,8 @@ export class TickY extends AbstractTick { super( data, { - y: {value: y, scale: "y"}, - x: {value: x, scale: "x", type: "band", optional: true} + y: {value: y}, + x: {value: x, type: "band", optional: true} }, options ); diff --git a/src/marks/vector.js b/src/marks/vector.js index e64b970d7f..8c252647c1 100644 --- a/src/marks/vector.js +++ b/src/marks/vector.js @@ -27,9 +27,9 @@ export class Vector extends Mark { super( data, { - x: {value: x, scale: "x", optional: true}, - y: {value: y, scale: "y", optional: true}, - length: {value: vl, scale: "length", optional: true}, + x: {value: x, optional: true}, + y: {value: y, optional: true}, + length: {value: vl, optional: true}, rotate: {value: vr, optional: true} }, options, diff --git a/src/plot.js b/src/plot.js index 239438d95c..5a89759fe5 100644 --- a/src/plot.js +++ b/src/plot.js @@ -675,11 +675,13 @@ export class Mark { if (extraChannels !== undefined) channels = {...maybeNamed(extraChannels), ...channels}; if (defaults !== undefined) channels = {...styles(this, options, defaults), ...channels}; this.channels = Object.fromEntries( - Object.entries(channels).filter(([name, {value, optional}]) => { - if (value != null) return true; - if (optional) return false; - throw new Error(`missing channel value: ${name}`); - }) + Object.entries(channels) + .filter(([name, {value, optional}]) => { + if (value != null) return true; + if (optional) return false; + throw new Error(`missing channel value: ${name}`); + }) + .map(([name, {scale = knownChannels[name].scale, ...rest}]) => [name, {scale, ...rest}]) ); this.dx = +dx || 0; this.dy = +dy || 0; From 6614ba8f80d6d106dd2c0e3cd7c988f67c14ab6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 7 Oct 2022 09:27:18 +0200 Subject: [PATCH 08/18] use a map and export a registerChannel method; with a pretty unimaginative test for a customMark. --- src/channel.js | 66 +++++++++++++++------------- src/facet.js | 13 ++---- src/index.js | 1 + src/plot.js | 5 ++- test/output/customMark.svg | 88 ++++++++++++++++++++++++++++++++++++++ test/plots/custom-mark.js | 47 ++++++++++++++++++++ test/plots/index.js | 1 + 7 files changed, 181 insertions(+), 40 deletions(-) create mode 100644 test/output/customMark.svg create mode 100644 test/plots/custom-mark.js diff --git a/src/channel.js b/src/channel.js index 910329f39a..4dcbef2203 100644 --- a/src/channel.js +++ b/src/channel.js @@ -4,41 +4,49 @@ import {registry} from "./scales/index.js"; import {maybeReduce} from "./transforms/group.js"; import {maybeColorChannel, maybeNumberChannel} from "./options.js"; import {maybeSymbolChannel} from "./symbols.js"; +import {warn} from "./warnings.js"; // An array of known channels, with an associated scale name, and a definition // that returns [variable, undefined] if variable, or [undefined, constant] if // constant (such as "#eee" for the color channel) -export const knownChannels = { - x: {scale: "x"}, - x1: {scale: "x"}, - x2: {scale: "x"}, - y: {scale: "y"}, - y1: {scale: "y"}, - y2: {scale: "y"}, - z: {}, - ariaLabel: {}, - href: {}, - title: {}, - fill: {scale: "color", definition: maybeColorChannel}, - stroke: {scale: "color", definition: maybeColorChannel}, - fillOpacity: {scale: "opacity", definition: maybeNumberChannel}, - strokeOpacity: {scale: "opacity", definition: maybeNumberChannel}, - opacity: {scale: "opacity", definition: maybeNumberChannel}, - strokeWidth: {definition: maybeNumberChannel}, - symbol: {scale: "symbol", definition: maybeSymbolChannel}, // dot - r: {scale: "r", definition: maybeNumberChannel}, // dot - rotate: {definition: maybeNumberChannel}, // dot, text - fontSize: {definition: maybeFontSizeChannel}, // text - text: {}, // text - length: {scale: "length", definition: maybeNumberChannel}, // vector - width: {definition: maybeNumberChannel}, // image - height: {definition: maybeNumberChannel}, // image - src: {definition: maybePathChannel}, // image - weight: {definition: maybeNumberChannel} // density -}; +export const knownChannels = new Map([ + ["x", {scale: "x"}], + ["x1", {scale: "x"}], + ["x2", {scale: "x"}], + ["y", {scale: "y"}], + ["y1", {scale: "y"}], + ["y2", {scale: "y"}], + ["z", {}], + ["ariaLabel", {}], + ["href", {}], + ["title", {}], + ["fill", {scale: "color", definition: maybeColorChannel}], + ["stroke", {scale: "color", definition: maybeColorChannel}], + ["fillOpacity", {scale: "opacity", definition: maybeNumberChannel}], + ["strokeOpacity", {scale: "opacity", definition: maybeNumberChannel}], + ["opacity", {scale: "opacity", definition: maybeNumberChannel}], + ["strokeWidth", {definition: maybeNumberChannel}], + ["symbol", {scale: "symbol", definition: maybeSymbolChannel}], // dot + ["r", {scale: "r", definition: maybeNumberChannel}], // dot + ["rotate", {definition: maybeNumberChannel}], // dot, text + ["fontSize", {definition: maybeFontSizeChannel}], // text + ["text", {}], // text + ["length", {scale: "length", definition: maybeNumberChannel}], // vector + ["width", {definition: maybeNumberChannel}], // image + ["height", {definition: maybeNumberChannel}], // image + ["src", {definition: maybePathChannel}], // image + ["weight", {definition: maybeNumberChannel}] // density +]); + +export function registerChannel(name, {scale, definition} = {}) { + knownChannels.set(name, {scale, definition}); +} export function definition(name, value, defaultValue) { - const {definition} = knownChannels[name]; + if (!knownChannels.has(name)) { + warn(`The ${name} channel is not registered and might be incompatible with some transforms.`); + } + const {definition} = knownChannels.get(name) || {}; return definition ? definition(value, defaultValue) : value === undefined ? [undefined, defaultValue] : [value]; } diff --git a/src/facet.js b/src/facet.js index aba6e59afc..557a0ba553 100644 --- a/src/facet.js +++ b/src/facet.js @@ -1,4 +1,4 @@ -import {isIterable, labelof, slice, valueof} from "./options.js"; +import {labelof, slice, valueof} from "./options.js"; import {knownChannels} from "./channel.js"; function facetReindex(facets, n) { @@ -51,16 +51,11 @@ export function maybeExpand(X, plan) { } // Iterate over the options and pull out any that represent columns of values. -function maybeExpandChannels({expandChannels, ...options}) { - if (expandChannels == null) { - expandChannels = Object.entries(knownChannels) - .filter(([name, {definition = (value) => [value]}]) => definition(options[name])[0] != null) - .map(([name]) => name); - } else if (!isIterable(expandChannels)) throw new Error(`the expandChannels option is not iterable`); +function maybeExpandChannels(options) { const channels = {}; let data, plan; - for (const name of expandChannels) { - const value = options[name]; + for (const [name, {definition = (value) => [value]}] of knownChannels) { + const value = definition(options[name])[0]; if (value != null) { channels[name] = { transform: () => maybeExpand(valueof(data, value), plan), diff --git a/src/index.js b/src/index.js index a6e5dc7a48..f95ea98394 100644 --- a/src/index.js +++ b/src/index.js @@ -34,3 +34,4 @@ export {treeNode, treeLink} from "./transforms/tree.js"; export {formatIsoDate, formatWeekday, formatMonth} from "./format.js"; export {scale} from "./scales.js"; export {legend} from "./legends.js"; +export {registerChannel} from "./channel.js"; diff --git a/src/plot.js b/src/plot.js index 5a89759fe5..b751d34b1c 100644 --- a/src/plot.js +++ b/src/plot.js @@ -677,11 +677,12 @@ export class Mark { this.channels = Object.fromEntries( Object.entries(channels) .filter(([name, {value, optional}]) => { + if (!knownChannels.has(name)) warn(`The ${name} channel is not registered.`); if (value != null) return true; if (optional) return false; throw new Error(`missing channel value: ${name}`); }) - .map(([name, {scale = knownChannels[name].scale, ...rest}]) => [name, {scale, ...rest}]) + .map(([name, {scale = (knownChannels.get(name) || {}).scale, ...rest}]) => [name, {scale, ...rest}]) ); this.dx = +dx || 0; this.dy = +dy || 0; @@ -760,7 +761,7 @@ function inferChannelScale(channels) { for (const name in channels) { const channel = channels[name]; const {scale} = channel; - if (scale === true) channel.scale = knownChannels[name].scale; + if (scale === true) channel.scale = knownChannels.get(name).scale; } } diff --git a/test/output/customMark.svg b/test/output/customMark.svg new file mode 100644 index 0000000000..00a98f435f --- /dev/null +++ b/test/output/customMark.svg @@ -0,0 +1,88 @@ + + + + + 0 + + + 5 + + + 10 + + + 15 + + + 20 + + + 25 + + + 30 + + + 35 + + + 40 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/custom-mark.js b/test/plots/custom-mark.js new file mode 100644 index 0000000000..1384ce27b5 --- /dev/null +++ b/test/plots/custom-mark.js @@ -0,0 +1,47 @@ +import * as d3 from "d3"; +import * as Plot from "@observablehq/plot"; + +Plot.registerChannel("alpha", {scale: "x"}); +Plot.registerChannel("beta"); + +class CustomMark extends Plot.Mark { + constructor(data, options = {}) { + const {alpha, beta} = options; + super( + data, + { + alpha: {value: alpha}, + beta: {value: beta, optional: true} + }, + options, + {} + ); + } + render(index, scales, channels, dimensions, context) { + const {alpha: A, beta: B} = channels; + return d3 + .create("svg:g", context) + .call((g) => + g + .selectAll() + .data(index) + .enter() + .append("circle") + .attr("transform", (i) => `translate(${A[i]}, ${6 + A[i] / 20})`) + .attr("r", B ? (i) => Math.sqrt(B[i]) : 2) + ) + .node(); + } +} + +function customMark(data, options) { + return new CustomMark(data, options); +} + +export default async function () { + return customMark(d3.range(41), { + alpha: (d) => d, + beta: (d) => d, + stroke: "red" + }).plot(); +} diff --git a/test/plots/index.js b/test/plots/index.js index dbd041ec4c..3999d39e2c 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -42,6 +42,7 @@ export {default as crimeanWarArrow} from "./crimean-war-arrow.js"; export {default as crimeanWarLine} from "./crimean-war-line.js"; export {default as crimeanWarOverlapped} from "./crimean-war-overlapped.js"; export {default as crimeanWarStacked} from "./crimean-war-stacked.js"; +export {default as customMark} from "./custom-mark.js"; export {default as d3Survey2015Comfort} from "./d3-survey-2015-comfort.js"; export {default as d3Survey2015Why} from "./d3-survey-2015-why.js"; export {default as darkerDodge} from "./darker-dodge.js"; From 21aaf4e62ae9c336303acc493b39b80d5d8bffc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 10 Oct 2022 11:17:59 -0700 Subject: [PATCH 09/18] Update src/transforms/dodge.js Co-authored-by: Mike Bostock --- src/transforms/dodge.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/transforms/dodge.js b/src/transforms/dodge.js index 3232574934..37bd375cd7 100644 --- a/src/transforms/dodge.js +++ b/src/transforms/dodge.js @@ -4,6 +4,7 @@ import {identity, maybeNamed, number, valueof} from "../options.js"; import {coerceNumbers} from "../scales.js"; import {initializer} from "./basic.js"; import {exclusiveFacets, maybeExpand} from "../facet.js"; + const anchorXLeft = ({marginLeft}) => [1, marginLeft]; const anchorXRight = ({width, marginRight}) => [-1, width - marginRight]; const anchorXMiddle = ({width, marginLeft, marginRight}) => [0, (marginLeft + width - marginRight) / 2]; From 01a6714d34a8abfdd373a8c5cdc534e564921737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 10 Oct 2022 11:18:10 -0700 Subject: [PATCH 10/18] Update src/plot.js Co-authored-by: Mike Bostock --- src/plot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plot.js b/src/plot.js index b751d34b1c..7a64f6c1ab 100644 --- a/src/plot.js +++ b/src/plot.js @@ -761,7 +761,7 @@ function inferChannelScale(channels) { for (const name in channels) { const channel = channels[name]; const {scale} = channel; - if (scale === true) channel.scale = knownChannels.get(name).scale; + if (scale === true) channel.scale = knownChannels.get(name)?.scale ?? null; } } From 6424882bb0802b0e42f595b5d08e6998c3d0b971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 10 Oct 2022 11:18:21 -0700 Subject: [PATCH 11/18] Update src/plot.js Co-authored-by: Mike Bostock --- src/plot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plot.js b/src/plot.js index 7a64f6c1ab..9256d13114 100644 --- a/src/plot.js +++ b/src/plot.js @@ -682,7 +682,7 @@ export class Mark { if (optional) return false; throw new Error(`missing channel value: ${name}`); }) - .map(([name, {scale = (knownChannels.get(name) || {}).scale, ...rest}]) => [name, {scale, ...rest}]) + .map(([name, {scale = knownChannels.get(name)?.scale, ...rest}]) => [name, {scale, ...rest}]) ); this.dx = +dx || 0; this.dy = +dy || 0; From 252ca0a93f2fb92806138450b7610e225b22a149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 10 Oct 2022 11:18:39 -0700 Subject: [PATCH 12/18] Update src/channel.js Co-authored-by: Mike Bostock --- src/channel.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/channel.js b/src/channel.js index 4dcbef2203..bebde3b70c 100644 --- a/src/channel.js +++ b/src/channel.js @@ -47,7 +47,9 @@ export function definition(name, value, defaultValue) { warn(`The ${name} channel is not registered and might be incompatible with some transforms.`); } const {definition} = knownChannels.get(name) || {}; - return definition ? definition(value, defaultValue) : value === undefined ? [undefined, defaultValue] : [value]; + return definition !== undefined ? definition(value, defaultValue) + : value === undefined ? [undefined, defaultValue] + : [value]; } // TODO Type coercion? From 9a41a9df14b76d10675f1d69edf0432afe7406c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 10 Oct 2022 11:22:50 -0700 Subject: [PATCH 13/18] channelRegistry --- src/channel.js | 14 ++++++++------ src/facet.js | 4 ++-- src/plot.js | 8 ++++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/channel.js b/src/channel.js index bebde3b70c..db950e7b42 100644 --- a/src/channel.js +++ b/src/channel.js @@ -9,7 +9,7 @@ import {warn} from "./warnings.js"; // An array of known channels, with an associated scale name, and a definition // that returns [variable, undefined] if variable, or [undefined, constant] if // constant (such as "#eee" for the color channel) -export const knownChannels = new Map([ +export const channelRegistry = new Map([ ["x", {scale: "x"}], ["x1", {scale: "x"}], ["x2", {scale: "x"}], @@ -39,16 +39,18 @@ export const knownChannels = new Map([ ]); export function registerChannel(name, {scale, definition} = {}) { - knownChannels.set(name, {scale, definition}); + channelRegistry.set(name, {scale, definition}); } export function definition(name, value, defaultValue) { - if (!knownChannels.has(name)) { + if (!channelRegistry.has(name)) { warn(`The ${name} channel is not registered and might be incompatible with some transforms.`); } - const {definition} = knownChannels.get(name) || {}; - return definition !== undefined ? definition(value, defaultValue) - : value === undefined ? [undefined, defaultValue] + const {definition} = channelRegistry.get(name) || {}; + return definition !== undefined + ? definition(value, defaultValue) + : value === undefined + ? [undefined, defaultValue] : [value]; } diff --git a/src/facet.js b/src/facet.js index 557a0ba553..182c069fe8 100644 --- a/src/facet.js +++ b/src/facet.js @@ -1,5 +1,5 @@ import {labelof, slice, valueof} from "./options.js"; -import {knownChannels} from "./channel.js"; +import {channelRegistry} from "./channel.js"; function facetReindex(facets, n) { if (facets.length === 1) return {facets}; @@ -54,7 +54,7 @@ export function maybeExpand(X, plan) { function maybeExpandChannels(options) { const channels = {}; let data, plan; - for (const [name, {definition = (value) => [value]}] of knownChannels) { + for (const [name, {definition = (value) => [value]}] of channelRegistry) { const value = definition(options[name])[0]; if (value != null) { channels[name] = { diff --git a/src/plot.js b/src/plot.js index 9256d13114..0dce574258 100644 --- a/src/plot.js +++ b/src/plot.js @@ -21,7 +21,7 @@ import {Scales, ScaleFunctions, autoScaleRange, exposeScales} from "./scales.js" import {position, registry as scaleRegistry} from "./scales/index.js"; import {applyInlineStyles, maybeClassName, maybeClip, styles} from "./style.js"; import {basic, initializer} from "./transforms/basic.js"; -import {knownChannels} from "./channel.js"; +import {channelRegistry} from "./channel.js"; import {maybeInterval} from "./transforms/interval.js"; import {consumeWarnings, warn} from "./warnings.js"; @@ -677,12 +677,12 @@ export class Mark { this.channels = Object.fromEntries( Object.entries(channels) .filter(([name, {value, optional}]) => { - if (!knownChannels.has(name)) warn(`The ${name} channel is not registered.`); + if (!channelRegistry.has(name)) warn(`The ${name} channel is not registered.`); if (value != null) return true; if (optional) return false; throw new Error(`missing channel value: ${name}`); }) - .map(([name, {scale = knownChannels.get(name)?.scale, ...rest}]) => [name, {scale, ...rest}]) + .map(([name, {scale = channelRegistry.get(name)?.scale, ...rest}]) => [name, {scale, ...rest}]) ); this.dx = +dx || 0; this.dy = +dy || 0; @@ -761,7 +761,7 @@ function inferChannelScale(channels) { for (const name in channels) { const channel = channels[name]; const {scale} = channel; - if (scale === true) channel.scale = knownChannels.get(name)?.scale ?? null; + if (scale === true) channel.scale = channelRegistry.get(name)?.scale ?? null; } } From b5cfb76e131461e3c8d8f28ad131b6732bbe0d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 10 Oct 2022 11:25:27 -0700 Subject: [PATCH 14/18] scaleRegistry --- src/axes.js | 4 ++-- src/channel.js | 4 ++-- src/plot.js | 2 +- src/scales.js | 14 +++++++------- src/scales/diverging.js | 4 ++-- src/scales/index.js | 2 +- src/scales/ordinal.js | 8 ++++---- src/scales/quantitative.js | 18 +++++++++--------- 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/axes.js b/src/axes.js index 7cac35bec9..0304a3363d 100644 --- a/src/axes.js +++ b/src/axes.js @@ -2,7 +2,7 @@ import {extent} from "d3"; import {AxisX, AxisY} from "./axis.js"; import {formatDefault} from "./format.js"; import {isOrdinalScale, isTemporalScale, scaleOrder} from "./scales.js"; -import {position, registry} from "./scales/index.js"; +import {position, scaleRegistry} from "./scales/index.js"; export function Axes( {x: xScale, y: yScale, fx: fxScale, fy: fyScale}, @@ -114,7 +114,7 @@ export function autoScaleLabels(channels, scales, {x, y, fx, fy}, dimensions, op y.labelOffset = y.axis === "left" ? marginLeft - facetMarginLeft : marginRight - facetMarginRight; } } - for (const [key, type] of registry) { + for (const [key, type] of scaleRegistry) { if (type !== position && scales[key]) { // not already handled above autoScaleLabel(key, scales[key], channels.get(key), options[key]); diff --git a/src/channel.js b/src/channel.js index db950e7b42..271cece7aa 100644 --- a/src/channel.js +++ b/src/channel.js @@ -1,6 +1,6 @@ import {ascending, descending, rollup, sort} from "d3"; import {first, isIterable, labelof, map, maybeValue, range, valueof} from "./options.js"; -import {registry} from "./scales/index.js"; +import {scaleRegistry} from "./scales/index.js"; import {maybeReduce} from "./transforms/group.js"; import {maybeColorChannel, maybeNumberChannel} from "./options.js"; import {maybeSymbolChannel} from "./symbols.js"; @@ -90,7 +90,7 @@ export function valueObject(channels, scales) { export function channelDomain(channels, facetChannels, data, options) { const {reverse: defaultReverse, reduce: defaultReduce = true, limit: defaultLimit} = options; for (const x in options) { - if (!registry.has(x)) continue; // ignore unknown scale keys (including generic options) + if (!scaleRegistry.has(x)) continue; // ignore unknown scale keys (including generic options) let {value: y, reverse = defaultReverse, reduce = defaultReduce, limit = defaultLimit} = maybeValue(options[x]); if (reverse === undefined) reverse = y === "width" || y === "height"; // default to descending for lengths if (reduce == null || reduce === false) continue; // disabled reducer diff --git a/src/plot.js b/src/plot.js index 0dce574258..91caa49461 100644 --- a/src/plot.js +++ b/src/plot.js @@ -18,7 +18,7 @@ import { yes } from "./options.js"; import {Scales, ScaleFunctions, autoScaleRange, exposeScales} from "./scales.js"; -import {position, registry as scaleRegistry} from "./scales/index.js"; +import {position, scaleRegistry} from "./scales/index.js"; import {applyInlineStyles, maybeClassName, maybeClip, styles} from "./style.js"; import {basic, initializer} from "./transforms/basic.js"; import {channelRegistry} from "./channel.js"; diff --git a/src/scales.js b/src/scales.js index fe153c92f7..c6c6bacbeb 100644 --- a/src/scales.js +++ b/src/scales.js @@ -13,7 +13,7 @@ import { order, slice } from "./options.js"; -import {registry, color, position, radius, opacity, symbol, length} from "./scales/index.js"; +import {scaleRegistry, color, position, radius, opacity, symbol, length} from "./scales/index.js"; import { ScaleLinear, ScaleSqrt, @@ -59,7 +59,7 @@ export function Scales( for (const [key, channels] of channelsByScale) { const scaleOptions = options[key]; const scale = Scale(key, channels, { - round: registry.get(key) === position ? round : undefined, // only for position + round: scaleRegistry.get(key) === position ? round : undefined, // only for position nice, clamp, zero, @@ -231,7 +231,7 @@ function Scale(key, channels = [], options = {}) { options = coerceType(channels, options, coerceNumbers); break; case "identity": - switch (registry.get(key)) { + switch (scaleRegistry.get(key)) { case position: options = coerceType(channels, options, coerceNumbers); break; @@ -288,7 +288,7 @@ function Scale(key, channels = [], options = {}) { case "band": return ScaleBand(key, channels, options); case "identity": - return registry.get(key) === position ? ScaleIdentity() : {type: "identity"}; + return scaleRegistry.get(key) === position ? ScaleIdentity() : {type: "identity"}; case undefined: return; default: @@ -319,7 +319,7 @@ function inferScaleType(key, channels, {type, domain, range, scheme, pivot}) { // If there’s no data (and no type) associated with this scale, don’t create a scale. if (domain === undefined && !channels.some(({value}) => value !== undefined)) return; - const kind = registry.get(key); + const kind = scaleRegistry.get(key); // For color scales, if no range or scheme is specified and all associated // defined values (from the domain if present, and otherwise from channels) @@ -511,7 +511,7 @@ export function coerceDate(x) { export function scale(options = {}) { let scale; for (const key in options) { - if (!registry.has(key)) continue; // ignore unknown properties + if (!scaleRegistry.has(key)) continue; // ignore unknown properties if (!isScaleOptions(options[key])) continue; // e.g., ignore {color: "red"} if (scale !== undefined) throw new Error("ambiguous scale definition; multiple scales found"); scale = exposeScale(normalizeScale(key, options[key])); @@ -522,7 +522,7 @@ export function scale(options = {}) { export function exposeScales(scaleDescriptors) { return (key) => { - if (!registry.has((key = `${key}`))) throw new Error(`unknown scale: ${key}`); + if (!scaleRegistry.has((key = `${key}`))) throw new Error(`unknown scale: ${key}`); return key in scaleDescriptors ? exposeScale(scaleDescriptors[key]) : undefined; }; } diff --git a/src/scales/diverging.js b/src/scales/diverging.js index 0e020937bb..1ac4616831 100644 --- a/src/scales/diverging.js +++ b/src/scales/diverging.js @@ -9,7 +9,7 @@ import { } from "d3"; import {positive, negative} from "../defined.js"; import {quantitativeScheme} from "./schemes.js"; -import {registry, color} from "./index.js"; +import {scaleRegistry, color} from "./index.js"; import {inferDomain, Interpolator, flip, interpolatePiecewise} from "./quantitative.js"; function ScaleD( @@ -27,7 +27,7 @@ function ScaleD( scheme, range, symmetric = true, - interpolate = registry.get(key) === color + interpolate = scaleRegistry.get(key) === color ? scheme == null && range !== undefined ? interpolateRgb : quantitativeScheme(scheme !== undefined ? scheme : "rdbu") diff --git a/src/scales/index.js b/src/scales/index.js index 8acd8ae051..b9d20ea853 100644 --- a/src/scales/index.js +++ b/src/scales/index.js @@ -25,7 +25,7 @@ export const symbol = Symbol("symbol"); // TODO Rather than hard-coding the list of known scale names, collect the names // and categories for each plot specification, so that custom marks can register // custom scales. -export const registry = new Map([ +export const scaleRegistry = new Map([ ["x", position], ["y", position], ["fx", position], diff --git a/src/scales/ordinal.js b/src/scales/ordinal.js index 4d3b6d834b..c054ed91c8 100644 --- a/src/scales/ordinal.js +++ b/src/scales/ordinal.js @@ -4,7 +4,7 @@ import {ascendingDefined} from "../defined.js"; import {isNoneish, map} from "../options.js"; import {maybeInterval} from "../transforms/interval.js"; import {maybeSymbol} from "../symbols.js"; -import {registry, color, position, symbol} from "./index.js"; +import {scaleRegistry, color, position, symbol} from "./index.js"; import {maybeBooleanRange, ordinalScheme, quantitativeScheme} from "./schemes.js"; // This denotes an implicitly ordinal color scale: the scale type was not set, @@ -31,10 +31,10 @@ export function ScaleOrdinal(key, channels, {type, interval, domain, range, sche interval = maybeInterval(interval); if (domain === undefined) domain = inferDomain(channels, interval, key); let hint; - if (registry.get(key) === symbol) { + if (scaleRegistry.get(key) === symbol) { hint = inferSymbolHint(channels); range = range === undefined ? inferSymbolRange(hint) : map(range, maybeSymbol); - } else if (registry.get(key) === color) { + } else if (scaleRegistry.get(key) === color) { if (range === undefined && (type === "ordinal" || type === ordinalImplicit)) { range = maybeBooleanRange(domain, scheme); if (range !== undefined) scheme = undefined; // Don’t re-apply scheme. @@ -99,7 +99,7 @@ function inferDomain(channels, interval, key) { const [min, max] = extent(values).map(interval.floor, interval); return interval.range(min, interval.offset(max)); } - if (values.size > 10e3 && registry.get(key) === position) + if (values.size > 10e3 && scaleRegistry.get(key) === position) throw new Error("implicit ordinal position domain has more than 10,000 values"); return sort(values, ascendingDefined); } diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index 20491cfd2a..949ceefe52 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -27,7 +27,7 @@ import {positive, negative, finite} from "../defined.js"; import {arrayify, constant, order, slice} from "../options.js"; import {ordinalRange, quantitativeScheme} from "./schemes.js"; import {maybeInterval} from "../transforms/interval.js"; -import {registry, radius, opacity, color, length} from "./index.js"; +import {scaleRegistry, radius, opacity, color, length} from "./index.js"; export const flip = (i) => (t) => i(1 - t); const unit = [0, 1]; @@ -63,14 +63,14 @@ export function ScaleQ( round, scheme, interval, - range = registry.get(key) === radius + range = scaleRegistry.get(key) === radius ? inferRadialRange(channels, domain) - : registry.get(key) === length + : scaleRegistry.get(key) === length ? inferLengthRange(channels, domain) - : registry.get(key) === opacity + : scaleRegistry.get(key) === opacity ? unit : undefined, - interpolate = registry.get(key) === color + interpolate = scaleRegistry.get(key) === color ? scheme == null && range !== undefined ? interpolateRgb : quantitativeScheme(scheme !== undefined ? scheme : type === "cyclical" ? "rainbow" : "turbo") @@ -166,7 +166,7 @@ export function ScaleQuantile( range = interpolate !== undefined ? quantize(interpolate, n) - : registry.get(key) === color + : scaleRegistry.get(key) === color ? ordinalRange(scheme, n) : undefined; return ScaleThreshold(key, channels, { @@ -198,7 +198,7 @@ export function ScaleQuantize( range = interpolate !== undefined ? quantize(interpolate, n) - : registry.get(key) === color + : scaleRegistry.get(key) === color ? ordinalRange(scheme, n) : undefined; } else { @@ -219,7 +219,7 @@ export function ScaleThreshold( interpolate, range = interpolate !== undefined ? quantize(interpolate, domain.length + 1) - : registry.get(key) === color + : scaleRegistry.get(key) === color ? ordinalRange(scheme, domain.length + 1) : undefined, reverse @@ -256,7 +256,7 @@ export function inferDomain(channels, f = finite) { } function inferAutoDomain(key, channels) { - const type = registry.get(key); + const type = scaleRegistry.get(key); return (type === radius || type === opacity || type === length ? inferZeroDomain : inferDomain)(channels); } From a4263ecd6fc3ebdeef7f70efd6c65177b20d8024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 10 Oct 2022 11:40:30 -0700 Subject: [PATCH 15/18] oops this test depended on the time zone --- test/plots/music-revenue-bars.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/plots/music-revenue-bars.js b/test/plots/music-revenue-bars.js index e29eda1572..4e90a622be 100644 --- a/test/plots/music-revenue-bars.js +++ b/test/plots/music-revenue-bars.js @@ -3,7 +3,7 @@ import * as d3 from "d3"; export default async function () { const data = await d3.csv("data/riaa-us-revenue.csv", d3.autoType); - const stack = {x: (d) => d["year"].getFullYear(), y: "revenue", z: "format", order: "value", reverse: true}; + const stack = {x: (d) => d["year"].getUTCFullYear(), y: "revenue", z: "format", order: "value", reverse: true}; return Plot.plot({ marginRight: 90, marginBottom: 35, From be6e887c9b83dab58165148879e156c9eb5853e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 10 Oct 2022 11:41:01 -0700 Subject: [PATCH 16/18] stricter --- src/channel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/channel.js b/src/channel.js index 271cece7aa..d733cc8266 100644 --- a/src/channel.js +++ b/src/channel.js @@ -46,7 +46,7 @@ export function definition(name, value, defaultValue) { if (!channelRegistry.has(name)) { warn(`The ${name} channel is not registered and might be incompatible with some transforms.`); } - const {definition} = channelRegistry.get(name) || {}; + const {definition} = channelRegistry.get(name) ?? {}; return definition !== undefined ? definition(value, defaultValue) : value === undefined From 1a5d83172f0ec2ba43f1078cc188984ec2fa9ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 10 Oct 2022 11:51:50 -0700 Subject: [PATCH 17/18] don't export registerChannel yet --- src/channel.js | 4 -- src/index.js | 1 - test/output/customMark.svg | 88 -------------------------------------- test/plots/custom-mark.js | 47 -------------------- test/plots/index.js | 1 - 5 files changed, 141 deletions(-) delete mode 100644 test/output/customMark.svg delete mode 100644 test/plots/custom-mark.js diff --git a/src/channel.js b/src/channel.js index d733cc8266..c3438c765d 100644 --- a/src/channel.js +++ b/src/channel.js @@ -38,10 +38,6 @@ export const channelRegistry = new Map([ ["weight", {definition: maybeNumberChannel}] // density ]); -export function registerChannel(name, {scale, definition} = {}) { - channelRegistry.set(name, {scale, definition}); -} - export function definition(name, value, defaultValue) { if (!channelRegistry.has(name)) { warn(`The ${name} channel is not registered and might be incompatible with some transforms.`); diff --git a/src/index.js b/src/index.js index f95ea98394..a6e5dc7a48 100644 --- a/src/index.js +++ b/src/index.js @@ -34,4 +34,3 @@ export {treeNode, treeLink} from "./transforms/tree.js"; export {formatIsoDate, formatWeekday, formatMonth} from "./format.js"; export {scale} from "./scales.js"; export {legend} from "./legends.js"; -export {registerChannel} from "./channel.js"; diff --git a/test/output/customMark.svg b/test/output/customMark.svg deleted file mode 100644 index 00a98f435f..0000000000 --- a/test/output/customMark.svg +++ /dev/null @@ -1,88 +0,0 @@ - - - - - 0 - - - 5 - - - 10 - - - 15 - - - 20 - - - 25 - - - 30 - - - 35 - - - 40 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/test/plots/custom-mark.js b/test/plots/custom-mark.js deleted file mode 100644 index 1384ce27b5..0000000000 --- a/test/plots/custom-mark.js +++ /dev/null @@ -1,47 +0,0 @@ -import * as d3 from "d3"; -import * as Plot from "@observablehq/plot"; - -Plot.registerChannel("alpha", {scale: "x"}); -Plot.registerChannel("beta"); - -class CustomMark extends Plot.Mark { - constructor(data, options = {}) { - const {alpha, beta} = options; - super( - data, - { - alpha: {value: alpha}, - beta: {value: beta, optional: true} - }, - options, - {} - ); - } - render(index, scales, channels, dimensions, context) { - const {alpha: A, beta: B} = channels; - return d3 - .create("svg:g", context) - .call((g) => - g - .selectAll() - .data(index) - .enter() - .append("circle") - .attr("transform", (i) => `translate(${A[i]}, ${6 + A[i] / 20})`) - .attr("r", B ? (i) => Math.sqrt(B[i]) : 2) - ) - .node(); - } -} - -function customMark(data, options) { - return new CustomMark(data, options); -} - -export default async function () { - return customMark(d3.range(41), { - alpha: (d) => d, - beta: (d) => d, - stroke: "red" - }).plot(); -} diff --git a/test/plots/index.js b/test/plots/index.js index 3999d39e2c..dbd041ec4c 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -42,7 +42,6 @@ export {default as crimeanWarArrow} from "./crimean-war-arrow.js"; export {default as crimeanWarLine} from "./crimean-war-line.js"; export {default as crimeanWarOverlapped} from "./crimean-war-overlapped.js"; export {default as crimeanWarStacked} from "./crimean-war-stacked.js"; -export {default as customMark} from "./custom-mark.js"; export {default as d3Survey2015Comfort} from "./d3-survey-2015-comfort.js"; export {default as d3Survey2015Why} from "./d3-survey-2015-why.js"; export {default as darkerDodge} from "./darker-dodge.js"; From a7f7ba9c262d43720fbec82b00a17e9797fb5ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 10 Oct 2022 11:57:19 -0700 Subject: [PATCH 18/18] maybeChannel --- src/channel.js | 2 +- src/legends/swatches.js | 12 ++++++------ src/marks/cell.js | 6 +++--- src/marks/dot.js | 8 ++++---- src/marks/image.js | 8 ++++---- src/marks/text.js | 6 +++--- src/marks/vector.js | 6 +++--- src/style.js | 14 +++++++------- src/transforms/bin.js | 6 +++--- src/transforms/group.js | 6 +++--- 10 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/channel.js b/src/channel.js index c3438c765d..9f501579f9 100644 --- a/src/channel.js +++ b/src/channel.js @@ -38,7 +38,7 @@ export const channelRegistry = new Map([ ["weight", {definition: maybeNumberChannel}] // density ]); -export function definition(name, value, defaultValue) { +export function maybeChannel(name, value, defaultValue) { if (!channelRegistry.has(name)) { warn(`The ${name} channel is not registered and might be incompatible with some transforms.`); } diff --git a/src/legends/swatches.js b/src/legends/swatches.js index 758fdc11fe..d5efa22977 100644 --- a/src/legends/swatches.js +++ b/src/legends/swatches.js @@ -3,7 +3,7 @@ import {inferFontVariant} from "../axes.js"; import {maybeAutoTickFormat} from "../axis.js"; import {Context, create} from "../context.js"; import {isNoneish} from "../options.js"; -import {definition} from "../channel.js"; +import {maybeChannel} from "../channel.js"; import {isOrdinalScale, isThresholdScale} from "../scales.js"; import {applyInlineStyles, impliedString, maybeClassName} from "../style.js"; @@ -43,14 +43,14 @@ export function legendSymbols( } = {}, scale ) { - const [vf, cf] = definition("fill", fill); - const [vs, cs] = definition("stroke", stroke); + const [vf, cf] = maybeChannel("fill", fill); + const [vs, cs] = maybeChannel("stroke", stroke); const sf = maybeScale(scale, vf); const ss = maybeScale(scale, vs); const size = r * r * Math.PI; - fillOpacity = definition("fillOpacity", fillOpacity)[1]; - strokeOpacity = definition("strokeOpacity", strokeOpacity)[1]; - strokeWidth = definition("strokeWidth", strokeWidth)[1]; + fillOpacity = maybeChannel("fillOpacity", fillOpacity)[1]; + strokeOpacity = maybeChannel("strokeOpacity", strokeOpacity)[1]; + strokeWidth = maybeChannel("strokeWidth", strokeWidth)[1]; return legendItems( symbol, options, diff --git a/src/marks/cell.js b/src/marks/cell.js index d2b4ba7991..9679d5de3c 100644 --- a/src/marks/cell.js +++ b/src/marks/cell.js @@ -1,5 +1,5 @@ import {identity, indexOf, maybeTuple} from "../options.js"; -import {definition} from "../channel.js"; +import {maybeChannel} from "../channel.js"; import {applyTransform} from "../style.js"; import {AbstractBar} from "./bar.js"; @@ -55,7 +55,7 @@ export function cell(data, options = {}) { */ export function cellX(data, options = {}) { let {x = indexOf, fill, stroke, ...remainingOptions} = options; - if (fill === undefined && definition("stroke", stroke)[0] === undefined) fill = identity; + if (fill === undefined && maybeChannel("stroke", stroke)[0] === undefined) fill = identity; return new Cell(data, {...remainingOptions, x, fill, stroke}); } @@ -73,6 +73,6 @@ export function cellX(data, options = {}) { */ export function cellY(data, options = {}) { let {y = indexOf, fill, stroke, ...remainingOptions} = options; - if (fill === undefined && definition("stroke", stroke)[0] === undefined) fill = identity; + if (fill === undefined && maybeChannel("stroke", stroke)[0] === undefined) fill = identity; return new Cell(data, {...remainingOptions, y, fill, stroke}); } diff --git a/src/marks/dot.js b/src/marks/dot.js index 46c810468a..8325c012f2 100644 --- a/src/marks/dot.js +++ b/src/marks/dot.js @@ -2,7 +2,7 @@ import {path, symbolCircle} from "d3"; import {create} from "../context.js"; import {positive} from "../defined.js"; import {identity, maybeFrameAnchor, maybeTuple} from "../options.js"; -import {definition} from "../channel.js"; +import {maybeChannel} from "../channel.js"; import {Mark} from "../plot.js"; import { applyChannelStyles, @@ -24,9 +24,9 @@ const defaults = { export class Dot extends Mark { constructor(data, options = {}) { const {x, y, r, rotate, symbol = symbolCircle, frameAnchor} = options; - const [vrotate, crotate] = definition("rotate", rotate, 0); - const [vsymbol, csymbol] = definition("symbol", symbol); - const [vr, cr] = definition("r", r, vsymbol == null ? 3 : 4.5); + const [vrotate, crotate] = maybeChannel("rotate", rotate, 0); + const [vsymbol, csymbol] = maybeChannel("symbol", symbol); + const [vr, cr] = maybeChannel("r", r, vsymbol == null ? 3 : 4.5); super( data, { diff --git a/src/marks/image.js b/src/marks/image.js index c98542e585..9c68fed98b 100644 --- a/src/marks/image.js +++ b/src/marks/image.js @@ -11,7 +11,7 @@ import { impliedString, applyFrameAnchor } from "../style.js"; -import {definition} from "../channel.js"; +import {maybeChannel} from "../channel.js"; const defaults = { ariaLabel: "image", @@ -24,9 +24,9 @@ export class Image extends Mark { let {x, y, width, height, src, preserveAspectRatio, crossOrigin, frameAnchor} = options; if (width === undefined && height !== undefined) width = height; else if (height === undefined && width !== undefined) height = width; - const [vs, cs] = definition("src", src); - const [vw, cw] = definition("width", width, 16); - const [vh, ch] = definition("height", height, 16); + const [vs, cs] = maybeChannel("src", src); + const [vw, cw] = maybeChannel("width", width, 16); + const [vh, ch] = maybeChannel("height", height, 16); super( data, { diff --git a/src/marks/text.js b/src/marks/text.js index 2b480fd2d4..50682b38ca 100644 --- a/src/marks/text.js +++ b/src/marks/text.js @@ -26,7 +26,7 @@ import { applyFrameAnchor } from "../style.js"; import {maybeIntervalMidX, maybeIntervalMidY} from "../transforms/interval.js"; -import {definition} from "../channel.js"; +import {maybeChannel} from "../channel.js"; const defaults = { ariaLabel: "text", @@ -54,8 +54,8 @@ export class Text extends Mark { fontWeight, rotate } = options; - const [vrotate, crotate] = definition("rotate", rotate, 0); - const [vfontSize, cfontSize] = definition("fontSize", fontSize); + const [vrotate, crotate] = maybeChannel("rotate", rotate, 0); + const [vfontSize, cfontSize] = maybeChannel("fontSize", fontSize); super( data, { diff --git a/src/marks/vector.js b/src/marks/vector.js index 8c252647c1..8025fdf148 100644 --- a/src/marks/vector.js +++ b/src/marks/vector.js @@ -1,7 +1,7 @@ import {create} from "../context.js"; import {radians} from "../math.js"; import {maybeFrameAnchor, maybeTuple, keyword, identity} from "../options.js"; -import {definition} from "../channel.js"; +import {maybeChannel} from "../channel.js"; import {Mark} from "../plot.js"; import { applyChannelStyles, @@ -22,8 +22,8 @@ const defaults = { export class Vector extends Mark { constructor(data, options = {}) { const {x, y, length, rotate, anchor = "middle", frameAnchor} = options; - const [vl, cl] = definition("length", length, 12); - const [vr, cr] = definition("rotate", rotate, 0); + const [vl, cl] = maybeChannel("length", length, 12); + const [vr, cr] = maybeChannel("rotate", rotate, 0); super( data, { diff --git a/src/style.js b/src/style.js index 42e114944d..f405ba61c9 100644 --- a/src/style.js +++ b/src/style.js @@ -3,7 +3,7 @@ import {defined, nonempty} from "./defined.js"; import {formatDefault} from "./format.js"; import {string, number, isNoneish, isNone, isRound, keyof} from "./options.js"; import {warn} from "./warnings.js"; -import {definition} from "./channel.js"; +import {maybeChannel} from "./channel.js"; export const offset = typeof window !== "undefined" && window.devicePixelRatio > 1 ? 0 : 0.5; @@ -71,11 +71,11 @@ export function styles( if (isNoneish(defaultStroke) && !isNoneish(stroke)) defaultFill = "none"; } - const [vfill, cfill] = definition("fill", fill, defaultFill); - const [vfillOpacity, cfillOpacity] = definition("fillOpacity", fillOpacity, defaultFillOpacity); - const [vstroke, cstroke] = definition("stroke", stroke, defaultStroke); - const [vstrokeOpacity, cstrokeOpacity] = definition("strokeOpacity", strokeOpacity, defaultStrokeOpacity); - const [vopacity, copacity] = definition("opacity", opacity); + const [vfill, cfill] = maybeChannel("fill", fill, defaultFill); + const [vfillOpacity, cfillOpacity] = maybeChannel("fillOpacity", fillOpacity, defaultFillOpacity); + const [vstroke, cstroke] = maybeChannel("stroke", stroke, defaultStroke); + const [vstrokeOpacity, cstrokeOpacity] = maybeChannel("strokeOpacity", strokeOpacity, defaultStrokeOpacity); + const [vopacity, copacity] = maybeChannel("opacity", opacity); // For styles that have no effect if there is no stroke, only apply the // defaults if the stroke is not the constant none. (If stroke is a channel, @@ -95,7 +95,7 @@ export function styles( if (!isNone(cfill) && paintOrder === undefined) paintOrder = defaultPaintOrder; } - const [vstrokeWidth, cstrokeWidth] = definition("strokeWidth", strokeWidth); + const [vstrokeWidth, cstrokeWidth] = maybeChannel("strokeWidth", strokeWidth); // Some marks don’t support fill (e.g., tick and rule). if (defaultFill !== null) { diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 8a877f8bfe..08c5b4827b 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -7,7 +7,7 @@ import { thresholdSturges, utcTickInterval } from "d3"; -import {definition} from "../channel.js"; +import {maybeChannel} from "../channel.js"; import { valueof, identity, @@ -158,8 +158,8 @@ function binn( ...options } = inputs; const [GZ, setGZ] = maybeColumn(z); - const [vfill] = definition("fill", fill); - const [vstroke] = definition("stroke", stroke); + const [vfill] = maybeChannel("fill", fill); + const [vstroke] = maybeChannel("stroke", stroke); const [GF, setGF] = maybeColumn(vfill); const [GS, setGS] = maybeColumn(vstroke); diff --git a/src/transforms/group.js b/src/transforms/group.js index aa66e2053d..dbd8b9199a 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -29,7 +29,7 @@ import { second, percentile } from "../options.js"; -import {definition} from "../channel.js"; +import {maybeChannel} from "../channel.js"; import {basic} from "./basic.js"; /** @@ -135,8 +135,8 @@ function groupn( ...options } = inputs; const [GZ, setGZ] = maybeColumn(z); - const [vfill] = definition("fill", fill); - const [vstroke] = definition("stroke", stroke); + const [vfill] = maybeChannel("fill", fill); + const [vstroke] = maybeChannel("stroke", stroke); const [GF, setGF] = maybeColumn(vfill); const [GS, setGS] = maybeColumn(vstroke);