From d95c618c910515f463958405e55e404776d67938 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 10 Mar 2022 15:31:36 -0800 Subject: [PATCH 01/36] mark initializers scale hex radius so that when hexagons touch, circles also touch without overlapping (#803) (supersedes #795) simpler hexagon hexgrid fix for unscaled channels reorder hexbin fix #806; handle missing hint infer channel scales pass data to initializer offset hexagonal grid slightly simpler without destructuring reinitialize (#823) * document layouts (as "scale-aware transforms") * document binWidth * document the initialize option after 42ac4f08093c5819e9fa2dcecf3e09e12785990e * sort hex bins by radius (descending) group by z inline hexbin binWidth is the distance between two centers (rebased on mbostock/reinitialize) * dodge rebased on mbostock/reinitialize * compose intializers * use composeInitialize to make dodge composable * add new channels as you compose initializers * darker transform, to demonstrate composition with dodgeY (added as an example, but we could promote it to a transform) * a more generic "remap" * jiggle layout (using the same remap intializer as in the darkerDodge plot) composeInitialize --- README.md | 75 ++- package.json | 2 + rollup.config.js | 2 + src/channel.js | 43 +- src/index.js | 5 +- src/marks/dot.js | 11 +- src/marks/hexgrid.js | 46 ++ src/options.js | 45 +- src/plot.js | 182 ++++-- src/scales.js | 87 +-- src/scales/ordinal.js | 29 +- src/scales/quantitative.js | 6 +- src/symbols.js | 61 ++ src/transforms/basic.js | 2 + src/transforms/dodge.js | 101 +++ src/transforms/hexbin.js | 111 ++++ src/transforms/initialize.js | 19 + test/output/carsJiggle.html | 499 +++++++++++++++ test/output/darkerDodge.svg | 200 ++++++ test/output/hexbin.svg | 284 +++++++++ test/output/hexbinOranges.svg | 151 +++++ test/output/hexbinR.html | 521 ++++++++++++++++ test/output/hexbinSymbol.html | 236 +++++++ test/output/hexbinText.svg | 195 ++++++ test/output/hexbinZ.svg | 310 +++++++++ test/output/penguinDodge.svg | 383 ++++++++++++ test/output/penguinDodgeHexbin.svg | 760 +++++++++++++++++++++++ test/output/penguinFacetDodge.svg | 411 ++++++++++++ test/output/penguinFacetDodgeIsland.html | 446 +++++++++++++ test/output/penguinFacetDodgeSymbol.html | 443 +++++++++++++ test/plots/cars-jiggle.js | 48 ++ test/plots/darker-dodge.js | 47 ++ test/plots/hexbin-oranges.js | 19 + test/plots/hexbin-r.js | 20 + test/plots/hexbin-symbol.js | 18 + test/plots/hexbin-text.js | 21 + test/plots/hexbin-z.js | 22 + test/plots/hexbin.js | 13 + test/plots/index.js | 13 + test/plots/penguin-dodge-hexbin.js | 26 + test/plots/penguin-dodge.js | 12 + test/plots/penguin-facet-dodge-island.js | 22 + test/plots/penguin-facet-dodge-symbol.js | 16 + test/plots/penguin-facet-dodge.js | 21 + test/transforms/normalize-test.js | 4 +- test/transforms/reduce-test.js | 4 +- yarn.lock | 63 +- 47 files changed, 5870 insertions(+), 185 deletions(-) create mode 100644 src/marks/hexgrid.js create mode 100644 src/symbols.js create mode 100644 src/transforms/dodge.js create mode 100644 src/transforms/hexbin.js create mode 100644 src/transforms/initialize.js create mode 100644 test/output/carsJiggle.html create mode 100644 test/output/darkerDodge.svg create mode 100644 test/output/hexbin.svg create mode 100644 test/output/hexbinOranges.svg create mode 100644 test/output/hexbinR.html create mode 100644 test/output/hexbinSymbol.html create mode 100644 test/output/hexbinText.svg create mode 100644 test/output/hexbinZ.svg create mode 100644 test/output/penguinDodge.svg create mode 100644 test/output/penguinDodgeHexbin.svg create mode 100644 test/output/penguinFacetDodge.svg create mode 100644 test/output/penguinFacetDodgeIsland.html create mode 100644 test/output/penguinFacetDodgeSymbol.html create mode 100644 test/plots/cars-jiggle.js create mode 100644 test/plots/darker-dodge.js create mode 100644 test/plots/hexbin-oranges.js create mode 100644 test/plots/hexbin-r.js create mode 100644 test/plots/hexbin-symbol.js create mode 100644 test/plots/hexbin-text.js create mode 100644 test/plots/hexbin-z.js create mode 100644 test/plots/hexbin.js create mode 100644 test/plots/penguin-dodge-hexbin.js create mode 100644 test/plots/penguin-dodge.js create mode 100644 test/plots/penguin-facet-dodge-island.js create mode 100644 test/plots/penguin-facet-dodge-symbol.js create mode 100644 test/plots/penguin-facet-dodge.js diff --git a/README.md b/README.md index 2deed0c1f7..c496cbc527 100644 --- a/README.md +++ b/README.md @@ -1037,6 +1037,14 @@ Plot.dotY(cars.map(d => d["economy (mpg)"])) Equivalent to [Plot.dot](#plotdotdata-options) except that if the **y** option is not specified, it defaults to the identity function and assumes that *data* = [*y₀*, *y₁*, *y₂*, …]. +### Hexgrid + +The hexgrid mark can be used to support marks using the [hexbin](#hexbin) layout. + +#### Plot.hexgrid([*options*]) + +The *binWidth* option specifies the distance between the centers of neighboring hexagons, in pixels (defaults to 20). The *clip* option defaults to true, clipping the mark to the frame’s dimensions. + ### Image [a scatterplot of Presidential portraits](https://observablehq.com/@observablehq/plot-image) @@ -1524,10 +1532,10 @@ The following aggregation methods are supported: * *pXX* - the percentile value, where XX is a number in [00,99] * *deviation* - the standard deviation * *variance* - the variance per [Welford’s algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) -* *x* - the middle the bin’s *x*-extent (when binning on *x*) +* *x* - the middle of the bin’s *x*-extent (when binning on *x*) * *x1* - the lower bound of the bin’s *x*-extent (when binning on *x*) * *x2* - the upper bound of the bin’s *x*-extent (when binning on *x*) -* *y* - the middle the bin’s *y*-extent (when binning on *y*) +* *y* - the middle of the bin’s *y*-extent (when binning on *y*) * *y1* - the lower bound of the bin’s *y*-extent (when binning on *y*) * *y2* - the upper bound of the bin’s *y*-extent (when binning on *y*) * a function to be passed the array of values for each bin and the extent of the bin @@ -2145,6 +2153,69 @@ This helper for constructing derived columns returns a [*column*, *setColumn*] a Plot.column is typically used by options transforms to define new channels; the associated columns are populated (derived) when the **transform** option function is invoked. +## Scale-aware transforms + +Some transforms need to operate in representation space (such as pixels and colors, *i.e.* after scales have been applied) rather than data space. Such a transform might, for example, modify the marks’ positions in screen space to avoid occlusion. These scale-aware transforms are applied *after* the initial setting of the scales, and can modify the channels or derive new channels—which can in turn be passed to scales. + +### Dodge + +The dodge transform can be applied to any mark that consumes *x* or *y*, such as the Dot, Image, Text and Vector marks. +#### Plot.dodgeY([*layoutOptions*, ]*options*) + +```js +Plot.dodgeY({x: "date"}) +``` + +If the marks are arranged along the *x* axis, the dodgeY transform piles them vertically, keeping their *x* position unchanged, and creating a *y* position that avoids overlapping. + +#### Plot.dodgeX([*layoutOptions*, ]*options*) + +```js +Plot.dodgeX({y: "value"}) +``` + +Equivalent to Plot.dodgeY, but the piling is horizontal, keeping the marks’ *y* position unchanged, and creating an *x* position that avoids overlapping. +The dodge transforms accept the following options: +* **padding** — a number of pixels added to the radius of the mark to estimate its size +* **anchor** - the frame anchor: one of *middle*, *right*, and *left* (default) for dodgeX, and one of *middle*, *top*, and *bottom* (default) for dodgeY. With the *middle* anchor the piles will grow from the center in both directions; with the other anchors, the piles will grow from the specified anchor towards the opposite direction. + +### Hexbin + +The hexbin transform can be applied to any mark that consumes *x* and *y*, such as the Dot, Image, Text and Vector marks. It aggregates the values into hexagonal bins of the given *radius* (in pixel space), and computes new values *x* and *y* as the centers of each bin. It can also return new channels by applying a reducer to each bin, such as the number of elements in the bin. + +#### Plot.hexbin(*outputs*, *options*) + +[Source](./src/transforms/hexbin.js) · [Examples](https://observablehq.com/@observablehq/plot-hexbin) · Aggregates the given inputs into hexagonal bins, and creates output channels with the reduced data. The options must specify the *x* and *y* channels, and can optionally indicate the *binWidth* in pixels (defaults to 20), defined as the distance between the centers of two neighboring hexagons. If any of **z**, **fill**, or **stroke** is a channel, the first of these channels will be used to subdivide bins. The *outputs* options are similar to Plot.bin’s outputs; each output channel receives as input, for each hexagon, the subset of the data which has been matched to its center. The outputs object specifies the aggregation method for each output channel. + +The following aggregation methods are supported: + +* *first* - the first value, in input order +* *last* - the last value, in input order +* *count* - the number of elements (frequency) +* *distinct* - the number of distinct values +* *sum* - the sum of values +* *proportion* - the sum proportional to the overall total (weighted frequency) +* *proportion-facet* - the sum proportional to the facet total +* *min* - the minimum value +* *min-index* - the zero-based index of the minimum value +* *max* - the maximum value +* *max-index* - the zero-based index of the maximum value +* *mean* - the mean value (average) +* *median* - the median value +* *deviation* - the standard deviation +* *variance* - the variance per [Welford’s algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) +* *mode* - the value with the most occurrences +* a function to be passed the array of values for each bin and the extent of the bin +* an object with a *reduce* method + +When the hexbin transform has an *r* output, the bins are returned in decreasing size order. + +See also the [hexgrid](#hexgrid) mark. + +### Custom scale-aware transforms + +When its *options* have an *initialize* property, the initialize function is called after the scales have been computed. It receives as inputs the (possibly transformed) data array, the index of elements of this array that belong to each facet, the input channels (as a key: array object), the scales, and the dimensions, with the mark as this. It must return the data, index, and the channels that need to be scaled in a second pass. + ## Curves A curve defines how to turn a discrete representation of a line as a sequence of points [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] into a continuous path; *i.e.*, how to interpolate between points. Curves are used by the [line](#line), [area](#area), and [link](#link) mark, and are implemented by [d3-shape](https://github.com/d3/d3-shape/blob/master/README.md#curves). diff --git a/package.json b/package.json index afd54781ee..baeaeedf07 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ }, "sideEffects": false, "devDependencies": { + "@rollup/plugin-commonjs": "^21.0.1", "@rollup/plugin-json": "4", "@rollup/plugin-node-resolve": "13", "canvas": "2", @@ -50,6 +51,7 @@ }, "dependencies": { "d3": "^7.3.0", + "interval-tree-1d": "1", "isoformat": "0.2" }, "engines": { diff --git a/rollup.config.js b/rollup.config.js index f2cded1105..ae8f8778b5 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,5 +1,6 @@ import fs from "fs"; import {terser} from "rollup-plugin-terser"; +import commonjs from "@rollup/plugin-commonjs"; import json from "@rollup/plugin-json"; import node from "@rollup/plugin-node-resolve"; import * as meta from "./package.json"; @@ -25,6 +26,7 @@ const config = { banner: `// ${meta.name} v${meta.version} Copyright ${copyrights.join(", ")}` }, plugins: [ + commonjs(), json(), node() ] diff --git a/src/channel.js b/src/channel.js index d44638ddd8..f11684847b 100644 --- a/src/channel.js +++ b/src/channel.js @@ -15,6 +15,28 @@ export function Channel(data, {scale, type, value, filter, hint}) { }; } +export function channelObject(channelDescriptors, data) { + const channels = {}; + for (const channel of channelDescriptors) { + channels[channel.name] = Channel(data, channel); + } + return channels; +} + +// TODO Use Float64Array for scales with numeric ranges, e.g. position? +export function valueObject(channels, scales) { + const values = {}; + for (const channelName in channels) { + const {scale: scaleName, value} = channels[channelName]; + const scale = scales[scaleName]; + values[channelName] = scale === undefined ? value : Array.from(value, scale); + } + return values; +} + +// Note: mutates channel.domain! This is set to a function so that it is lazily +// computed; i.e., if the scale’s domain is set explicitly, that takes priority +// over the sort option, and we don’t need to do additional work. export function channelSort(channels, facetChannels, data, options) { const {reverse: defaultReverse, reduce: defaultReduce = true, limit: defaultLimit} = options; for (const x in options) { @@ -22,12 +44,12 @@ export function channelSort(channels, facetChannels, data, 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 - const X = channels.find(([, {scale}]) => scale === x) || facetChannels && facetChannels.find(([, {scale}]) => scale === x); + const X = findScaleChannel(channels, x) || facetChannels && findScaleChannel(facetChannels, x); if (!X) throw new Error(`missing channel for scale: ${x}`); - const XV = X[1].value; + const XV = X.value; const [lo = 0, hi = Infinity] = limit && typeof limit[Symbol.iterator] === "function" ? limit : limit < 0 ? [limit] : [0, limit]; if (y == null) { - X[1].domain = () => { + X.domain = () => { let domain = XV; if (reverse) domain = domain.slice().reverse(); if (lo !== 0 || hi !== Infinity) domain = domain.slice(lo, hi); @@ -39,7 +61,7 @@ export function channelSort(channels, facetChannels, data, options) { : y === "width" ? difference(channels, "x1", "x2") : values(channels, y, y === "y" ? "y2" : y === "x" ? "x2" : undefined); const reducer = maybeReduce(reduce === true ? "max" : reduce, YV); - X[1].domain = () => { + X.domain = () => { let domain = rollup(range(XV), I => reducer.reduce(I, YV), i => XV[i]); domain = sort(domain, reverse ? descendingGroup : ascendingGroup); if (lo !== 0 || hi !== Infinity) domain = domain.slice(lo, hi); @@ -49,6 +71,13 @@ export function channelSort(channels, facetChannels, data, options) { } } +function findScaleChannel(channels, scale) { + for (const name in channels) { + const channel = channels[name]; + if (channel.scale === scale) return channel; + } +} + function difference(channels, k1, k2) { const X1 = values(channels, k1); const X2 = values(channels, k2); @@ -56,9 +85,9 @@ function difference(channels, k1, k2) { } function values(channels, name, alias) { - let channel = channels.find(([n]) => n === name); - if (!channel && alias !== undefined) channel = channels.find(([n]) => n === alias); - if (channel) return channel[1].value; + let channel = channels[name]; + if (!channel && alias !== undefined) channel = channels[alias]; + if (channel) return channel.value; throw new Error(`missing channel: ${name}`); } diff --git a/src/index.js b/src/index.js index 671cc22af8..0c6e976309 100644 --- a/src/index.js +++ b/src/index.js @@ -4,8 +4,9 @@ export {Arrow, arrow} from "./marks/arrow.js"; export {BarX, BarY, barX, barY} from "./marks/bar.js"; export {boxX, boxY} from "./marks/box.js"; export {Cell, cell, cellX, cellY} from "./marks/cell.js"; -export {Dot, dot, dotX, dotY} from "./marks/dot.js"; +export {Dot, dot, dotX, dotY, circle, hexagon} from "./marks/dot.js"; export {Frame, frame} from "./marks/frame.js"; +export {Hexgrid, hexgrid} from "./marks/hexgrid.js"; export {Image, image} from "./marks/image.js"; export {Line, line, lineX, lineY} from "./marks/line.js"; export {Link, link} from "./marks/link.js"; @@ -18,7 +19,9 @@ export {Vector, vector, vectorX, vectorY} from "./marks/vector.js"; export {valueof, column} from "./options.js"; export {filter, reverse, sort, shuffle, basic as transform} from "./transforms/basic.js"; export {bin, binX, binY} from "./transforms/bin.js"; +export {dodgeX, dodgeY} from "./transforms/dodge.js"; export {group, groupX, groupY, groupZ} from "./transforms/group.js"; +export {hexbin} from "./transforms/hexbin.js"; export {normalize, normalizeX, normalizeY} from "./transforms/normalize.js"; export {map, mapX, mapY} from "./transforms/map.js"; export {window, windowX, windowY} from "./transforms/window.js"; diff --git a/src/marks/dot.js b/src/marks/dot.js index b1b06e7321..aee2ec734f 100644 --- a/src/marks/dot.js +++ b/src/marks/dot.js @@ -1,8 +1,9 @@ import {create, path, symbolCircle} from "d3"; import {positive} from "../defined.js"; -import {identity, maybeFrameAnchor, maybeNumberChannel, maybeSymbolChannel, maybeTuple} from "../options.js"; +import {identity, maybeFrameAnchor, maybeNumberChannel, maybeTuple} from "../options.js"; import {Mark} from "../plot.js"; import {applyChannelStyles, applyDirectStyles, applyFrameAnchor, applyIndirectStyles, applyTransform, offset} from "../style.js"; +import {maybeSymbolChannel} from "../symbols.js"; const defaults = { ariaLabel: "dot", @@ -100,3 +101,11 @@ export function dotX(data, {x = identity, ...options} = {}) { export function dotY(data, {y = identity, ...options} = {}) { return new Dot(data, {...options, y}); } + +export function circle(data, options) { + return dot(data, {...options, symbol: "circle"}); +} + +export function hexagon(data, options) { + return dot(data, {...options, symbol: "hexagon"}); +} diff --git a/src/marks/hexgrid.js b/src/marks/hexgrid.js new file mode 100644 index 0000000000..4fbb9f2244 --- /dev/null +++ b/src/marks/hexgrid.js @@ -0,0 +1,46 @@ +import {create} from "d3"; +import {Mark} from "../plot.js"; +import {number} from "../options.js"; +import {applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js"; +import {sqrt4_3} from "../symbols.js"; +import {ox, oy} from "../transforms/hexbin.js"; + +const defaults = { + ariaLabel: "hexgrid", + fill: "none", + stroke: "currentColor", + strokeOpacity: 0.1 +}; + +export function hexgrid(options) { + return new Hexgrid(options); +} + +export class Hexgrid extends Mark { + constructor({binWidth = 20, clip = true, ...options} = {}) { + super(undefined, undefined, {clip, ...options}, defaults); + this.binWidth = number(binWidth); + } + render(index, scales, channels, dimensions) { + const {dx, dy, binWidth} = this; + const {marginTop, marginRight, marginBottom, marginLeft, width, height} = dimensions; + const x0 = marginLeft - ox, x1 = width - marginRight - ox, y0 = marginTop - oy, y1 = height - marginBottom - oy; + const rx = binWidth / 2, ry = rx * sqrt4_3, hy = ry / 2, wx = rx * 2, wy = ry * 1.5; + const path = `m0,${-ry}l${rx},${hy}v${ry}l${-rx},${hy}`; + const i0 = Math.floor(x0 / wx), i1 = Math.ceil(x1 / wx); + const j0 = Math.floor((y0 + hy) / wy), j1 = Math.ceil((y1 - hy) / wy) + 1; + const m = []; + for (let j = j0; j < j1; ++j) { + for (let i = i0; i < i1; ++i) { + m.push(`M${i * wx + (j & 1) * rx},${j * wy}${path}`); + } + } + return create("svg:g") + .call(applyIndirectStyles, this, dimensions) + .call(g => g.append("path") + .call(applyDirectStyles, this) + .call(applyTransform, null, null, offset + dx + ox, offset + dy + oy) + .attr("d", m.join(""))) + .node(); + } +} diff --git a/src/options.js b/src/options.js index adc602f504..5e879dd62e 100644 --- a/src/options.js +++ b/src/options.js @@ -1,7 +1,5 @@ import {parse as isoParse} from "isoformat"; import {color, descending, quantile} from "d3"; -import {symbolAsterisk, symbolDiamond2, symbolPlus, symbolSquare2, symbolTriangle2, symbolX as symbolTimes} from "d3"; -import {symbolCircle, symbolCross, symbolDiamond, symbolSquare, symbolStar, symbolTriangle, symbolWye} from "d3"; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray const TypedArray = Object.getPrototypeOf(Uint8Array); @@ -23,6 +21,7 @@ export const indexOf = (d, i) => i; export const identity = {transform: d => d}; export const zero = () => 0; export const one = () => 1; +export const yes = () => true; export const string = x => x == null ? x : `${x}`; export const number = x => x == null ? x : +x; export const boolean = x => x == null ? x : !!x; @@ -319,48 +318,6 @@ export function isRound(value) { return /^\s*round\s*$/i.test(value); } -const symbols = new Map([ - ["asterisk", symbolAsterisk], - ["circle", symbolCircle], - ["cross", symbolCross], - ["diamond", symbolDiamond], - ["diamond2", symbolDiamond2], - ["plus", symbolPlus], - ["square", symbolSquare], - ["square2", symbolSquare2], - ["star", symbolStar], - ["times", symbolTimes], - ["triangle", symbolTriangle], - ["triangle2", symbolTriangle2], - ["wye", symbolWye] -]); - -function isSymbolObject(value) { - return value && typeof value.draw === "function"; -} - -export function isSymbol(value) { - if (isSymbolObject(value)) return true; - if (typeof value !== "string") return false; - return symbols.has(value.toLowerCase()); -} - -export function maybeSymbol(symbol) { - if (symbol == null || isSymbolObject(symbol)) return symbol; - const value = symbols.get(`${symbol}`.toLowerCase()); - if (value) return value; - throw new Error(`invalid symbol: ${symbol}`); -} - -export function maybeSymbolChannel(symbol) { - if (symbol == null || isSymbolObject(symbol)) return [undefined, symbol]; - if (typeof symbol === "string") { - const value = symbols.get(`${symbol}`.toLowerCase()); - if (value) return [undefined, value]; - } - return [symbol, undefined]; -} - export function maybeFrameAnchor(value = "middle") { return keyword(value, "frameAnchor", ["middle", "top-left", "top", "top-right", "right", "bottom-right", "bottom", "bottom-left", "left"]); } diff --git a/src/plot.js b/src/plot.js index bd2d391400..b3f2d7b1dd 100644 --- a/src/plot.js +++ b/src/plot.js @@ -1,11 +1,12 @@ import {create, cross, difference, groups, InternMap, select} from "d3"; import {Axes, autoAxisTicks, autoScaleLabels} from "./axes.js"; -import {Channel, channelSort} from "./channel.js"; +import {Channel, channelObject, channelSort, valueObject} from "./channel.js"; import {defined} from "./defined.js"; import {Dimensions} from "./dimensions.js"; import {Legends, exposeLegends} from "./legends.js"; -import {arrayify, isOptions, keyword, range, second, where} from "./options.js"; -import {Scales, ScaleFunctions, autoScaleRange, applyScales, exposeScales} from "./scales.js"; +import {arrayify, isOptions, isScaleOptions, keyword, range, second, where, yes} from "./options.js"; +import {Scales, ScaleFunctions, autoScaleRange, exposeScales} from "./scales.js"; +import {registry as scaleRegistry} from "./scales/index.js"; import {applyInlineStyles, maybeClassName, maybeClip, styles} from "./style.js"; import {basic} from "./transforms/basic.js"; import {consumeWarnings} from "./warnings.js"; @@ -29,25 +30,35 @@ export function plot(options = {}) { // A Map from scale name to an array of associated channels. const channelsByScale = new Map(); + // If a scale is explicitly declared in options, initialize its associated + // channels to the empty array; this will guarantee that a corresponding scale + // will be created later (even if there are no other channels). But ignore + // facet scale declarations if faceting is not enabled. + for (const key of scaleRegistry.keys()) { + if (isScaleOptions(options[key]) && key !== "fx" && key !== "fy") { + channelsByScale.set(key, []); + } + } + // Faceting! let facets; // array of facet definitions (e.g. [["foo", [0, 1, 3, …]], …]) let facetIndex; // index over the facet data, e.g. [0, 1, 2, 3, …] - let facetChannels; // e.g. [["fx", {value}], ["fy", {value}]] + let facetChannels; // e.g. {fx: {value}, fy: {value}} let facetsIndex; // nested array of facet indexes [[0, 1, 3, …], [2, 5, …], …] let facetsExclude; // lazily-constructed opposite of facetsIndex if (facet !== undefined) { const {x, y} = facet; if (x != null || y != null) { const facetData = arrayify(facet.data); - facetChannels = []; + facetChannels = {}; if (x != null) { const fx = Channel(facetData, {value: x, scale: "fx"}); - facetChannels.push(["fx", fx]); + facetChannels.fx = fx; channelsByScale.set("fx", [fx]); } if (y != null) { const fy = Channel(facetData, {value: y, scale: "fy"}); - facetChannels.push(["fy", fy]); + facetChannels.fy = fy; channelsByScale.set("fy", [fy]); } facetIndex = range(facetData); @@ -56,33 +67,21 @@ export function plot(options = {}) { } } - // Initialize the marks’ channels, indexing them by mark and scale as needed. + // Initialize the marks’ state. for (const mark of marks) { if (stateByMark.has(mark)) throw new Error("duplicate mark; each mark must be unique"); - const markFacets = facets === undefined ? undefined + const markFacets = facetsIndex === undefined ? undefined : mark.facet === "auto" ? mark.data === facet.data ? facetsIndex : undefined : mark.facet === "include" ? facetsIndex : mark.facet === "exclude" ? facetsExclude || (facetsExclude = facetsIndex.map(f => Uint32Array.from(difference(facetIndex, f)))) : undefined; - const {index, channels} = mark.initialize(markFacets, facetChannels); - for (const [, channel] of channels) { - const {scale} = channel; - if (scale !== undefined) { - const channels = channelsByScale.get(scale); - if (channels !== undefined) channels.push(channel); - else channelsByScale.set(scale, [channel]); - } - } - stateByMark.set(mark, {index, channels, faceted: markFacets !== undefined}); - } - - // Apply scale transforms, mutating channel.value. - for (const [scale, channels] of channelsByScale) { - const {percent, transform = percent ? x => x * 100 : undefined} = options[scale] || {}; - if (transform != null) for (const c of channels) c.value = Array.from(c.value, transform); + const {data, facets, channels} = mark.initialize(markFacets, facetChannels); + applyScaleTransforms(channels, options); + stateByMark.set(mark, {data, facets, channels}); } - const scaleDescriptors = Scales(channelsByScale, options); + // Initalize the scales and axes. + const scaleDescriptors = Scales(addScaleChannels(channelsByScale, stateByMark), options); const scales = ScaleFunctions(scaleDescriptors); const axes = Axes(scaleDescriptors, options); const dimensions = Dimensions(scaleDescriptors, axes, options); @@ -91,9 +90,37 @@ export function plot(options = {}) { autoScaleLabels(channelsByScale, scaleDescriptors, axes, dimensions, options); autoAxisTicks(scaleDescriptors, axes); + const {fx, fy} = scales; + const fyMargins = fy && {marginTop: 0, marginBottom: 0, height: fy.bandwidth()}; + const fxMargins = fx && {marginRight: 0, marginLeft: 0, width: fx.bandwidth()}; + const subdimensions = {...dimensions, ...fxMargins, ...fyMargins}; + + // Reinitialize; for deriving channels dependent on other channels. + const newByScale = new Set(); + for (const [mark, state] of stateByMark) { + if (mark.reinitialize != null) { + const {facets, channels} = mark.reinitialize(state.data, state.facets, state.channels, scales, subdimensions); + if (facets !== undefined) state.facets = facets; + if (channels !== undefined) { + inferChannelScale(channels, mark); + applyScaleTransforms(channels, options); + Object.assign(state.channels, channels); + for (const {scale} of Object.values(channels)) if (scale != null) newByScale.add(scale); + } + } + } + + // Reconstruct scales if new scaled channels were created during reinitialization. + if (newByScale.size) { + const newScaleDescriptors = Scales(addScaleChannels(new Map(), stateByMark, key => newByScale.has(key)), options); + const newScales = ScaleFunctions(newScaleDescriptors); + Object.assign(scaleDescriptors, newScaleDescriptors); + Object.assign(scales, newScales); + } + // Compute value objects, applying scales as needed. for (const state of stateByMark.values()) { - state.values = applyScales(state.channels, scales); + state.values = valueObject(state.channels, scales); } const {width, height} = dimensions; @@ -126,7 +153,6 @@ export function plot(options = {}) { .node(); // When faceting, render axes for fx and fy instead of x and y. - const {fx, fy} = scales; const axisY = axes[facets !== undefined && fy ? "fy" : "y"]; const axisX = axes[facets !== undefined && fx ? "fx" : "x"]; if (axisY) svg.appendChild(axisY.render(null, scales, dimensions)); @@ -136,9 +162,6 @@ export function plot(options = {}) { if (facets !== undefined) { const fyDomain = fy && fy.domain(); const fxDomain = fx && fx.domain(); - const fyMargins = fy && {marginTop: 0, marginBottom: 0, height: fy.bandwidth()}; - const fxMargins = fx && {marginRight: 0, marginLeft: 0, width: fx.bandwidth()}; - const subdimensions = {...dimensions, ...fxMargins, ...fyMargins}; const indexByFacet = facetMap(facetChannels); facets.forEach(([key], i) => indexByFacet.set(key, i)); const selection = select(svg); @@ -175,16 +198,16 @@ export function plot(options = {}) { .attr("transform", facetTranslate(fx, fy)) .each(function(key) { const j = indexByFacet.get(key); - for (const [mark, {channels, values, index, faceted}] of stateByMark) { - const renderIndex = mark.filter(faceted ? index[j] : index, channels, values); - const node = mark.render(renderIndex, scales, values, subdimensions); + for (const [mark, {channels, values, facets}] of stateByMark) { + const facet = facets ? mark.filter(facets[j] ?? facets[0], channels, values) : null; + const node = mark.render(facet, scales, values, subdimensions); if (node != null) this.appendChild(node); } }); } else { - for (const [mark, {channels, values, index}] of stateByMark) { - const renderIndex = mark.filter(index, channels, values); - const node = mark.render(renderIndex, scales, values, dimensions); + for (const [mark, {channels, values, facets}] of stateByMark) { + const facet = facets ? mark.filter(facets[0], channels, values) : null; + const node = mark.render(facet, scales, values, dimensions); if (node != null) svg.appendChild(node); } } @@ -227,6 +250,7 @@ export class Mark { const {facet = "auto", sort, dx, dy, clip} = options; const names = new Set(); this.data = data; + this.reinitialize = options.initialize; this.sort = isOptions(sort) ? sort : null; this.facet = facet == null || facet === false ? null : keyword(facet === true ? "include" : facet, "facet", ["auto", "include", "exclude"]); const {transform} = basic(options); @@ -249,25 +273,18 @@ export class Mark { this.dy = +dy || 0; this.clip = maybeClip(clip); } - initialize(facetIndex, facetChannels) { + initialize(facets, facetChannels) { let data = arrayify(this.data); - let index = facetIndex === undefined && data != null ? range(data) : facetIndex; - if (data !== undefined && this.transform !== undefined) { - if (facetIndex === undefined) index = index.length ? [index] : []; - ({facets: index, data} = this.transform(data, index)); - data = arrayify(data); - if (facetIndex === undefined && index.length) ([index] = index); - } - const channels = this.channels.map(channel => { - const {name} = channel; - return [name == null ? undefined : `${name}`, Channel(data, channel)]; - }); + if (facets === undefined && data != null) facets = [range(data)]; + if (this.transform != null) ({facets, data} = this.transform(data, facets)), data = arrayify(data); + const channels = channelObject(this.channels, data); if (this.sort != null) channelSort(channels, facetChannels, data, this.sort); - return {index, channels}; + return {data, facets, channels}; } filter(index, channels, values) { - for (const [name, {filter = defined}] of channels) { - if (name !== undefined && filter !== null) { + for (const name in channels) { + const {filter = defined} = channels[name]; + if (filter !== null) { const value = values[name]; index = index.filter(i => filter(value[i])); } @@ -298,6 +315,53 @@ class Render extends Mark { render() {} } +// Note: mutates channel.value to apply the scale transform, if any. +function applyScaleTransforms(channels, options) { + for (const name in channels) { + const channel = channels[name]; + const {scale} = channel; + if (scale != null) { + const {percent, transform = percent ? x => x * 100 : undefined} = options[scale] || {}; + if (transform != null) channel.value = Array.from(channel.value, transform); + } + } + return channels; +} + +// An initializer may generate channels without knowing how the downstream mark +// will use them. Marks are typically responsible associated scales with +// channels, but here we assume common behavior across marks. +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; + case "r": case "length": case "symbol": scale = name; break; + default: scale = null; + } + channel.scale = scale; + } + } +} + +function addScaleChannels(channelsByScale, stateByMark, filter = yes) { + for (const {channels} of stateByMark.values()) { + for (const name in channels) { + const channel = channels[name]; + const {scale} = channel; + if (scale != null && filter(scale)) { + const channels = channelsByScale.get(scale); + if (channels !== undefined) channels.push(channel); + else channelsByScale.set(scale, [channel]); + } + } + } + return channelsByScale; +} + // Derives a copy of the specified axis with the label disabled. function nolabel(axis) { return axis === undefined || axis.label === undefined @@ -316,15 +380,17 @@ function facetKeys({fx, fy}) { // Returns an array of [[key1, index1], [key2, index2], …] representing the data // indexes associated with each facet. For two-dimensional faceting, each key // is a two-element array; see also facetMap. -function facetGroups(index, channels) { - return (channels.length > 1 ? facetGroup2 : facetGroup1)(index, ...channels); +function facetGroups(index, {fx, fy}) { + return fx && fy ? facetGroup2(index, fx, fy) + : fx ? facetGroup1(index, fx) + : facetGroup1(index, fy); } -function facetGroup1(index, [, {value: F}]) { +function facetGroup1(index, {value: F}) { return groups(index, i => F[i]); } -function facetGroup2(index, [, {value: FX}], [, {value: FY}]) { +function facetGroup2(index, {value: FX}, {value: FY}) { return groups(index, i => FX[i], i => FY[i]) .flatMap(([x, xgroup]) => xgroup .map(([y, ygroup]) => [[x, y], ygroup])); @@ -337,8 +403,8 @@ function facetTranslate(fx, fy) { : ky => `translate(0,${fy(ky)})`; } -function facetMap(channels) { - return new (channels.length > 1 ? FacetMap2 : FacetMap); +function facetMap({fx, fy}) { + return new (fx && fy ? FacetMap2 : FacetMap); } class FacetMap { diff --git a/src/scales.js b/src/scales.js index 5316f8e285..552d17e52b 100644 --- a/src/scales.js +++ b/src/scales.js @@ -1,13 +1,14 @@ import {parse as isoParse} from "isoformat"; -import {isColor, isEvery, isOrdinal, isFirst, isSymbol, isTemporal, isTemporalString, isNumericString, isScaleOptions, isTypedArray, map, maybeSymbol, order} from "./options.js"; +import {isColor, isEvery, isOrdinal, isFirst, isTemporal, isTemporalString, isNumericString, isScaleOptions, isTypedArray, map, order} from "./options.js"; import {registry, color, position, radius, opacity, symbol, length} from "./scales/index.js"; import {ScaleLinear, ScaleSqrt, ScalePow, ScaleLog, ScaleSymlog, ScaleQuantile, ScaleQuantize, ScaleThreshold, ScaleIdentity} from "./scales/quantitative.js"; import {ScaleDiverging, ScaleDivergingSqrt, ScaleDivergingPow, ScaleDivergingLog, ScaleDivergingSymlog} from "./scales/diverging.js"; import {ScaleTime, ScaleUtc} from "./scales/temporal.js"; import {ScaleOrdinal, ScalePoint, ScaleBand, ordinalImplicit} from "./scales/ordinal.js"; import {warn} from "./warnings.js"; +import {isSymbol, maybeSymbol} from "./symbols.js"; -export function Scales(channels, { +export function Scales(channelsByScale, { inset: globalInset = 0, insetTop: globalInsetTop = globalInset, insetRight: globalInsetRight = globalInset, @@ -21,42 +22,39 @@ export function Scales(channels, { ...options } = {}) { const scales = {}; - for (const key of registry.keys()) { - const scaleChannels = channels.get(key); + for (const [key, channels] of channelsByScale) { const scaleOptions = options[key]; - if (scaleChannels || scaleOptions) { - const scale = Scale(key, scaleChannels, { - round: registry.get(key) === position ? round : undefined, // only for position - nice, - clamp, - align, - padding, - ...scaleOptions - }); - if (scale) { - // populate generic scale options (percent, transform, insets) - let { - percent, - transform, - inset, - insetTop = inset !== undefined ? inset : key === "y" ? globalInsetTop : 0, // not fy - insetRight = inset !== undefined ? inset : key === "x" ? globalInsetRight : 0, // not fx - insetBottom = inset !== undefined ? inset : key === "y" ? globalInsetBottom : 0, // not fy - insetLeft = inset !== undefined ? inset : key === "x" ? globalInsetLeft : 0 // not fx - } = scaleOptions || {}; - if (transform == null) transform = undefined; - else if (typeof transform !== "function") throw new Error(`invalid scale transform; not a function`); - scale.percent = !!percent; - scale.transform = transform; - if (key === "x" || key === "fx") { - scale.insetLeft = +insetLeft; - scale.insetRight = +insetRight; - } else if (key === "y" || key === "fy") { - scale.insetTop = +insetTop; - scale.insetBottom = +insetBottom; - } - scales[key] = scale; + const scale = Scale(key, channels, { + round: registry.get(key) === position ? round : undefined, // only for position + nice, + clamp, + align, + padding, + ...scaleOptions + }); + if (scale) { + // populate generic scale options (percent, transform, insets) + let { + percent, + transform, + inset, + insetTop = inset !== undefined ? inset : key === "y" ? globalInsetTop : 0, // not fy + insetRight = inset !== undefined ? inset : key === "x" ? globalInsetRight : 0, // not fx + insetBottom = inset !== undefined ? inset : key === "y" ? globalInsetBottom : 0, // not fy + insetLeft = inset !== undefined ? inset : key === "x" ? globalInsetLeft : 0 // not fx + } = scaleOptions || {}; + if (transform == null) transform = undefined; + else if (typeof transform !== "function") throw new Error("invalid scale transform; not a function"); + scale.percent = !!percent; + scale.transform = transform; + if (key === "x" || key === "fx") { + scale.insetLeft = +insetLeft; + scale.insetRight = +insetRight; + } else if (key === "y" || key === "fy") { + scale.insetTop = +insetTop; + scale.insetBottom = +insetBottom; } + scales[key] = scale; } } return scales; @@ -319,23 +317,6 @@ export function scaleOrder({range, domain = range}) { return Math.sign(order(domain)) * Math.sign(order(range)); } -// TODO use Float64Array.from for position and radius scales? -export function applyScales(channels, scales) { - const values = Object.create(null); - for (let [name, {value, scale}] of channels) { - if (name !== undefined) { - if (scale !== undefined) { - scale = scales[scale]; - if (scale !== undefined) { - value = Array.from(value, scale); - } - } - values[name] = value; - } - } - return values; -} - // Certain marks have special behavior if a scale is collapsed, i.e. if the // domain is degenerate and represents only a single value such as [3, 3]; for // example, a rect will span the full extent of the chart along a collapsed diff --git a/src/scales/ordinal.js b/src/scales/ordinal.js index 90ee306dec..cb661c204c 100644 --- a/src/scales/ordinal.js +++ b/src/scales/ordinal.js @@ -1,7 +1,8 @@ import {InternSet, quantize, reverse as reverseof, sort, symbolsFill, symbolsStroke} from "d3"; import {scaleBand, scaleOrdinal, scalePoint, scaleImplicit} from "d3"; import {ascendingDefined} from "../defined.js"; -import {maybeSymbol, isNoneish} from "../options.js"; +import {isNoneish} from "../options.js"; +import {maybeSymbol} from "../symbols.js"; import {registry, color, symbol} from "./index.js"; import {maybeBooleanRange, ordinalScheme, quantitativeScheme} from "./schemes.js"; @@ -105,7 +106,7 @@ function maybeRound(scale, channels, options) { function inferDomain(channels) { const values = new InternSet(); for (const {value, domain} of channels) { - if (domain !== undefined) return domain(); + if (domain !== undefined) return domain(); // see channelSort if (value === undefined) continue; for (const v of value) values.add(v); } @@ -113,16 +114,22 @@ function inferDomain(channels) { } // If all channels provide a consistent hint, propagate it to the scale. -function inferSymbolHint(channels) { - const hint = {}; - for (const {hint: channelHint} of channels) { - for (const key of ["fill", "stroke"]) { - const value = channelHint[key]; - if (!(key in hint)) hint[key] = value; - else if (hint[key] !== value) hint[key] = undefined; - } +function inferHint(channels, key) { + let value; + for (const {hint} of channels) { + const candidate = hint?.[key]; + if (candidate === undefined) continue; // no hint here + if (value === undefined) value = candidate; // first hint + else if (value !== candidate) return; // inconsistent hint } - return hint; + return value; +} + +function inferSymbolHint(channels) { + return { + fill: inferHint(channels, "fill"), + stroke: inferHint(channels, "stroke") + }; } function inferSymbolRange(hint) { diff --git a/src/scales/quantitative.js b/src/scales/quantitative.js index 9c69c18dcb..5babc5c275 100644 --- a/src/scales/quantitative.js +++ b/src/scales/quantitative.js @@ -214,9 +214,11 @@ function inferZeroDomain(channels) { } // We don’t want the upper bound of the radial domain to be zero, as this would -// be degenerate, so we ignore nonpositive values. We also don’t want the maximum -// default radius to exceed 30px. +// be degenerate, so we ignore nonpositive values. We also don’t want the +// maximum default radius to exceed 30px. function inferRadialRange(channels, domain) { + const hint = channels.find(({radius}) => radius !== undefined); + if (hint !== undefined) return [0, hint.radius]; // a natural maximum radius, e.g. hexbins const h25 = quantile(channels, 0.5, ({value}) => value === undefined ? NaN : quantile(value, 0.25, positive)); const range = domain.map(d => 3 * Math.sqrt(d / h25)); const k = 30 / max(range); diff --git a/src/symbols.js b/src/symbols.js new file mode 100644 index 0000000000..e35aaec694 --- /dev/null +++ b/src/symbols.js @@ -0,0 +1,61 @@ +import {symbolAsterisk, symbolDiamond2, symbolPlus, symbolSquare2, symbolTriangle2, symbolX as symbolTimes} from "d3"; +import {symbolCircle, symbolCross, symbolDiamond, symbolSquare, symbolStar, symbolTriangle, symbolWye} from "d3"; + +export const sqrt3 = Math.sqrt(3); +export const sqrt4_3 = 2 / sqrt3; + +const symbolHexagon = { + draw(context, size) { + const rx = Math.sqrt(size / Math.PI), ry = rx * sqrt4_3, hy = ry / 2; + context.moveTo(0, ry); + context.lineTo(rx, hy); + context.lineTo(rx, -hy); + context.lineTo(0, -ry); + context.lineTo(-rx, -hy); + context.lineTo(-rx, hy); + context.closePath(); + } +}; + +const symbols = new Map([ + ["asterisk", symbolAsterisk], + ["circle", symbolCircle], + ["cross", symbolCross], + ["diamond", symbolDiamond], + ["diamond2", symbolDiamond2], + ["hexagon", symbolHexagon], + ["plus", symbolPlus], + ["square", symbolSquare], + ["square2", symbolSquare2], + ["star", symbolStar], + ["times", symbolTimes], + ["triangle", symbolTriangle], + ["triangle2", symbolTriangle2], + ["wye", symbolWye] +]); + +function isSymbolObject(value) { + return value && typeof value.draw === "function"; +} + +export function isSymbol(value) { + if (isSymbolObject(value)) return true; + if (typeof value !== "string") return false; + return symbols.has(value.toLowerCase()); +} + +export function maybeSymbol(symbol) { + if (symbol == null || isSymbolObject(symbol)) return symbol; + const value = symbols.get(`${symbol}`.toLowerCase()); + if (value) return value; + throw new Error(`invalid symbol: ${symbol}`); +} + +export function maybeSymbolChannel(symbol) { + if (symbol == null || isSymbolObject(symbol)) return [undefined, symbol]; + if (typeof symbol === "string") { + const value = symbols.get(`${symbol}`.toLowerCase()); + if (value) return [undefined, value]; + } + return [symbol, undefined]; +} diff --git a/src/transforms/basic.js b/src/transforms/basic.js index 2f99f3b2e3..cea9ae9dfc 100644 --- a/src/transforms/basic.js +++ b/src/transforms/basic.js @@ -9,6 +9,7 @@ export function basic({ sort: s1, reverse: r1, transform: t1, + initialize: i1, ...options } = {}, t2) { if (t1 === undefined) { // explicit transform overrides filter, sort, and reverse @@ -16,6 +17,7 @@ export function basic({ if (s1 != null && !isOptions(s1)) t1 = composeTransform(t1, sortTransform(s1)); if (r1) t1 = composeTransform(t1, reverseTransform); } + if (t2 != null && i1 != null) throw new Error("Data transforms must appear before any channel transform"); return { ...options, ...isOptions(s1) && {sort: s1}, diff --git a/src/transforms/dodge.js b/src/transforms/dodge.js new file mode 100644 index 0000000000..770a051816 --- /dev/null +++ b/src/transforms/dodge.js @@ -0,0 +1,101 @@ +import {max} from "d3"; +import IntervalTree from "interval-tree-1d"; +import {finite, positive} from "../defined.js"; +import {initialize} from "./initialize.js"; + +const anchorXLeft = ({marginLeft}) => [1, marginLeft]; +const anchorXRight = ({width, marginRight}) => [-1, width - marginRight]; +const anchorXMiddle = ({width, marginLeft, marginRight}) => [0, (marginLeft + width - marginRight) / 2]; +const anchorYTop = ({marginTop}) => [1, marginTop]; +const anchorYBottom = ({height, marginBottom}) => [-1, height - marginBottom]; +const anchorYMiddle = ({height, marginTop, marginBottom}) => [0, (marginTop + height - marginBottom) / 2]; + +function maybeAnchor(anchor) { + return typeof anchor === "string" ? {anchor} : anchor; +} + +export function dodgeX(dodgeOptions = {}, options = {}) { + if (arguments.length === 1) [options, dodgeOptions] = [dodgeOptions, options]; + let {anchor = "left", padding = 1} = maybeAnchor(dodgeOptions); + switch (`${anchor}`.toLowerCase()) { + case "left": anchor = anchorXLeft; break; + case "right": anchor = anchorXRight; break; + case "middle": anchor = anchorXMiddle; break; + default: throw new Error(`unknown dodge anchor: ${anchor}`); + } + return dodge("x", "y", anchor, +padding, options); +} + +export function dodgeY(dodgeOptions = {}, options = {}) { + if (arguments.length === 1) [options, dodgeOptions] = [dodgeOptions, options]; + let {anchor = "bottom", padding = 1} = maybeAnchor(dodgeOptions); + switch (`${anchor}`.toLowerCase()) { + case "top": anchor = anchorYTop; break; + case "bottom": anchor = anchorYBottom; break; + case "middle": anchor = anchorYMiddle; break; + default: throw new Error(`unknown dodge anchor: ${anchor}`); + } + return dodge("y", "x", anchor, +padding, options); +} + +function dodge(y, x, anchor, padding, options) { + return initialize(options, function(data, facets, {[x]: X, r: R}, {[x]: xscale, r: rscale}, dimensions) { + if (!X) throw new Error(`missing channel ${x}`); + X = X.value.map(xscale); + const r = R ? undefined : this.r !== undefined ? this.r : options.r !== undefined ? +options.r : 3; + if (R) R = R.value.map(rscale); + if (X == null) throw new Error(`missing channel: ${x}`); + let [ky, ty] = anchor(dimensions); + const compare = ky ? compareAscending : compareSymmetric; + if (ky) ty += ky * ((R ? max(facets.flat(), i => R[i]) : r) + padding); else ky = 1; + const Y = new Float64Array(X.length); + const radius = R ? i => R[i] : () => r; + for (let I of facets) { + const tree = IntervalTree(); + I = I.filter(R + ? i => finite(X[i]) && positive(R[i]) + : i => finite(X[i])); + for (const i of I) { + const intervals = []; + const l = X[i] - radius(i); + const h = X[i] + radius(i); + + // For any previously placed circles that may overlap this circle, compute + // the y-positions that place this circle tangent to these other circles. + // https://observablehq.com/@mbostock/circle-offset-along-line + tree.queryInterval(l - padding, h + padding, ([,, j]) => { + const yj = Y[j]; + const dx = X[i] - X[j]; + const dr = padding + (R ? R[i] + R[j] : 2 * r); + const dy = Math.sqrt(dr * dr - dx * dx); + intervals.push([yj - dy, yj + dy]); + }); + + // Find the best y-value where this circle can fit. + for (let y of intervals.flat().sort(compare)) { + if (intervals.every(([lo, hi]) => y <= lo || y >= hi)) { + Y[i] = y; + break; + } + } + + // Insert the placed circle into the interval tree. + tree.insert([l, h, i]); + } + for (const i of I) Y[i] = Y[i] * ky + ty; + } + return {data, facets, channels: { + [x]: {value: X}, + [y]: {value: Y}, + ...R && {r: {value: R}} + }}; + }); +} + +function compareSymmetric(a, b) { + return Math.abs(a) - Math.abs(b); +} + +function compareAscending(a, b) { + return (a < 0) - (b < 0) || (a - b); +} diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js new file mode 100644 index 0000000000..e3ca03b827 --- /dev/null +++ b/src/transforms/hexbin.js @@ -0,0 +1,111 @@ +import {group} from "d3"; +import {sqrt3} from "../symbols.js"; +import {identity, maybeColumn, maybeColorChannel, valueof} from "../options.js"; +import {hasOutput, maybeOutputs} from "./group.js"; +import {initialize} from "./initialize.js"; + +// We don’t want the hexagons to align with the edges of the plot frame, as that +// would cause extreme x-values (the upper bound of the default x-scale domain) +// to be rounded up into a floating bin to the right of the plot. Therefore, +// rather than centering the origin hexagon around ⟨0,0⟩ in screen coordinates, +// we offset slightly to ⟨0.5,0⟩. The hexgrid mark uses the same origin. +export const ox = 0.5, oy = 0; + +export function hexbin(outputs = {fill: "count"}, options = {}) { + const {binWidth, ...rest} = outputs; + return hexbinn(rest, {binWidth, ...options}); +} + +// TODO filter e.g. to show empty hexbins? +// TODO disallow x, x1, x2, y, y1, y2 reducers? +function hexbinn(outputs, {binWidth = 20, fill, stroke, z, ...options}) { + binWidth = +binWidth; + const [GZ, setGZ] = maybeColumn(z); + const [vfill] = maybeColorChannel(fill); + const [vstroke] = maybeColorChannel(stroke); + const [GF = fill, setGF] = maybeColumn(vfill); + const [GS = stroke, setGS] = maybeColumn(vstroke); + outputs = maybeOutputs({ + ...setGF && {fill: "first"}, + ...setGS && {stroke: "first"}, + ...outputs + }, {fill, stroke, ...options}); + return { + symbol: "hexagon", + ...!hasOutput(outputs, "r") && {r: binWidth / 2}, + ...!setGF && {fill}, + ...((hasOutput(outputs, "fill") || setGF) && stroke === undefined) ? {stroke: "none"} : {stroke}, + ...initialize(options, function(data, facets, {x: X, y: Y}, scales) { + if (setGF) setGF(valueof(data, vfill)); + if (setGS) setGS(valueof(data, vstroke)); + if (setGZ) setGZ(valueof(data, z)); + for (const o of outputs) o.initialize(data); + if (X === undefined) throw new Error("missing channel: x"); + if (Y === undefined) throw new Error("missing channel: y"); + const x = X.scale !== undefined ? scales[X.scale] : identity.transform; + const y = Y.scale !== undefined ? scales[Y.scale] : identity.transform; + X = X.value.map(x); + Y = Y.value.map(y); + const F = setGF && GF.transform(); + const S = setGS && GS.transform(); + const Z = setGZ ? GZ.transform() : (F || S); + const binFacets = []; + const BX = []; + const BY = []; + let i = -1; + for (const facet of facets) { + const binFacet = []; + for (const o of outputs) o.scope("facet", facet); + for (const index of Z ? group(facet, i => Z[i]).values() : [facet]) { + for (const bin of hbin(index, X, Y, binWidth)) { + binFacet.push(++i); + BX.push(bin.x); + BY.push(bin.y); + for (const o of outputs) o.reduce(bin); + } + } + binFacets.push(binFacet); + } + const channels = { + x: {value: BX}, + y: {value: BY}, + ...Object.fromEntries(outputs.map(({name, output}) => [name, {scale: true, binWidth: name === "r" ? binWidth : undefined, value: output.transform()}])) + }; + if ("r" in channels) { + const R = channels.r.value; + binFacets.forEach(index => index.sort((i, j) => R[j] - R[i])); + } + return {data, facets: binFacets, channels}; + }) + }; +} + +function hbin(I, X, Y, dx) { + const dy = dx * sqrt3 / 2; + const bins = new Map(); + for (const i of I) { + let px = X[i] / dx; + let py = Y[i] / dy; + if (isNaN(px) || isNaN(py)) continue; + let pj = Math.round(py), + pi = Math.round(px = px - (pj & 1) / 2), + py1 = py - pj; + if (Math.abs(py1) * 3 > 1) { + let px1 = px - pi, + pi2 = pi + (px < pi ? -1 : 1) / 2, + pj2 = pj + (py < pj ? -1 : 1), + px2 = px - pi2, + py2 = py - pj2; + if (px1 * px1 + py1 * py1 > px2 * px2 + py2 * py2) pi = pi2 + (pj & 1 ? 1 : -1) / 2, pj = pj2; + } + const key = `${pi},${pj}`; + let g = bins.get(key); + if (g === undefined) { + bins.set(key, g = []); + g.x = (pi + (pj & 1) / 2) * dx; + g.y = pj * dy; + } + g.push(i); + } + return bins.values(); +} diff --git a/src/transforms/initialize.js b/src/transforms/initialize.js new file mode 100644 index 0000000000..a89a8cfdca --- /dev/null +++ b/src/transforms/initialize.js @@ -0,0 +1,19 @@ +// If both i1 and i2 are defined, returns a composite initializer that first +// applies i1 and then applies i2. +export function initialize({initialize: i1, ...options} = {}, i2) { + return { + ...options, + initialize: composeInitialize(i1, i2) + }; +} + +function composeInitialize(i1, i2) { + if (i1 == null) return i2 === null ? undefined : i2; + if (i2 == null) return i1 === null ? undefined : i1; + return function(data, facets, channels, scales, dimensions) { + let c1, c2; + ({data, facets, channels: c1} = i1.call(this, data, facets, channels, scales, dimensions)); + ({data, facets, channels: c2} = i2.call(this, data, facets, {...channels, ...c1}, scales, dimensions)); + return {data, facets, channels: {...c1, ...c2}}; + }; +} diff --git a/test/output/carsJiggle.html b/test/output/carsJiggle.html new file mode 100644 index 0000000000..e2b2d8cc82 --- /dev/null +++ b/test/output/carsJiggle.html @@ -0,0 +1,499 @@ +
+ + + + + 50 + + + 100 + + + 150 + + + 200 + power (hp) + + + + + + + 8 + + + + 6 + + + + 5 + + + + 4 + + + + 3 + cylinders + + + + 1,500 + + + 2,000 + + + 2,500 + + + 3,000 + + + 3,500 + + + 4,000 + + + 4,500 + + + 5,000 + + + 5,500 + weight (lb) → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/test/output/darkerDodge.svg b/test/output/darkerDodge.svg new file mode 100644 index 0000000000..2946ee7fc0 --- /dev/null +++ b/test/output/darkerDodge.svg @@ -0,0 +1,200 @@ + + + + + 0 + + + 1 + + + 2 + + + 3 + + + 4 + + + 5 + + + 6 + + + 7 + + + 8 + + + 9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/hexbin.svg b/test/output/hexbin.svg new file mode 100644 index 0000000000..b600b89934 --- /dev/null +++ b/test/output/hexbin.svg @@ -0,0 +1,284 @@ + + + + + 34 + + + 36 + + + 38 + + + 40 + + + 42 + + + 44 + + + 46 + + + 48 + + + 50 + + + 52 + + + 54 + + + 56 + + + 58 + ↑ culmen_length_mm + + + + 14 + + + 15 + + + 16 + + + 17 + + + 18 + + + 19 + + + 20 + + + 21 + culmen_depth_mm → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/hexbinOranges.svg b/test/output/hexbinOranges.svg new file mode 100644 index 0000000000..a855e5439e --- /dev/null +++ b/test/output/hexbinOranges.svg @@ -0,0 +1,151 @@ + + + + + 35 + + + 40 + + + 45 + + + 50 + + + 55 + ↑ culmen_length_mm + + + + 14 + + + 15 + + + 16 + + + 17 + + + 18 + + + 19 + + + 20 + + + 21 + culmen_depth_mm → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/hexbinR.html b/test/output/hexbinR.html new file mode 100644 index 0000000000..be11949cf5 --- /dev/null +++ b/test/output/hexbinR.html @@ -0,0 +1,521 @@ +
+ + + + + 0 + + + 5 + + + 10 + + + 15 + + + 20 + Proportion of each facet (%) + + + + + + 35 + + + 40 + + + 45 + + + 50 + + + 55 + ↑ culmen_length_mm + + + + FEMALE + + + MALE + + + + sex + + + + 14 + + + 16 + + + 18 + + + 20 + + + + + 14 + + + 16 + + + 18 + + + 20 + + + + + 14 + + + 16 + + + 18 + + + 20 + culmen_depth_mm → + + + + + + 0.067 + + + 0.055 + + + 0.048 + + + 0.048 + + + 0.042 + + + 0.042 + + + 0.042 + + + 0.036 + + + 0.036 + + + 0.03 + + + 0.03 + + + 0.03 + + + 0.03 + + + 0.024 + + + 0.024 + + + 0.024 + + + 0.018 + + + 0.018 + + + 0.018 + + + 0.018 + + + 0.018 + + + 0.018 + + + 0.018 + + + 0.012 + + + 0.012 + + + 0.012 + + + 0.012 + + + 0.012 + + + 0.012 + + + 0.012 + + + 0.012 + + + 0.012 + + + 0.012 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + + + + + + 0.083 + + + 0.065 + + + 0.048 + + + 0.042 + + + 0.042 + + + 0.03 + + + 0.03 + + + 0.03 + + + 0.03 + + + 0.024 + + + 0.024 + + + 0.024 + + + 0.024 + + + 0.024 + + + 0.018 + + + 0.018 + + + 0.018 + + + 0.018 + + + 0.018 + + + 0.018 + + + 0.018 + + + 0.018 + + + 0.018 + + + 0.018 + + + 0.012 + + + 0.012 + + + 0.012 + + + 0.012 + + + 0.012 + + + 0.012 + + + 0.012 + + + 0.012 + + + 0.012 + + + 0.012 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + 0.006 + + + + + + + + 0.182 + + + 0.091 + + + 0.091 + + + 0.091 + + + 0.091 + + + 0.091 + + + 0.091 + + + 0.091 + + + +
\ No newline at end of file diff --git a/test/output/hexbinSymbol.html b/test/output/hexbinSymbol.html new file mode 100644 index 0000000000..11c1d0bb82 --- /dev/null +++ b/test/output/hexbinSymbol.html @@ -0,0 +1,236 @@ +
+
+ + + FEMALE + + MALE +
+ + + + + 34 + + + + 36 + + + + 38 + + + + 40 + + + + 42 + + + + 44 + + + + 46 + + + + 48 + + + + 50 + + + + 52 + + + + 54 + + + + 56 + + + + 58 + ↑ culmen_length_mm + + + + + 14 + + + + 15 + + + + 16 + + + + 17 + + + + 18 + + + + 19 + + + + 20 + + + + 21 + culmen_depth_mm → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/test/output/hexbinText.svg b/test/output/hexbinText.svg new file mode 100644 index 0000000000..0e70e704da --- /dev/null +++ b/test/output/hexbinText.svg @@ -0,0 +1,195 @@ + + + + + 35 + + + 40 + + + 45 + + + 50 + + + 55 + ↑ culmen_length_mm + + + + FEMALE + + + MALE + + + + sex + + + + 15 + + + 20 + + + + + 15 + + + 20 + + + + + 15 + + + 20 + culmen_depth_mm → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 729103256610131211311412281224113111611124149142111 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 18212131514179311224111293442123113171034964311112221121 + + + + + + + + + + + + + + 11211111 + + \ No newline at end of file diff --git a/test/output/hexbinZ.svg b/test/output/hexbinZ.svg new file mode 100644 index 0000000000..d2de751545 --- /dev/null +++ b/test/output/hexbinZ.svg @@ -0,0 +1,310 @@ + + + + + 34 + + + 36 + + + 38 + + + 40 + + + 42 + + + 44 + + + 46 + + + 48 + + + 50 + + + 52 + + + 54 + + + 56 + + + 58 + ↑ culmen_length_mm + + + + 14 + + + 15 + + + 16 + + + 17 + + + 18 + + + 19 + + + 20 + + + 21 + culmen_depth_mm → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/penguinDodge.svg b/test/output/penguinDodge.svg new file mode 100644 index 0000000000..2292f7bb81 --- /dev/null +++ b/test/output/penguinDodge.svg @@ -0,0 +1,383 @@ + + + + + 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/output/penguinDodgeHexbin.svg b/test/output/penguinDodgeHexbin.svg new file mode 100644 index 0000000000..7af2735b03 --- /dev/null +++ b/test/output/penguinDodgeHexbin.svg @@ -0,0 +1,760 @@ + + + + + Adelie + + + Chinstrap + + + Gentoo + + + + + 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/output/penguinFacetDodge.svg b/test/output/penguinFacetDodge.svg new file mode 100644 index 0000000000..b81b2ede60 --- /dev/null +++ b/test/output/penguinFacetDodge.svg @@ -0,0 +1,411 @@ + + + + + Adelie + + + Chinstrap + + + Gentoo + + + + + 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/output/penguinFacetDodgeIsland.html b/test/output/penguinFacetDodgeIsland.html new file mode 100644 index 0000000000..8a0a5c05df --- /dev/null +++ b/test/output/penguinFacetDodgeIsland.html @@ -0,0 +1,446 @@ +
+
+ BiscoeDreamTorgersen +
+ + + + Adelie + + + Chinstrap + + + Gentoo + + + + + 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/output/penguinFacetDodgeSymbol.html b/test/output/penguinFacetDodgeSymbol.html new file mode 100644 index 0000000000..0b45525c2e --- /dev/null +++ b/test/output/penguinFacetDodgeSymbol.html @@ -0,0 +1,443 @@ +
+
+ + + Adelie + + Chinstrap + + Gentoo +
+ + + + + 2,500 + + + + 3,000 + + + + 3,500 + + + + 4,000 + + + + 4,500 + + + + 5,000 + + + + 5,500 + + + + 6,000 + + + + 6,500 + ↑ body_mass_g + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/test/plots/cars-jiggle.js b/test/plots/cars-jiggle.js new file mode 100644 index 0000000000..b20a2900e7 --- /dev/null +++ b/test/plots/cars-jiggle.js @@ -0,0 +1,48 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; +import {initialize} from "../../src/transforms/initialize.js"; + +function remap(outputs = {}, options) { + return initialize(options, (data, facets, channels, scales) => { + const newChannels = {}; + for (const [key, map] of Object.entries(outputs)) { + const input = channels[key]; + if (input == null) throw new Error(`missing channel: ${key}`); + const V = Array.from(input.value); + if (input.scale != null) { + const scale = scales[input.scale]; + if (scale != null) { + for (let i = 0; i < V.length; ++i) V[i] = scale(V[i]); + } + } + for (let i = 0; i < V.length; ++i) V[i] = map(V[i]); + newChannels[key] = {value: V}; + } + return { + data, + facets, + channels: newChannels + }; + }); +} + +const random = d3.randomNormal.source(d3.randomLcg(42))(0, 7); + +export default async function() { + const data = await d3.csv("data/cars.csv", d3.autoType); + return Plot.plot({ + height: 350, + y: {type: "band", reverse: true, grid: true}, + color: {nice: true, scheme: "warm", reverse: true, legend: true}, + nice: true, + marks: [ + Plot.dot(data, remap({y: d => d + random()}, { + x: "weight (lb)", + y: "cylinders", + fill: "power (hp)", + stroke: "white", + strokeWidth: 0.5 + })) + ] + }); +} diff --git a/test/plots/darker-dodge.js b/test/plots/darker-dodge.js new file mode 100644 index 0000000000..4395562645 --- /dev/null +++ b/test/plots/darker-dodge.js @@ -0,0 +1,47 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; +import {initialize} from "../../src/transforms/initialize.js"; + +function remap(outputs = {}, options) { + return initialize(options, (data, facets, channels, scales) => { + const newChannels = {}; + for (const [key, map] of Object.entries(outputs)) { + const input = channels[key]; + if (input == null) throw new Error(`missing channel: ${key}`); + const V = Array.from(input.value); + if (input.scale != null) { + const scale = scales[input.scale]; + if (scale != null) { + for (let i = 0; i < V.length; ++i) V[i] = scale(V[i]); + } + } + for (let i = 0; i < V.length; ++i) V[i] = map(V[i]); + newChannels[key] = {value: V}; + } + return { + data, + facets, + channels: newChannels + }; + }); +} + +// In the following, darker and Plot.dodgeY are interchangeable +export default async function() { + return Plot.plot({ + marginTop: 10, + nice: true, + marks: [ + Plot.dotX( + Array.from({ length: 150 }, d3.randomLogNormal.source(d3.randomLcg(42))()), + Plot.dodgeY("middle", remap({ + fill: v => d3.rgb(v).darker(0.7).formatHex() + }, { + x: (d) => d, + fill: (d) => d + })) + ) + ], + height: 170 + }); +} diff --git a/test/plots/hexbin-oranges.js b/test/plots/hexbin-oranges.js new file mode 100644 index 0000000000..29bb84bdeb --- /dev/null +++ b/test/plots/hexbin-oranges.js @@ -0,0 +1,19 @@ +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({ + color: {scheme: "oranges"}, + inset: 30, + marks: [ + Plot.frame(), + Plot.circle(penguins, Plot.hexbin({fill: "count"}, { + x: "culmen_depth_mm", + y: "culmen_length_mm", + binWidth: 35, + strokeWidth: 1 + })) + ] + }); +} diff --git a/test/plots/hexbin-r.js b/test/plots/hexbin-r.js new file mode 100644 index 0000000000..e63eb72ae5 --- /dev/null +++ b/test/plots/hexbin-r.js @@ -0,0 +1,20 @@ +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({ + width: 820, + height: 320, + color: {scheme: "reds", nice: true, tickFormat: d => 100 * d, label: "Proportion of each facet (%)", legend: true}, + facet: { + data: penguins, + x: "sex", + marginRight: 80 + }, + marks: [ + Plot.frame(), + Plot.dot(penguins, Plot.hexbin({title: "proportion-facet", r: "count", fill: "proportion-facet"}, {x: "culmen_depth_mm", y: "culmen_length_mm", strokeWidth: 1})) + ] + }); +} diff --git a/test/plots/hexbin-symbol.js b/test/plots/hexbin-symbol.js new file mode 100644 index 0000000000..d26da69415 --- /dev/null +++ b/test/plots/hexbin-symbol.js @@ -0,0 +1,18 @@ +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({ + grid: true, + marks: [ + Plot.dot(penguins, Plot.hexbin({r: "count", symbol: "mode"}, { + binWidth: 40, + symbol: "sex", + x: "culmen_depth_mm", + y: "culmen_length_mm" + })) + ], + symbol: {legend: true} + }); +} diff --git a/test/plots/hexbin-text.js b/test/plots/hexbin-text.js new file mode 100644 index 0000000000..8ab4086588 --- /dev/null +++ b/test/plots/hexbin-text.js @@ -0,0 +1,21 @@ +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({ + width: 820, + height: 320, + facet: { + data: penguins, + x: "sex", + marginRight: 80 + }, + inset: 14, + marks: [ + Plot.frame(), + Plot.dot(penguins, Plot.hexbin({fillOpacity: "count"}, {x: "culmen_depth_mm", y: "culmen_length_mm", fill: "brown", stroke: "black", strokeWidth: 0.5})), + Plot.text(penguins, Plot.hexbin({text: "count"}, {x: "culmen_depth_mm", y: "culmen_length_mm"})) + ] + }); +} diff --git a/test/plots/hexbin-z.js b/test/plots/hexbin-z.js new file mode 100644 index 0000000000..0ac30caff3 --- /dev/null +++ b/test/plots/hexbin-z.js @@ -0,0 +1,22 @@ +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({ + x: {inset: 10}, + y: {inset: 10}, + marks: [ + Plot.frame(), + Plot.hexgrid(), + Plot.dot(penguins, Plot.hexbin({r: "count"}, { + x: "culmen_depth_mm", + y: "culmen_length_mm", + strokeWidth: 2, + stroke: "sex", + fill: "sex", + fillOpacity: 0.5 + })) + ] + }); +} diff --git a/test/plots/hexbin.js b/test/plots/hexbin.js new file mode 100644 index 0000000000..9b2e63872b --- /dev/null +++ b/test/plots/hexbin.js @@ -0,0 +1,13 @@ +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({ + marks: [ + Plot.frame(), + Plot.hexgrid(), + Plot.dot(penguins, Plot.hexbin({r: "count"}, {x: "culmen_depth_mm", y: "culmen_length_mm"})) + ] + }); +} diff --git a/test/plots/index.js b/test/plots/index.js index 693cd10825..12f9611a39 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -28,6 +28,7 @@ export {default as binTimestamps} from "./bin-timestamps.js"; export {default as boxplot} from "./boxplot.js"; export {default as caltrain} from "./caltrain.js"; export {default as caltrainDirection} from "./caltrain-direction.js"; +export {default as carsJiggle} from "./cars-jiggle.js"; export {default as carsMpg} from "./cars-mpg.js"; export {default as carsParcoords} from "./cars-parcoords.js"; export {default as clamp} from "./clamp.js"; @@ -39,6 +40,7 @@ export {default as collapsedHistogram} from "./collapsed-histogram.js"; export {default as covidIhmeProjectedDeaths} from "./covid-ihme-projected-deaths.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"; export {default as decathlon} from "./decathlon.js"; export {default as diamondsCaratPrice} from "./diamonds-carat-price.js"; export {default as diamondsCaratPriceDots} from "./diamonds-carat-price-dots.js"; @@ -67,6 +69,12 @@ export {default as greekGods} from "./greek-gods.js"; export {default as gridChoropleth} from "./grid-choropleth.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 hexbinOranges} from "./hexbin-oranges.js"; +export {default as hexbinR} from "./hexbin-r.js"; +export {default as hexbinSymbol} from "./hexbin-symbol.js"; +export {default as hexbinText} from "./hexbin-text.js"; +export {default as hexbinZ} from "./hexbin-z.js"; export {default as highCardinalityOrdinal} from "./high-cardinality-ordinal.js"; export {default as identityScale} from "./identity-scale.js"; export {default as industryUnemployment} from "./industry-unemployment.js"; @@ -107,6 +115,11 @@ export {default as musicRevenue} from "./music-revenue.js"; export {default as ordinalBar} from "./ordinal-bar.js"; export {default as penguinCulmen} from "./penguin-culmen.js"; export {default as penguinCulmenArray} from "./penguin-culmen-array.js"; +export {default as penguinDodge} from "./penguin-dodge.js"; +export {default as penguinDodgeHexbin} from "./penguin-dodge-hexbin.js"; +export {default as penguinFacetDodge} from "./penguin-facet-dodge.js"; +export {default as penguinFacetDodgeIsland} from "./penguin-facet-dodge-island.js"; +export {default as penguinFacetDodgeSymbol} from "./penguin-facet-dodge-symbol.js"; export {default as penguinIslandUnknown} from "./penguin-island-unknown.js"; export {default as penguinMass} from "./penguin-mass.js"; export {default as penguinMassSex} from "./penguin-mass-sex.js"; diff --git a/test/plots/penguin-dodge-hexbin.js b/test/plots/penguin-dodge-hexbin.js new file mode 100644 index 0000000000..c2b159f77b --- /dev/null +++ b/test/plots/penguin-dodge-hexbin.js @@ -0,0 +1,26 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +// test channel transform composition +export default async function() { + const penguins = await d3.csv("data/penguins.csv", d3.autoType); + return Plot.plot({ + height: 300, + x: { + grid: true, + inset: 7 + }, + facet: { + data: penguins, + y: "species", + label: null, + marginLeft: 60 + }, + marks: [ + Plot.frame(), + Plot.dot(penguins, Plot.dodgeY("bottom", {x: "body_mass_g", stroke: "red", r: 3})), + Plot.dot(penguins, Plot.hexbin({binWidth: 7}, Plot.dodgeY("bottom", {x: "body_mass_g", fill: "black", r: 3}))) + ], + color: {legend: true} + }); +} diff --git a/test/plots/penguin-dodge.js b/test/plots/penguin-dodge.js new file mode 100644 index 0000000000..47fc6ce892 --- /dev/null +++ b/test/plots/penguin-dodge.js @@ -0,0 +1,12 @@ +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({ + height: 200, + marks: [ + Plot.dot(penguins, Plot.dodgeY({x: "body_mass_g"})) + ] + }); +} diff --git a/test/plots/penguin-facet-dodge-island.js b/test/plots/penguin-facet-dodge-island.js new file mode 100644 index 0000000000..4c40151192 --- /dev/null +++ b/test/plots/penguin-facet-dodge-island.js @@ -0,0 +1,22 @@ +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({ + height: 300, + x: { + grid: true + }, + facet: { + data: penguins, + y: "species", + label: null, + marginLeft: 60 + }, + marks: [ + Plot.dot(penguins, Plot.dodgeY("middle", {x: "body_mass_g", fill: "island"})) + ], + color: {legend: true} + }); +} diff --git a/test/plots/penguin-facet-dodge-symbol.js b/test/plots/penguin-facet-dodge-symbol.js new file mode 100644 index 0000000000..a16dcb5737 --- /dev/null +++ b/test/plots/penguin-facet-dodge-symbol.js @@ -0,0 +1,16 @@ +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({ + height: 450, + width: 300, + marginRight: 60, + y: {grid: true, nice: true}, + symbol: {legend: true}, + marks: [ + Plot.dot(penguins, Plot.dodgeX("left", {y: "body_mass_g", symbol: "species", stroke: "species", dx: 2})) + ] + }); +} diff --git a/test/plots/penguin-facet-dodge.js b/test/plots/penguin-facet-dodge.js new file mode 100644 index 0000000000..77ed20a433 --- /dev/null +++ b/test/plots/penguin-facet-dodge.js @@ -0,0 +1,21 @@ +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({ + height: 300, + x: { + grid: true + }, + facet: { + data: penguins, + y: "species", + label: null, + marginLeft: 60 + }, + marks: [ + Plot.dot(penguins, Plot.dodgeY("middle", {x: "body_mass_g"})) + ] + }); +} diff --git a/test/transforms/normalize-test.js b/test/transforms/normalize-test.js index 92b5884954..98599c34f4 100644 --- a/test/transforms/normalize-test.js +++ b/test/transforms/normalize-test.js @@ -43,6 +43,6 @@ it("Plot.normalize deviation doesn’t crash on equal values", () => { function testNormalize(data, basis, r) { const mark = Plot.dot(data, Plot.normalizeY(basis, {y: data})); - const c = new Map(mark.initialize().channels); - assert.deepStrictEqual(c.get("y").value, r); + const {channels: {y: {value: Y}}} = mark.initialize(); + assert.deepStrictEqual(Y, r); } diff --git a/test/transforms/reduce-test.js b/test/transforms/reduce-test.js index c57c6af9df..b1480407c2 100644 --- a/test/transforms/reduce-test.js +++ b/test/transforms/reduce-test.js @@ -20,6 +20,6 @@ it("function reducers reduce as expected", () => { function testReducer(data, x, r) { const mark = Plot.dot(data, Plot.groupZ({x}, {x: d => d})); - const c = new Map(mark.initialize().channels); - assert.deepStrictEqual(c.get("x").value, [r]); + const {channels: {x: {value: X}}} = mark.initialize(); + assert.deepStrictEqual(X, [r]); } diff --git a/yarn.lock b/yarn.lock index 7dd5f0542e..ccc377322a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -67,6 +67,19 @@ semver "^7.3.5" tar "^6.1.11" +"@rollup/plugin-commonjs@^21.0.1": + version "21.0.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-21.0.2.tgz#0b9c539aa1837c94abfaf87945838b0fc8564891" + integrity sha512-d/OmjaLVO4j/aQX69bwpWPpbvI3TJkQuxoAk7BH8ew1PyoMBLTOuvJTjzG8oEoW7drIIqB0KCJtfFLu/2GClWg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + commondir "^1.0.1" + estree-walker "^2.0.1" + glob "^7.1.6" + is-reference "^1.2.1" + magic-string "^0.25.7" + resolve "^1.17.0" + "@rollup/plugin-json@4": version "4.1.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3" @@ -100,6 +113,11 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== +"@types/estree@*": + version "0.0.51" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== + "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" @@ -242,6 +260,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +binary-search-bounds@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz#125e5bd399882f71e6660d4bf1186384e989fba7" + integrity sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -388,6 +411,11 @@ commander@^2.19.0, commander@^2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -1039,6 +1067,11 @@ estree-walker@^1.0.1: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== +estree-walker@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -1169,7 +1202,7 @@ glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob@7.2.0, glob@^7.1.3: +glob@7.2.0, glob@^7.1.3, glob@^7.1.6: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -1297,6 +1330,13 @@ ini@^1.3.4: resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== +interval-tree-1d@1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/interval-tree-1d/-/interval-tree-1d-1.0.4.tgz#b44f657de7ddae69ea3f98e0a9ad4bb046b07d11" + integrity sha512-wY8QJH+6wNI0uh4pDQzMvl+478Qh7Rl4qLmqiluxALlNvl+I+o5x38Pw3/z7mDPTPS1dQalZJXsmbvxx5gclhQ== + dependencies: + binary-search-bounds "^2.0.0" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -1355,6 +1395,13 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== +is-reference@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" + integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== + dependencies: + "@types/estree" "*" + is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" @@ -1500,6 +1547,13 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +magic-string@^0.25.7: + version "0.25.9" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" + integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== + dependencies: + sourcemap-codec "^1.4.8" + make-dir@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -1831,7 +1885,7 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve@^1.19.0, resolve@^1.22.0: +resolve@^1.17.0, resolve@^1.19.0, resolve@^1.22.0: version "1.22.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== @@ -1988,6 +2042,11 @@ source-map@~0.8.0-beta.0: dependencies: whatwg-url "^7.0.0" +sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" From 5aac7806a12468b45e89a6197e4ef7f8fde1c4e5 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 26 May 2022 09:02:45 -0700 Subject: [PATCH 02/36] update dependencies --- package.json | 4 +- yarn.lock | 415 ++++++++++++++++++++++++++------------------------- 2 files changed, 212 insertions(+), 207 deletions(-) diff --git a/package.json b/package.json index baeaeedf07..f9ce070949 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ }, "sideEffects": false, "devDependencies": { - "@rollup/plugin-commonjs": "^21.0.1", + "@rollup/plugin-commonjs": "22", "@rollup/plugin-json": "4", "@rollup/plugin-node-resolve": "13", "canvas": "2", @@ -43,7 +43,7 @@ "htl": "0.3", "js-beautify": "1", "jsdom": "19", - "mocha": "9", + "mocha": "10", "module-alias": "2", "rollup": "2", "rollup-plugin-terser": "7", diff --git a/yarn.lock b/yarn.lock index ccc377322a..9ea4ae7f41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,23 +15,23 @@ integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== "@babel/highlight@^7.16.7": - version "7.17.9" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.9.tgz#61b2ee7f32ea0454612def4fccdae0de232b73e3" - integrity sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.12.tgz#257de56ee5afbd20451ac0a75686b6b404257351" + integrity sha512-7yykMVF3hfZY2jsHZEEgLc+3x4o1O+fYyULu11GynEUQNwB6lua+IIQn1FiJxNucd5UlyJryrwsOh8PL9Sn8Qg== dependencies: "@babel/helper-validator-identifier" "^7.16.7" chalk "^2.0.0" js-tokens "^4.0.0" -"@eslint/eslintrc@^1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.3.tgz#fcaa2bcef39e13d6e9e7f6271f4cc7cae1174886" - integrity sha512-uGo44hIwoLGNyduRpjdEpovcbMdd+Nv7amtmJxnKmI8xj6yd5LncmSwDa5NgX/41lIFJtkjD6YdVfgEzPfJ5UA== +"@eslint/eslintrc@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.0.tgz#29f92c30bb3e771e4a2048c95fa6855392dfac4f" + integrity sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw== dependencies: ajv "^6.12.4" debug "^4.3.2" espree "^9.3.2" - globals "^13.9.0" + globals "^13.15.0" ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" @@ -67,10 +67,10 @@ semver "^7.3.5" tar "^6.1.11" -"@rollup/plugin-commonjs@^21.0.1": - version "21.0.2" - resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-21.0.2.tgz#0b9c539aa1837c94abfaf87945838b0fc8564891" - integrity sha512-d/OmjaLVO4j/aQX69bwpWPpbvI3TJkQuxoAk7BH8ew1PyoMBLTOuvJTjzG8oEoW7drIIqB0KCJtfFLu/2GClWg== +"@rollup/plugin-commonjs@22": + version "22.0.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-22.0.0.tgz#f4d87016e2fbf187a593ab9f46626fe05b59e8bd" + integrity sha512-Ktvf2j+bAO+30awhbYoCaXpBcyPmJbaEUYClQns/+6SNCYFURbvBiNbWgHITEsIgDDWCDUclWRKEuf8cwZCFoQ== dependencies: "@rollup/pluginutils" "^3.1.0" commondir "^1.0.1" @@ -124,9 +124,9 @@ integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== "@types/node@*": - version "17.0.31" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.31.tgz#a5bb84ecfa27eec5e1c802c6bbf8139bdb163a5d" - integrity sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q== + version "17.0.35" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.35.tgz#635b7586086d51fb40de0a2ec9d1014a5283ba4a" + integrity sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg== "@types/resolve@1.17.1": version "1.17.1" @@ -248,7 +248,7 @@ argparse@^2.0.1: asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== balanced-match@^1.0.0: version "1.0.2" @@ -273,6 +273,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -296,9 +303,9 @@ buffer-from@^1.0.0: integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== builtin-modules@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" - integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA== + version "3.3.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== callsites@^3.0.0: version "3.1.0" @@ -382,7 +389,7 @@ color-convert@^2.0.1: color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== color-name@~1.1.4: version "1.1.4" @@ -414,7 +421,7 @@ commander@^2.19.0, commander@^2.20.0: commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== concat-map@0.0.1: version "0.0.1" @@ -713,20 +720,13 @@ data-urls@^3.0.1: whatwg-mimetype "^3.0.0" whatwg-url "^11.0.0" -debug@4, debug@^4.1.1, debug@^4.3.2: +debug@4, debug@4.3.4, debug@^4.1.1, debug@^4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" -debug@4.3.3: - version "4.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== - dependencies: - ms "2.1.2" - decamelize@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" @@ -810,131 +810,131 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -esbuild-android-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.38.tgz#5b94a1306df31d55055f64a62ff6b763a47b7f64" - integrity sha512-aRFxR3scRKkbmNuGAK+Gee3+yFxkTJO/cx83Dkyzo4CnQl/2zVSurtG6+G86EQIZ+w+VYngVyK7P3HyTBKu3nw== - -esbuild-android-arm64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.38.tgz#78acc80773d16007de5219ccce544c036abd50b8" - integrity sha512-L2NgQRWuHFI89IIZIlpAcINy9FvBk6xFVZ7xGdOwIm8VyhX1vNCEqUJO3DPSSy945Gzdg98cxtNt8Grv1CsyhA== - -esbuild-darwin-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.38.tgz#e02b1291f629ebdc2aa46fabfacc9aa28ff6aa46" - integrity sha512-5JJvgXkX87Pd1Og0u/NJuO7TSqAikAcQQ74gyJ87bqWRVeouky84ICoV4sN6VV53aTW+NE87qLdGY4QA2S7KNA== - -esbuild-darwin-arm64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.38.tgz#01eb6650ec010b18c990e443a6abcca1d71290a9" - integrity sha512-eqF+OejMI3mC5Dlo9Kdq/Ilbki9sQBw3QlHW3wjLmsLh+quNfHmGMp3Ly1eWm981iGBMdbtSS9+LRvR2T8B3eQ== - -esbuild-freebsd-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.38.tgz#790b8786729d4aac7be17648f9ea8e0e16475b5e" - integrity sha512-epnPbhZUt93xV5cgeY36ZxPXDsQeO55DppzsIgWM8vgiG/Rz+qYDLmh5ts3e+Ln1wA9dQ+nZmVHw+RjaW3I5Ig== - -esbuild-freebsd-arm64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.38.tgz#b66340ab28c09c1098e6d9d8ff656db47d7211e6" - integrity sha512-/9icXUYJWherhk+y5fjPI5yNUdFPtXHQlwP7/K/zg8t8lQdHVj20SqU9/udQmeUo5pDFHMYzcEFfJqgOVeKNNQ== - -esbuild-linux-32@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.38.tgz#7927f950986fd39f0ff319e92839455912b67f70" - integrity sha512-QfgfeNHRFvr2XeHFzP8kOZVnal3QvST3A0cgq32ZrHjSMFTdgXhMhmWdKzRXP/PKcfv3e2OW9tT9PpcjNvaq6g== - -esbuild-linux-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.38.tgz#4893d07b229d9cfe34a2b3ce586399e73c3ac519" - integrity sha512-uuZHNmqcs+Bj1qiW9k/HZU3FtIHmYiuxZ/6Aa+/KHb/pFKr7R3aVqvxlAudYI9Fw3St0VCPfv7QBpUITSmBR1Q== - -esbuild-linux-arm64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.38.tgz#8442402e37d0b8ae946ac616784d9c1a2041056a" - integrity sha512-HlMGZTEsBrXrivr64eZ/EO0NQM8H8DuSENRok9d+Jtvq8hOLzrxfsAT9U94K3KOGk2XgCmkaI2KD8hX7F97lvA== - -esbuild-linux-arm@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.38.tgz#d5dbf32d38b7f79be0ec6b5fb2f9251fd9066986" - integrity sha512-FiFvQe8J3VKTDXG01JbvoVRXQ0x6UZwyrU4IaLBZeq39Bsbatd94Fuc3F1RGqPF5RbIWW7RvkVQjn79ejzysnA== - -esbuild-linux-mips64le@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.38.tgz#95081e42f698bbe35d8ccee0e3a237594b337eb5" - integrity sha512-qd1dLf2v7QBiI5wwfil9j0HG/5YMFBAmMVmdeokbNAMbcg49p25t6IlJFXAeLzogv1AvgaXRXvgFNhScYEUXGQ== - -esbuild-linux-ppc64le@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.38.tgz#dceb0a1b186f5df679618882a7990bd422089b47" - integrity sha512-mnbEm7o69gTl60jSuK+nn+pRsRHGtDPfzhrqEUXyCl7CTOCLtWN2bhK8bgsdp6J/2NyS/wHBjs1x8aBWwP2X9Q== - -esbuild-linux-riscv64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.38.tgz#61fb8edb75f475f9208c4a93ab2bfab63821afd2" - integrity sha512-+p6YKYbuV72uikChRk14FSyNJZ4WfYkffj6Af0/Tw63/6TJX6TnIKE+6D3xtEc7DeDth1fjUOEqm+ApKFXbbVQ== - -esbuild-linux-s390x@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.38.tgz#34c7126a4937406bf6a5e69100185fd702d12fe0" - integrity sha512-0zUsiDkGJiMHxBQ7JDU8jbaanUY975CdOW1YDrurjrM0vWHfjv9tLQsW9GSyEb/heSK1L5gaweRjzfUVBFoybQ== - -esbuild-netbsd-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.38.tgz#322ea9937d9e529183ee281c7996b93eb38a5d95" - integrity sha512-cljBAApVwkpnJZfnRVThpRBGzCi+a+V9Ofb1fVkKhtrPLDYlHLrSYGtmnoTVWDQdU516qYI8+wOgcGZ4XIZh0Q== - -esbuild-openbsd-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.38.tgz#1ca29bb7a2bf09592dcc26afdb45108f08a2cdbd" - integrity sha512-CDswYr2PWPGEPpLDUO50mL3WO/07EMjnZDNKpmaxUPsrW+kVM3LoAqr/CE8UbzugpEiflYqJsGPLirThRB18IQ== - -esbuild-sunos-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.38.tgz#c9446f7d8ebf45093e7bb0e7045506a88540019b" - integrity sha512-2mfIoYW58gKcC3bck0j7lD3RZkqYA7MmujFYmSn9l6TiIcAMpuEvqksO+ntBgbLep/eyjpgdplF7b+4T9VJGOA== - -esbuild-windows-32@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.38.tgz#f8e9b4602fd0ccbd48e5c8d117ec0ba4040f2ad1" - integrity sha512-L2BmEeFZATAvU+FJzJiRLFUP+d9RHN+QXpgaOrs2klshoAm1AE6Us4X6fS9k33Uy5SzScn2TpcgecbqJza1Hjw== - -esbuild-windows-64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.38.tgz#280f58e69f78535f470905ce3e43db1746518107" - integrity sha512-Khy4wVmebnzue8aeSXLC+6clo/hRYeNIm0DyikoEqX+3w3rcvrhzpoix0S+MF9vzh6JFskkIGD7Zx47ODJNyCw== - -esbuild-windows-arm64@0.14.38: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.38.tgz#d97e9ac0f95a4c236d9173fa9f86c983d6a53f54" - integrity sha512-k3FGCNmHBkqdJXuJszdWciAH77PukEyDsdIryEHn9cKLQFxzhT39dSumeTuggaQcXY57UlmLGIkklWZo2qzHpw== +esbuild-android-64@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.39.tgz#09f12a372eed9743fd77ff6d889ac14f7b340c21" + integrity sha512-EJOu04p9WgZk0UoKTqLId9VnIsotmI/Z98EXrKURGb3LPNunkeffqQIkjS2cAvidh+OK5uVrXaIP229zK6GvhQ== + +esbuild-android-arm64@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.39.tgz#f608d00ea03fe26f3b1ab92a30f99220390f3071" + integrity sha512-+twajJqO7n3MrCz9e+2lVOnFplRsaGRwsq1KL/uOy7xK7QdRSprRQcObGDeDZUZsacD5gUkk6OiHiYp6RzU3CA== + +esbuild-darwin-64@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.39.tgz#31528daa75b4c9317721ede344195163fae3e041" + integrity sha512-ImT6eUw3kcGcHoUxEcdBpi6LfTRWaV6+qf32iYYAfwOeV+XaQ/Xp5XQIBiijLeo+LpGci9M0FVec09nUw41a5g== + +esbuild-darwin-arm64@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.39.tgz#247f770d86d90a215fa194f24f90e30a0bd97245" + integrity sha512-/fcQ5UhE05OiT+bW5v7/up1bDsnvaRZPJxXwzXsMRrr7rZqPa85vayrD723oWMT64dhrgWeA3FIneF8yER0XTw== + +esbuild-freebsd-64@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.39.tgz#479414d294905055eb396ebe455ed42213284ee0" + integrity sha512-oMNH8lJI4wtgN5oxuFP7BQ22vgB/e3Tl5Woehcd6i2r6F3TszpCnNl8wo2d/KvyQ4zvLvCWAlRciumhQg88+kQ== + +esbuild-freebsd-arm64@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.39.tgz#cedeb10357c88533615921ae767a67dc870a474c" + integrity sha512-1GHK7kwk57ukY2yI4ILWKJXaxfr+8HcM/r/JKCGCPziIVlL+Wi7RbJ2OzMcTKZ1HpvEqCTBT/J6cO4ZEwW4Ypg== + +esbuild-linux-32@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.39.tgz#d9f008c4322d771f3958f59c1eee5a05cdf92485" + integrity sha512-g97Sbb6g4zfRLIxHgW2pc393DjnkTRMeq3N1rmjDUABxpx8SjocK4jLen+/mq55G46eE2TA0MkJ4R3SpKMu7dg== + +esbuild-linux-64@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.39.tgz#ba58d7f66858913aeb1ab5c6bde1bbd824731795" + integrity sha512-4tcgFDYWdI+UbNMGlua9u1Zhu0N5R6u9tl5WOM8aVnNX143JZoBZLpCuUr5lCKhnD0SCO+5gUyMfupGrHtfggQ== + +esbuild-linux-arm64@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.39.tgz#708785a30072702b5b1c16b65cf9c25c51202529" + integrity sha512-23pc8MlD2D6Px1mV8GMglZlKgwgNKAO8gsgsLLcXWSs9lQsCYkIlMo/2Ycfo5JrDIbLdwgP8D2vpfH2KcBqrDQ== + +esbuild-linux-arm@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.39.tgz#4e8b5deaa7ab60d0d28fab131244ef82b40684f4" + integrity sha512-t0Hn1kWVx5UpCzAJkKRfHeYOLyFnXwYynIkK54/h3tbMweGI7dj400D1k0Vvtj2u1P+JTRT9tx3AjtLEMmfVBQ== + +esbuild-linux-mips64le@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.39.tgz#6f3bf3023f711084e5a1e8190487d2020f39f0f7" + integrity sha512-epwlYgVdbmkuRr5n4es3B+yDI0I2e/nxhKejT9H0OLxFAlMkeQZxSpxATpDc9m8NqRci6Kwyb/SfmD1koG2Zuw== + +esbuild-linux-ppc64le@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.39.tgz#900e718a4ea3f6aedde8424828eeefdd4b48d4b9" + integrity sha512-W/5ezaq+rQiQBThIjLMNjsuhPHg+ApVAdTz2LvcuesZFMsJoQAW2hutoyg47XxpWi7aEjJGrkS26qCJKhRn3QQ== + +esbuild-linux-riscv64@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.39.tgz#dcbff622fa37047a75d2ff7a1d8d2949d80277e4" + integrity sha512-IS48xeokcCTKeQIOke2O0t9t14HPvwnZcy+5baG13Z1wxs9ZrC5ig5ypEQQh4QMKxURD5TpCLHw2W42CLuVZaA== + +esbuild-linux-s390x@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.39.tgz#3f725a7945b419406c99d93744b28552561dcdfd" + integrity sha512-zEfunpqR8sMomqXhNTFEKDs+ik7HC01m3M60MsEjZOqaywHu5e5682fMsqOlZbesEAAaO9aAtRBsU7CHnSZWyA== + +esbuild-netbsd-64@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.39.tgz#e10e40b6a765798b90d4eb85901cc85c8b7ff85e" + integrity sha512-Uo2suJBSIlrZCe4E0k75VDIFJWfZy+bOV6ih3T4MVMRJh1lHJ2UyGoaX4bOxomYN3t+IakHPyEoln1+qJ1qYaA== + +esbuild-openbsd-64@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.39.tgz#935ec143f75ce10bd9cdb1c87fee00287eb0edbc" + integrity sha512-secQU+EpgUPpYjJe3OecoeGKVvRMLeKUxSMGHnK+aK5uQM3n1FPXNJzyz1LHFOo0WOyw+uoCxBYdM4O10oaCAA== + +esbuild-sunos-64@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.39.tgz#0e7aa82b022a2e6d55b0646738b2582c2d72c3c0" + integrity sha512-qHq0t5gePEDm2nqZLb+35p/qkaXVS7oIe32R0ECh2HOdiXXkj/1uQI9IRogGqKkK+QjDG+DhwiUw7QoHur/Rwg== + +esbuild-windows-32@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.39.tgz#3f1538241f31b538545f4b5841b248cac260fa35" + integrity sha512-XPjwp2OgtEX0JnOlTgT6E5txbRp6Uw54Isorm3CwOtloJazeIWXuiwK0ONJBVb/CGbiCpS7iP2UahGgd2p1x+Q== + +esbuild-windows-64@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.39.tgz#b100c59f96d3c2da2e796e42fee4900d755d3e03" + integrity sha512-E2wm+5FwCcLpKsBHRw28bSYQw0Ikxb7zIMxw3OPAkiaQhLVr3dnVO8DofmbWhhf6b97bWzg37iSZ45ZDpLw7Ow== + +esbuild-windows-arm64@0.14.39: + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.39.tgz#00268517e665b33c89778d61f144e4256b39f631" + integrity sha512-sBZQz5D+Gd0EQ09tZRnz/PpVdLwvp/ufMtJ1iDFYddDaPpZXKqPyaxfYBLs3ueiaksQ26GGa7sci0OqFzNs7KA== esbuild@^0.14.27: - version "0.14.38" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.38.tgz#99526b778cd9f35532955e26e1709a16cca2fb30" - integrity sha512-12fzJ0fsm7gVZX1YQ1InkOE5f9Tl7cgf6JPYXRJtPIoE0zkWAbHdPHVPPaLi9tYAcEBqheGzqLn/3RdTOyBfcA== + version "0.14.39" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.39.tgz#c926b2259fe6f6d3a94f528fb42e103c5a6d909a" + integrity sha512-2kKujuzvRWYtwvNjYDY444LQIA3TyJhJIX3Yo4+qkFlDDtGlSicWgeHVJqMUP/2sSfH10PGwfsj+O2ro1m10xQ== optionalDependencies: - esbuild-android-64 "0.14.38" - esbuild-android-arm64 "0.14.38" - esbuild-darwin-64 "0.14.38" - esbuild-darwin-arm64 "0.14.38" - esbuild-freebsd-64 "0.14.38" - esbuild-freebsd-arm64 "0.14.38" - esbuild-linux-32 "0.14.38" - esbuild-linux-64 "0.14.38" - esbuild-linux-arm "0.14.38" - esbuild-linux-arm64 "0.14.38" - esbuild-linux-mips64le "0.14.38" - esbuild-linux-ppc64le "0.14.38" - esbuild-linux-riscv64 "0.14.38" - esbuild-linux-s390x "0.14.38" - esbuild-netbsd-64 "0.14.38" - esbuild-openbsd-64 "0.14.38" - esbuild-sunos-64 "0.14.38" - esbuild-windows-32 "0.14.38" - esbuild-windows-64 "0.14.38" - esbuild-windows-arm64 "0.14.38" + esbuild-android-64 "0.14.39" + esbuild-android-arm64 "0.14.39" + esbuild-darwin-64 "0.14.39" + esbuild-darwin-arm64 "0.14.39" + esbuild-freebsd-64 "0.14.39" + esbuild-freebsd-arm64 "0.14.39" + esbuild-linux-32 "0.14.39" + esbuild-linux-64 "0.14.39" + esbuild-linux-arm "0.14.39" + esbuild-linux-arm64 "0.14.39" + esbuild-linux-mips64le "0.14.39" + esbuild-linux-ppc64le "0.14.39" + esbuild-linux-riscv64 "0.14.39" + esbuild-linux-s390x "0.14.39" + esbuild-netbsd-64 "0.14.39" + esbuild-openbsd-64 "0.14.39" + esbuild-sunos-64 "0.14.39" + esbuild-windows-32 "0.14.39" + esbuild-windows-64 "0.14.39" + esbuild-windows-arm64 "0.14.39" escalade@^3.1.1: version "3.1.1" @@ -989,11 +989,11 @@ eslint-visitor-keys@^3.3.0: integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== eslint@8: - version "8.15.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.15.0.tgz#fea1d55a7062da48d82600d2e0974c55612a11e9" - integrity sha512-GG5USZ1jhCu8HJkzGgeK8/+RGnHaNYZGrGDzUtigK3BsGESW/rs2az23XqE0WVwDxy1VRvvjSSGu5nB0Bu+6SA== + version "8.16.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.16.0.tgz#6d936e2d524599f2a86c708483b4c372c5d3bbae" + integrity sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA== dependencies: - "@eslint/eslintrc" "^1.2.3" + "@eslint/eslintrc" "^1.3.0" "@humanwhocodes/config-array" "^0.9.2" ajv "^6.10.0" chalk "^4.0.0" @@ -1011,7 +1011,7 @@ eslint@8: file-entry-cache "^6.0.1" functional-red-black-tree "^1.0.1" glob-parent "^6.0.1" - globals "^13.6.0" + globals "^13.15.0" ignore "^5.2.0" import-fresh "^3.0.0" imurmurhash "^0.1.4" @@ -1202,7 +1202,7 @@ glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob@7.2.0, glob@^7.1.3, glob@^7.1.6: +glob@7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -1214,17 +1214,24 @@ glob@7.2.0, glob@^7.1.3, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -globals@^13.6.0, globals@^13.9.0: - version "13.13.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.13.0.tgz#ac32261060d8070e2719dd6998406e27d2b5727b" - integrity sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A== +glob@^7.1.3, glob@^7.1.6: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: - type-fest "^0.20.2" + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" -growl@1.10.5: - version "1.10.5" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" - integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== +globals@^13.15.0: + version "13.15.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.15.0.tgz#38113218c907d2f7e98658af246cef8b77e90bac" + integrity sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog== + dependencies: + type-fest "^0.20.2" has-flag@^3.0.0: version "3.0.0" @@ -1583,14 +1590,14 @@ mimic-response@^2.0.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== -minimatch@4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" - integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== dependencies: - brace-expansion "^1.1.7" + brace-expansion "^2.0.1" -minimatch@^3.0.4, minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -1617,32 +1624,30 @@ mkdirp@^1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mocha@9: - version "9.2.2" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9" - integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== +mocha@10: + version "10.0.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.0.0.tgz#205447d8993ec755335c4b13deba3d3a13c4def9" + integrity sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA== dependencies: "@ungap/promise-all-settled" "1.1.2" ansi-colors "4.1.1" browser-stdout "1.3.1" chokidar "3.5.3" - debug "4.3.3" + debug "4.3.4" diff "5.0.0" escape-string-regexp "4.0.0" find-up "5.0.0" glob "7.2.0" - growl "1.10.5" he "1.2.0" js-yaml "4.1.0" log-symbols "4.1.0" - minimatch "4.2.1" + minimatch "5.0.1" ms "2.1.3" - nanoid "3.3.1" + nanoid "3.3.3" serialize-javascript "6.0.0" strip-json-comments "3.1.1" supports-color "8.1.1" - which "2.0.2" - workerpool "6.2.0" + workerpool "6.2.1" yargs "16.2.0" yargs-parser "20.2.4" yargs-unparser "2.0.0" @@ -1663,16 +1668,16 @@ ms@2.1.3: integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== nan@^2.15.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" - integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== + version "2.16.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916" + integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA== -nanoid@3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" - integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== +nanoid@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== -nanoid@^3.3.3: +nanoid@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== @@ -1809,11 +1814,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2: integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== postcss@^8.4.13: - version "8.4.13" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.13.tgz#7c87bc268e79f7f86524235821dfdf9f73e5d575" - integrity sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA== + version "8.4.14" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" + integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig== dependencies: - nanoid "^3.3.3" + nanoid "^3.3.4" picocolors "^1.0.0" source-map-js "^1.0.2" @@ -1917,9 +1922,9 @@ rollup-plugin-terser@7: terser "^5.0.0" rollup@2, rollup@^2.59.0: - version "2.72.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.72.0.tgz#f94280b003bcf9f2f1f2594059a9db5abced371e" - integrity sha512-KqtR2YcO35/KKijg4nx4STO3569aqCUeGRkKWnJ6r+AvBBrVY9L4pmf4NHVrQr4mTOq6msbohflxr2kpihhaOA== + version "2.74.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.74.1.tgz#4fba0ff1c312cc4ee82691b154eee69a0d01929f" + integrity sha512-K2zW7kV8Voua5eGkbnBtWYfMIhYhT9Pel2uhBk2WO5eMee161nPze/XRfvEQPFYz7KgrCCnmh2Wy0AMFLGGmMA== optionalDependencies: fsevents "~2.3.2" @@ -2210,9 +2215,9 @@ v8-compile-cache@^2.0.3: integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== vite@2: - version "2.9.8" - resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.8.tgz#2c2cb0790beb0fbe4b8c0995b80fe691a91c2545" - integrity sha512-zsBGwn5UT3YS0NLSJ7hnR54+vUKfgzMUh/Z9CxF1YKEBVIe213+63jrFLmZphgGI5zXwQCSmqIdbPuE8NJywPw== + version "2.9.9" + resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.9.tgz#8b558987db5e60fedec2f4b003b73164cb081c5e" + integrity sha512-ffaam+NgHfbEmfw/Vuh6BHKKlI/XIAhxE5QSS7gFLIngxg171mg1P3a4LSRME0z2ZU1ScxoKzphkipcYwSD5Ew== dependencies: esbuild "^0.14.27" postcss "^8.4.13" @@ -2295,7 +2300,7 @@ whatwg-url@^7.0.0: tr46 "^1.0.1" webidl-conversions "^4.0.2" -which@2.0.2, which@^2.0.1: +which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== @@ -2314,10 +2319,10 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -workerpool@6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" - integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== wrap-ansi@^7.0.0: version "7.0.0" From 86089810d5a00a11e4c25e2ab7a37354bf8cd7c1 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 26 May 2022 09:05:57 -0700 Subject: [PATCH 03/36] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c496cbc527..0731e3bf48 100644 --- a/README.md +++ b/README.md @@ -1043,7 +1043,7 @@ The hexgrid mark can be used to support marks using the [hexbin](#hexbin) layout #### Plot.hexgrid([*options*]) -The *binWidth* option specifies the distance between the centers of neighboring hexagons, in pixels (defaults to 20). The *clip* option defaults to true, clipping the mark to the frame’s dimensions. +The **binWidth** option specifies the distance between the centers of neighboring hexagons, in pixels (defaults to 20). The **clip** option defaults to true, clipping the mark to the frame’s dimensions. ### Image From 6db2ebde34d37e811d2aaa56c8a3d06c3a8869c1 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 26 May 2022 09:20:19 -0700 Subject: [PATCH 04/36] Update README --- README.md | 47 +++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 0731e3bf48..6e7fb49c5d 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Renders a new plot given the specified *options* and returns the corresponding S ### Mark options -The **marks** option specifies an array of [marks](#marks) to render. Each mark has its own data and options; see the respective mark type (*e.g.*, [bar](#bar) or [dot](#dot)) for which mark options are supported. Each mark may be a nested array of marks, allowing composition. Marks may also be a function which returns an SVG element, if you wish to insert some arbitrary content into your plot. And marks may be null or undefined, which produce no output; this is useful for showing marks conditionally (*e.g.*, when a box is checked). Marks are drawn in *z*-order, last on top. For example, here a single rule at *y* = 0 is drawn on top of blue bars for the [*alphabet* dataset](./test/data/alphabet.csv). +The **marks** option specifies an array of [marks](#marks) to render. Each mark has its own data and options; see the respective mark type (*e.g.*, [bar](#bar) or [dot](#dot)) for which mark options are supported. Each mark may be a nested array of marks, allowing composition. Marks may also be a function which returns an SVG element, if you wish to insert some arbitrary content into your plot. And marks may be null or undefined, which produce no output; this is useful for showing marks conditionally (*e.g.*, when a box is checked). Marks are drawn in *z* order, last on top. For example, here a single rule at *y* = 0 is drawn on top of blue bars for the [*alphabet* dataset](./test/data/alphabet.csv). ```js Plot.plot({ @@ -263,7 +263,7 @@ Similarly, the *y* and *fy* scales support asymmetric insets with: The inset scale options can provide “breathing room” to separate marks from axes or the plot’s edge. For example, in a scatterplot with a Plot.dot with the default 3-pixel radius and 1.5-pixel stroke width, an inset of 5 pixels prevents dots from overlapping with the axes. The *scale*.round option is useful for crisp edges by rounding to the nearest pixel boundary. -In addition to the generic *ordinal* scale type, which requires an explicit output range value for each input domain value, Plot supports special *point* and *band* scale types for encoding ordinal data as position. These scale types accept a [*min*, *max*] range similar to quantitative scales, and divide this continuous interval into discrete points or bands based on the number of distinct values in the domain (*i.e.*, the domain’s cardinality). If the associated marks have no effective width along the ordinal dimension—such as a dot, rule, or tick—then use a *point* scale; otherwise, say for a bar, use a *band* scale. In the image below, the top *x*-scale is a *point* scale while the bottom *x*-scale is a *band* scale; see [Plot: Scales](https://observablehq.com/@observablehq/plot-scales) for an interactive version. +In addition to the generic *ordinal* scale type, which requires an explicit output range value for each input domain value, Plot supports special *point* and *band* scale types for encoding ordinal data as position. These scale types accept a [*min*, *max*] range similar to quantitative scales, and divide this continuous interval into discrete points or bands based on the number of distinct values in the domain (*i.e.*, the domain’s cardinality). If the associated marks have no effective width along the ordinal dimension—such as a dot, rule, or tick—then use a *point* scale; otherwise, say for a bar, use a *band* scale. In the image below, the top *x* scale is a *point* scale while the bottom *x* scale is a *band* scale; see [Plot: Scales](https://observablehq.com/@observablehq/plot-scales) for an interactive version. point and band scales @@ -717,8 +717,8 @@ The rectangular marks ([bar](#bar), [cell](#cell), and [rect](#rect)) support in * **insetRight** - inset the right edge * **insetBottom** - inset the bottom edge * **insetLeft** - inset the left edge -* **rx** - the [*x*-radius](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/rx) for rounded corners -* **ry** - the [*y*-radius](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/ry) for rounded corners +* **rx** - the [*x* radius](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/rx) for rounded corners +* **ry** - the [*y* radius](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/ry) for rounded corners Insets are specified in pixels. Corner radii are specified in either pixels or percentages (strings). Both default to zero. Insets are typically used to ensure a one-pixel gap between adjacent bars; note that the [bin transform](#bin) provides default insets, and that the [band scale padding](#position-options) defaults to 0.1, which also provides separation. @@ -1441,7 +1441,7 @@ The **filter** transform is similar to filtering the data with [*array*.filter]( Plot.barY(alphabet.filter(d => /[aeiou]/i.test(d.letter)), {x: "letter", y: "frequency"}) ``` -Together the **sort** and **reverse** transforms allow control over *z*-order, which can be important when addressing overplotting. If the sort option is a function but does not take exactly one argument, it is assumed to be a [comparator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#description); otherwise, the sort option is interpreted as a channel value definition and thus may be either as a column name, accessor function, or array of values. +Together the **sort** and **reverse** transforms allow control over *z* order, which can be important when addressing overplotting. If the sort option is a function but does not take exactly one argument, it is assumed to be a [comparator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#description); otherwise, the sort option is interpreted as a channel value definition and thus may be either as a column name, accessor function, or array of values. For greater control, you can also implement a [custom transform function](#custom-transforms): @@ -1451,7 +1451,7 @@ The basic transforms are composable: the *filter* transform is applied first, th Plot’s option transforms, listed below, do more than populate the **transform** function: they derive new mark options and channels. These transforms take a mark’s *options* object (and possibly transform-specific options as the first argument) and return a new, transformed, *options*. Option transforms are composable: you can pass an *options* objects through more than one transform before passing it to a mark. You can also reuse the same transformed *options* on multiple marks. -The *filter*, *sort* and *reverse* transforms are also available as functions, allowing the order of operations to be specified explicitly. For example, sorting before binning results in sorted data inside bins, whereas sorting after binning results affects the *z*-order of rendered marks. +The *filter*, *sort* and *reverse* transforms are also available as functions, allowing the order of operations to be specified explicitly. For example, sorting before binning results in sorted data inside bins, whereas sorting after binning results affects the *z* order of rendered marks. #### Plot.sort(*order*, *options*) @@ -1532,12 +1532,12 @@ The following aggregation methods are supported: * *pXX* - the percentile value, where XX is a number in [00,99] * *deviation* - the standard deviation * *variance* - the variance per [Welford’s algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) -* *x* - the middle of the bin’s *x*-extent (when binning on *x*) -* *x1* - the lower bound of the bin’s *x*-extent (when binning on *x*) -* *x2* - the upper bound of the bin’s *x*-extent (when binning on *x*) -* *y* - the middle of the bin’s *y*-extent (when binning on *y*) -* *y1* - the lower bound of the bin’s *y*-extent (when binning on *y*) -* *y2* - the upper bound of the bin’s *y*-extent (when binning on *y*) +* *x* - the middle of the bin’s *x* extent (when binning on *x*) +* *x1* - the lower bound of the bin’s *x* extent (when binning on *x*) +* *x2* - the upper bound of the bin’s *x* extent (when binning on *x*) +* *y* - the middle of the bin’s *y* extent (when binning on *y*) +* *y1* - the lower bound of the bin’s *y* extent (when binning on *y*) +* *y2* - the upper bound of the bin’s *y* extent (when binning on *y*) * a function to be passed the array of values for each bin and the extent of the bin * an object with a *reduce* method, and optionally a *scope* @@ -1952,7 +1952,7 @@ The following **order** methods are supported: - a named field or function of data - order data by priority - an array of *z* values -The **reverse** option reverses the effective order. For the *value* order, Plot.stackY uses the *y*-value while Plot.stackX uses the *x*-value. For the *appearance* order, Plot.stackY uses the *x*-position of the maximum *y*-value while Plot.stackX uses the *y*-position of the maximum *x*-value. If an array of *z* values are specified, they should enumerate the *z* values for all series in the desired order; this array is typically hard-coded or computed with [d3.groupSort](https://github.com/d3/d3-array/blob/main/README.md#groupSort). Note that the input order (null) and *value* order can produce crossing paths: they do not guarantee a consistent series order across stacks. +The **reverse** option reverses the effective order. For the *value* order, Plot.stackY uses the *y* value while Plot.stackX uses the *x* value. For the *appearance* order, Plot.stackY uses the *x* position of the maximum *y* value while Plot.stackX uses the *y* position of the maximum *x* value. If an array of *z* values are specified, they should enumerate the *z* values for all series in the desired order; this array is typically hard-coded or computed with [d3.groupSort](https://github.com/d3/d3-array/blob/main/README.md#groupSort). Note that the input order (null) and *value* order can produce crossing paths: they do not guarantee a consistent series order across stacks. The stack transform supports diverging stacks: negative values are stacked below zero while positive values are stacked above zero. For Plot.stackY, the **y1** channel contains the value of lesser magnitude (closer to zero) while the **y2** channel contains the value of greater magnitude (farther from zero); the difference between the two corresponds to the input **y** channel value. For Plot.stackX, the same is true, except for **x1**, **x2**, and **x** respectively. @@ -2155,18 +2155,24 @@ Plot.column is typically used by options transforms to define new channels; the ## Scale-aware transforms -Some transforms need to operate in representation space (such as pixels and colors, *i.e.* after scales have been applied) rather than data space. Such a transform might, for example, modify the marks’ positions in screen space to avoid occlusion. These scale-aware transforms are applied *after* the initial setting of the scales, and can modify the channels or derive new channels—which can in turn be passed to scales. +Some transforms need to operate in visual representation space such as pixel coordinates and colors rather than abstract data space. Such a transform might, for example, modify the marks’ positions to avoid occlusion. These scale-aware transforms are applied *after* the initial scales are constructed and can modify the channels or derive new channels; these in turn may (or may not, as desired) be passed to scales. ### Dodge -The dodge transform can be applied to any mark that consumes *x* or *y*, such as the Dot, Image, Text and Vector marks. +The dodge transform can be applied to any mark that consumes *x* or *y* channels, such as the [dot](#dot), [image](#image), [text](#text), and [vector](#vector) marks. The dodge transforms accept the following options: + +* **padding** — a number of pixels added to the radius of the mark to estimate its size +* **anchor** - the dodge anchor; defaults to *left* for dodgeX, or *bottom* for dodgeY + +The **anchor** option may one of *middle*, *right*, and *left* for dodgeX, and one of *middle*, *top*, and *bottom* for dodgeY. With the *middle* anchor the piles will grow from the center in both directions; with the other anchors, the piles will grow from the specified anchor towards the opposite direction. + #### Plot.dodgeY([*layoutOptions*, ]*options*) ```js Plot.dodgeY({x: "date"}) ``` -If the marks are arranged along the *x* axis, the dodgeY transform piles them vertically, keeping their *x* position unchanged, and creating a *y* position that avoids overlapping. +Given marks arranged along the *x* axis, the dodgeY transform piles them vertically by defining a *y* position channel that avoids overlapping. The *x* position channel is unchanged. #### Plot.dodgeX([*layoutOptions*, ]*options*) @@ -2174,18 +2180,15 @@ If the marks are arranged along the *x* axis, the dodgeY transform piles them ve Plot.dodgeX({y: "value"}) ``` -Equivalent to Plot.dodgeY, but the piling is horizontal, keeping the marks’ *y* position unchanged, and creating an *x* position that avoids overlapping. -The dodge transforms accept the following options: -* **padding** — a number of pixels added to the radius of the mark to estimate its size -* **anchor** - the frame anchor: one of *middle*, *right*, and *left* (default) for dodgeX, and one of *middle*, *top*, and *bottom* (default) for dodgeY. With the *middle* anchor the piles will grow from the center in both directions; with the other anchors, the piles will grow from the specified anchor towards the opposite direction. +Equivalent to Plot.dodgeY, but piling horizontally, creating a new *x* position channel that avoids overlapping. The *y* position channel is unchanged. ### Hexbin -The hexbin transform can be applied to any mark that consumes *x* and *y*, such as the Dot, Image, Text and Vector marks. It aggregates the values into hexagonal bins of the given *radius* (in pixel space), and computes new values *x* and *y* as the centers of each bin. It can also return new channels by applying a reducer to each bin, such as the number of elements in the bin. +The hexbin transform can be applied to any mark that consumes *x* and *y*, such as the [dot](#dot), [image](#image), [text](#text), and [vector](#vector) marks. It aggregates values into hexagonal bins of the given **binWidth** (in pixels) and computes new position channels *x* and *y* as the centers of each bin. It can also create new channels by applying a specified reducer to each bin, such as the *count* of elements in the bin. #### Plot.hexbin(*outputs*, *options*) -[Source](./src/transforms/hexbin.js) · [Examples](https://observablehq.com/@observablehq/plot-hexbin) · Aggregates the given inputs into hexagonal bins, and creates output channels with the reduced data. The options must specify the *x* and *y* channels, and can optionally indicate the *binWidth* in pixels (defaults to 20), defined as the distance between the centers of two neighboring hexagons. If any of **z**, **fill**, or **stroke** is a channel, the first of these channels will be used to subdivide bins. The *outputs* options are similar to Plot.bin’s outputs; each output channel receives as input, for each hexagon, the subset of the data which has been matched to its center. The outputs object specifies the aggregation method for each output channel. +[Source](./src/transforms/hexbin.js) · [Examples](https://observablehq.com/@observablehq/plot-hexbin) · Aggregates the given input channels into hexagonal bins, creating output channels with the reduced data. The *options* must specify the **x** and **y** channels and can optionally indicate the **binWidth** (defaults to 20), defined as the distance between the centers of two neighboring hexagons in pixels. If any of **z**, **fill**, or **stroke** is a channel, the first of these channels will be used to subdivide bins. The *outputs* options are similar to the [bin transform](#bin); each output channel receives as input, for each hexagon, the subset of the data which has been matched to its center. The outputs object specifies the aggregation method for each output channel. The following aggregation methods are supported: From 5460c581282bc7d4735fc269757bf38ac3d92a2c Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 26 May 2022 09:39:01 -0700 Subject: [PATCH 05/36] tweak error message --- src/transforms/basic.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/basic.js b/src/transforms/basic.js index cea9ae9dfc..e3271e0cce 100644 --- a/src/transforms/basic.js +++ b/src/transforms/basic.js @@ -17,7 +17,7 @@ export function basic({ if (s1 != null && !isOptions(s1)) t1 = composeTransform(t1, sortTransform(s1)); if (r1) t1 = composeTransform(t1, reverseTransform); } - if (t2 != null && i1 != null) throw new Error("Data transforms must appear before any channel transform"); + if (t2 != null && i1 != null) throw new Error("transforms cannot be applied after initializers"); return { ...options, ...isOptions(s1) && {sort: s1}, From 027cdebcf505824c36de13f1abb6ae1ba6ea888c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 27 May 2022 22:11:43 +0200 Subject: [PATCH 06/36] Fix Plot.hexbin default reducer, and simplify (#884) --- src/transforms/hexbin.js | 46 ++- test/output/hexbinZ.html | 282 +++++++++++++ test/output/{hexbinZ.svg => hexbinZNull.svg} | 410 +++++++++---------- test/plots/hexbin-z-null.js | 23 ++ test/plots/hexbin-z.js | 18 +- test/plots/index.js | 1 + 6 files changed, 530 insertions(+), 250 deletions(-) create mode 100644 test/output/hexbinZ.html rename test/output/{hexbinZ.svg => hexbinZNull.svg} (57%) create mode 100644 test/plots/hexbin-z-null.js diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js index e3ca03b827..acea8e2988 100644 --- a/src/transforms/hexbin.js +++ b/src/transforms/hexbin.js @@ -1,7 +1,6 @@ -import {group} from "d3"; import {sqrt3} from "../symbols.js"; -import {identity, maybeColumn, maybeColorChannel, valueof} from "../options.js"; -import {hasOutput, maybeOutputs} from "./group.js"; +import {identity, maybeColorChannel, valueof} from "../options.js"; +import {hasOutput, maybeGroup, maybeOutputs, maybeSubgroup} from "./group.js"; import {initialize} from "./initialize.js"; // We don’t want the hexagons to align with the edges of the plot frame, as that @@ -18,45 +17,45 @@ export function hexbin(outputs = {fill: "count"}, options = {}) { // TODO filter e.g. to show empty hexbins? // TODO disallow x, x1, x2, y, y1, y2 reducers? -function hexbinn(outputs, {binWidth = 20, fill, stroke, z, ...options}) { +function hexbinn(outputs, {binWidth = 20, z, fill, stroke, ...options}) { binWidth = +binWidth; - const [GZ, setGZ] = maybeColumn(z); const [vfill] = maybeColorChannel(fill); const [vstroke] = maybeColorChannel(stroke); - const [GF = fill, setGF] = maybeColumn(vfill); - const [GS = stroke, setGS] = maybeColumn(vstroke); - outputs = maybeOutputs({ - ...setGF && {fill: "first"}, - ...setGS && {stroke: "first"}, - ...outputs - }, {fill, stroke, ...options}); + outputs = maybeOutputs(outputs, {z, fill, stroke, ...options}); return { symbol: "hexagon", ...!hasOutput(outputs, "r") && {r: binWidth / 2}, - ...!setGF && {fill}, - ...((hasOutput(outputs, "fill") || setGF) && stroke === undefined) ? {stroke: "none"} : {stroke}, + ...!hasOutput(outputs, "fill") && {fill}, + ...((hasOutput(outputs, "fill") || vstroke != null) && stroke === undefined) ? {stroke: "none"} : {stroke}, ...initialize(options, function(data, facets, {x: X, y: Y}, scales) { - if (setGF) setGF(valueof(data, vfill)); - if (setGS) setGS(valueof(data, vstroke)); - if (setGZ) setGZ(valueof(data, z)); - for (const o of outputs) o.initialize(data); if (X === undefined) throw new Error("missing channel: x"); if (Y === undefined) throw new Error("missing channel: y"); const x = X.scale !== undefined ? scales[X.scale] : identity.transform; const y = Y.scale !== undefined ? scales[Y.scale] : identity.transform; X = X.value.map(x); Y = Y.value.map(y); - const F = setGF && GF.transform(); - const S = setGS && GS.transform(); - const Z = setGZ ? GZ.transform() : (F || S); + const Z = valueof(data, z); + const F = valueof(data, vfill); + const S = valueof(data, vstroke); + const G = maybeSubgroup(outputs, Z, F, S); + if (Z && !outputs.find(r => r.name === "z")) { + outputs.push(...maybeOutputs({z: "first"}, {z: Z})); + } + if (F && !outputs.find(r => r.name === "fill")) { + outputs.push(...maybeOutputs({fill: "first"}, {fill: F})); + } + if (S && !outputs.find(r => r.name === "stroke")) { + outputs.push(...maybeOutputs({stroke: "first"}, {stroke: S})); + } const binFacets = []; const BX = []; const BY = []; let i = -1; + for (const o of outputs) o.initialize(data); for (const facet of facets) { const binFacet = []; for (const o of outputs) o.scope("facet", facet); - for (const index of Z ? group(facet, i => Z[i]).values() : [facet]) { + for (const [, index] of maybeGroup(facet, G)) { for (const bin of hbin(index, X, Y, binWidth)) { binFacet.push(++i); BX.push(bin.x); @@ -69,6 +68,9 @@ function hexbinn(outputs, {binWidth = 20, fill, stroke, z, ...options}) { const channels = { x: {value: BX}, y: {value: BY}, + ...Z && {z: {value: Z}}, + ...F && {fill: {value: F, scale: true}}, + ...S && {stroke: {value: S, scale: true}}, ...Object.fromEntries(outputs.map(({name, output}) => [name, {scale: true, binWidth: name === "r" ? binWidth : undefined, value: output.transform()}])) }; if ("r" in channels) { diff --git a/test/output/hexbinZ.html b/test/output/hexbinZ.html new file mode 100644 index 0000000000..198c51cd95 --- /dev/null +++ b/test/output/hexbinZ.html @@ -0,0 +1,282 @@ +
+
+ AdelieChinstrapGentoo +
+ + + + 3,000 + + + 3,500 + + + 4,000 + + + 4,500 + + + 5,000 + + + 5,500 + + + 6,000 + ↑ body_mass_g + + + + 35 + + + 40 + + + 45 + + + 50 + + + 55 + culmen_length_mm → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/test/output/hexbinZ.svg b/test/output/hexbinZNull.svg similarity index 57% rename from test/output/hexbinZ.svg rename to test/output/hexbinZNull.svg index d2de751545..adbcdef875 100644 --- a/test/output/hexbinZ.svg +++ b/test/output/hexbinZNull.svg @@ -87,224 +87,196 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/hexbin-z-null.js b/test/plots/hexbin-z-null.js new file mode 100644 index 0000000000..3680146c8a --- /dev/null +++ b/test/plots/hexbin-z-null.js @@ -0,0 +1,23 @@ +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({ + x: {inset: 10}, + y: {inset: 10}, + marks: [ + Plot.frame(), + Plot.hexgrid(), + Plot.dot(penguins, Plot.hexbin({r: "count"}, { + x: "culmen_depth_mm", + y: "culmen_length_mm", + stroke: "species", + fill: "island", + z: null, + fillOpacity: 0.5, + symbol: "dot" + })) + ] + }); +} diff --git a/test/plots/hexbin-z.js b/test/plots/hexbin-z.js index 0ac30caff3..e6adc73976 100644 --- a/test/plots/hexbin-z.js +++ b/test/plots/hexbin-z.js @@ -9,14 +9,14 @@ export default async function() { marks: [ Plot.frame(), Plot.hexgrid(), - Plot.dot(penguins, Plot.hexbin({r: "count"}, { - x: "culmen_depth_mm", - y: "culmen_length_mm", - strokeWidth: 2, - stroke: "sex", - fill: "sex", - fillOpacity: 0.5 - })) - ] + Plot.dot( + penguins, + Plot.hexbin( + { r: "count" }, + { x: "culmen_length_mm", y: "body_mass_g", stroke: "species", strokeOpacity: 0.8 } + ) + ) + ], + color: {legend: true} }); } diff --git a/test/plots/index.js b/test/plots/index.js index 12f9611a39..1d49ca4d0f 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -75,6 +75,7 @@ export {default as hexbinR} from "./hexbin-r.js"; export {default as hexbinSymbol} from "./hexbin-symbol.js"; export {default as hexbinText} from "./hexbin-text.js"; export {default as hexbinZ} from "./hexbin-z.js"; +export {default as hexbinZNull} from "./hexbin-z-null.js"; export {default as highCardinalityOrdinal} from "./high-cardinality-ordinal.js"; export {default as identityScale} from "./identity-scale.js"; export {default as industryUnemployment} from "./industry-unemployment.js"; From e362ff38eab0f5fb93fce051da03b6e8c9feac31 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 13:14:00 -0700 Subject: [PATCH 07/36] sort tests --- test/plots/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/plots/index.js b/test/plots/index.js index 1d49ca4d0f..84e5dc44a3 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -11,8 +11,8 @@ export {default as athletesBinsColors} from "./athletes-bins-colors.js"; export {default as athletesHeightWeight} from "./athletes-height-weight.js"; export {default as athletesHeightWeightBin} from "./athletes-height-weight-bin.js"; export {default as athletesHeightWeightBinStroke} from "./athletes-height-weight-bin-stroke.js"; -export {default as athletesHeightWeightSport} from "./athletes-height-weight-sport.js"; export {default as athletesHeightWeightSex} from "./athletes-height-weight-sex.js"; +export {default as athletesHeightWeightSport} from "./athletes-height-weight-sport.js"; export {default as athletesNationality} from "./athletes-nationality.js"; export {default as athletesSample} from "./athletes-sample.js"; export {default as athletesSexWeight} from "./athletes-sex-weight.js"; @@ -32,12 +32,12 @@ export {default as carsJiggle} from "./cars-jiggle.js"; export {default as carsMpg} from "./cars-mpg.js"; export {default as carsParcoords} from "./cars-parcoords.js"; export {default as clamp} from "./clamp.js"; +export {default as collapsedHistogram} from "./collapsed-histogram.js"; +export {default as covidIhmeProjectedDeaths} from "./covid-ihme-projected-deaths.js"; 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 collapsedHistogram} from "./collapsed-histogram.js"; -export {default as covidIhmeProjectedDeaths} from "./covid-ihme-projected-deaths.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 156c2a2124bb5e874cf0dc6e46385c44f6302a50 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 13:45:54 -0700 Subject: [PATCH 08/36] revert inlined hexbin implementation --- package.json | 1 + src/transforms/hexbin.js | 76 ++-- test/output/hexbin.svg | 382 ++++++++-------- test/output/hexbinOranges.svg | 178 ++++---- test/output/hexbinR.html | 279 ++++++------ test/output/hexbinSymbol.html | 176 ++++---- test/output/hexbinText.svg | 226 +++++----- test/output/hexbinZ.html | 365 ++++++++-------- test/output/hexbinZNull.svg | 379 ++++++++-------- test/output/penguinDodgeHexbin.svg | 680 ++++++++++++++--------------- yarn.lock | 5 + 11 files changed, 1384 insertions(+), 1363 deletions(-) diff --git a/package.json b/package.json index f9ce070949..ff5390901b 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ }, "dependencies": { "d3": "^7.3.0", + "d3-hexbin": "^0.2.2", "interval-tree-1d": "1", "isoformat": "0.2" }, diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js index acea8e2988..fe93ea1b38 100644 --- a/src/transforms/hexbin.js +++ b/src/transforms/hexbin.js @@ -1,4 +1,6 @@ -import {sqrt3} from "../symbols.js"; +import {hexbin as Hexbin} from "d3-hexbin"; // TODO inline +// import {sqrt3} from "../symbols.js"; +import {sqrt4_3} from "../symbols.js"; import {identity, maybeColorChannel, valueof} from "../options.js"; import {hasOutput, maybeGroup, maybeOutputs, maybeSubgroup} from "./group.js"; import {initialize} from "./initialize.js"; @@ -32,8 +34,11 @@ function hexbinn(outputs, {binWidth = 20, z, fill, stroke, ...options}) { if (Y === undefined) throw new Error("missing channel: y"); const x = X.scale !== undefined ? scales[X.scale] : identity.transform; const y = Y.scale !== undefined ? scales[Y.scale] : identity.transform; - X = X.value.map(x); - Y = Y.value.map(y); + X = X.value; + Y = Y.value; + // X = X.value.map(x); // TODO What if value is e.g. Uint32Array? + // Y = Y.value.map(y); // TODO What if value is e.g. Uint32Array? + const binsof = Hexbin().x(i => x(X[i]) - ox).y(i => y(Y[i]) - oy).radius(binWidth / 2 * sqrt4_3); const Z = valueof(data, z); const F = valueof(data, vfill); const S = valueof(data, vstroke); @@ -56,10 +61,11 @@ function hexbinn(outputs, {binWidth = 20, z, fill, stroke, ...options}) { const binFacet = []; for (const o of outputs) o.scope("facet", facet); for (const [, index] of maybeGroup(facet, G)) { - for (const bin of hbin(index, X, Y, binWidth)) { + // for (const bin of hbin(index, X, Y, binWidth)) { + for (const bin of binsof(index)) { binFacet.push(++i); - BX.push(bin.x); - BY.push(bin.y); + BX.push(bin.x + ox); + BY.push(bin.y + oy); for (const o of outputs) o.reduce(bin); } } @@ -82,32 +88,32 @@ function hexbinn(outputs, {binWidth = 20, z, fill, stroke, ...options}) { }; } -function hbin(I, X, Y, dx) { - const dy = dx * sqrt3 / 2; - const bins = new Map(); - for (const i of I) { - let px = X[i] / dx; - let py = Y[i] / dy; - if (isNaN(px) || isNaN(py)) continue; - let pj = Math.round(py), - pi = Math.round(px = px - (pj & 1) / 2), - py1 = py - pj; - if (Math.abs(py1) * 3 > 1) { - let px1 = px - pi, - pi2 = pi + (px < pi ? -1 : 1) / 2, - pj2 = pj + (py < pj ? -1 : 1), - px2 = px - pi2, - py2 = py - pj2; - if (px1 * px1 + py1 * py1 > px2 * px2 + py2 * py2) pi = pi2 + (pj & 1 ? 1 : -1) / 2, pj = pj2; - } - const key = `${pi},${pj}`; - let g = bins.get(key); - if (g === undefined) { - bins.set(key, g = []); - g.x = (pi + (pj & 1) / 2) * dx; - g.y = pj * dy; - } - g.push(i); - } - return bins.values(); -} +// function hbin(I, X, Y, dx) { +// const dy = dx * sqrt3 / 2; +// const bins = new Map(); +// for (const i of I) { +// let px = X[i] / dx; +// let py = Y[i] / dy; +// if (isNaN(px) || isNaN(py)) continue; +// let pj = Math.round(py), +// pi = Math.round(px = px - (pj & 1) / 2), +// py1 = py - pj; +// if (Math.abs(py1) * 3 > 1) { +// let px1 = px - pi, +// pi2 = pi + (px < pi ? -1 : 1) / 2, +// pj2 = pj + (py < pj ? -1 : 1), +// px2 = px - pi2, +// py2 = py - pj2; +// if (px1 * px1 + py1 * py1 > px2 * px2 + py2 * py2) pi = pi2 + (pj & 1 ? 1 : -1) / 2, pj = pj2; +// } +// const key = `${pi},${pj}`; +// let g = bins.get(key); +// if (g === undefined) { +// bins.set(key, g = []); +// g.x = (pi + (pj & 1) / 2) * dx; +// g.y = pj * dy; +// } +// g.push(i); +// } +// return bins.values(); +// } diff --git a/test/output/hexbin.svg b/test/output/hexbin.svg index b600b89934..084dcad571 100644 --- a/test/output/hexbin.svg +++ b/test/output/hexbin.svg @@ -88,197 +88,195 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/hexbinOranges.svg b/test/output/hexbinOranges.svg index a855e5439e..a3e2ec48d4 100644 --- a/test/output/hexbinOranges.svg +++ b/test/output/hexbinOranges.svg @@ -58,94 +58,94 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/hexbinR.html b/test/output/hexbinR.html index be11949cf5..37b3f86c0e 100644 --- a/test/output/hexbinR.html +++ b/test/output/hexbinR.html @@ -119,172 +119,181 @@ - + 0.067 - - 0.055 + + 0.061 - + 0.048 - + 0.048 - + 0.042 - + 0.042 - + 0.042 - + 0.036 - + 0.036 - + 0.03 - + 0.03 - + 0.03 - - 0.03 - - + 0.024 - + 0.024 - + 0.024 - + 0.018 - + 0.018 - + 0.018 - + 0.018 - + 0.018 - + 0.018 - - 0.018 + + 0.012 - + 0.012 - + 0.012 - + 0.012 - + 0.012 - + 0.012 - + 0.012 - + 0.012 - + 0.012 - + 0.012 - + 0.012 - + + 0.012 + + + 0.006 + + + 0.006 + + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + + 0.006 + + 0.006 @@ -292,199 +301,199 @@ - + 0.083 - - 0.065 + + 0.071 - + 0.048 - + 0.042 - + 0.042 - + 0.03 - + 0.03 - + 0.03 - + 0.03 - + 0.024 - + 0.024 - + 0.024 - + 0.024 - + 0.024 - + 0.018 - + 0.018 - + 0.018 - + 0.018 - + 0.018 - + 0.018 - + 0.018 - + 0.018 - + 0.018 - - 0.018 + + 0.012 - + 0.012 - + 0.012 - + 0.012 - + 0.012 - + 0.012 - + 0.012 - + 0.012 - + 0.012 - + 0.012 - + 0.012 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 - + 0.006 @@ -492,28 +501,28 @@ - + 0.182 - + 0.091 - + 0.091 - + 0.091 - + 0.091 - + 0.091 - + 0.091 - + 0.091 diff --git a/test/output/hexbinSymbol.html b/test/output/hexbinSymbol.html index 11c1d0bb82..7092960b6e 100644 --- a/test/output/hexbinSymbol.html +++ b/test/output/hexbinSymbol.html @@ -144,93 +144,95 @@ culmen_depth_mm → - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/hexbinText.svg b/test/output/hexbinText.svg index 0e70e704da..d0e0594137 100644 --- a/test/output/hexbinText.svg +++ b/test/output/hexbinText.svg @@ -68,128 +68,130 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - 729103256610131211311412281224113111611124149142111 + 7291032566101312113114122712241131111611124158142111 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - 18212131514179311224111293442123113171034964311112221121 + 1822213151417931221311121034321231131511348743111122111212 - - - - - - - - + + + + + + + + - 11211111 + 11211111 \ No newline at end of file diff --git a/test/output/hexbinZ.html b/test/output/hexbinZ.html index 198c51cd95..82e6980b2d 100644 --- a/test/output/hexbinZ.html +++ b/test/output/hexbinZ.html @@ -92,191 +92,190 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/hexbinZNull.svg b/test/output/hexbinZNull.svg index adbcdef875..ba15e0c6cd 100644 --- a/test/output/hexbinZNull.svg +++ b/test/output/hexbinZNull.svg @@ -88,195 +88,194 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/penguinDodgeHexbin.svg b/test/output/penguinDodgeHexbin.svg index 7af2735b03..2a4b7b57fd 100644 --- a/test/output/penguinDodgeHexbin.svg +++ b/test/output/penguinDodgeHexbin.svg @@ -210,156 +210,156 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -435,74 +435,74 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -633,128 +633,128 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 9ea4ae7f41..4f95aebb80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -571,6 +571,11 @@ d3-geo@3: dependencies: d3-array "2.5.0 - 3" +d3-hexbin@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/d3-hexbin/-/d3-hexbin-0.2.2.tgz#9c5837dacfd471ab05337a9e91ef10bfc4f98831" + integrity sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w== + d3-hierarchy@3: version "3.1.2" resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6" From 2dc40c09aeba3238572b153b8c78c375c2b1fb74 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 17:13:19 -0700 Subject: [PATCH 09/36] simpler z --- src/transforms/bin.js | 4 +- src/transforms/group.js | 4 +- src/transforms/hexbin.js | 136 ++++++++++++++--------------- test/plots/hexbin-z-null.js | 4 +- test/plots/hexbin-z.js | 12 +-- test/plots/penguin-dodge-hexbin.js | 2 +- 6 files changed, 76 insertions(+), 86 deletions(-) diff --git a/src/transforms/bin.js b/src/transforms/bin.js index 6691dff201..ad3ccf6f6c 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -97,8 +97,8 @@ function binn( const [GZ, setGZ] = maybeColumn(z); const [vfill] = maybeColorChannel(fill); const [vstroke] = maybeColorChannel(stroke); - const [GF = fill, setGF] = maybeColumn(vfill); - const [GS = stroke, setGS] = maybeColumn(vstroke); + const [GF, setGF] = maybeColumn(vfill); + const [GS, setGS] = maybeColumn(vstroke); return { ..."z" in inputs && {z: GZ || z}, diff --git a/src/transforms/group.js b/src/transforms/group.js index 0e188cf1ec..0f08165d8d 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -68,8 +68,8 @@ function groupn( const [GZ, setGZ] = maybeColumn(z); const [vfill] = maybeColorChannel(fill); const [vstroke] = maybeColorChannel(stroke); - const [GF = fill, setGF] = maybeColumn(vfill); - const [GS = stroke, setGS] = maybeColumn(vstroke); + const [GF, setGF] = maybeColumn(vfill); + const [GS, setGS] = maybeColumn(vstroke); return { ..."z" in inputs && {z: GZ || z}, diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js index fe93ea1b38..cb34f5c22b 100644 --- a/src/transforms/hexbin.js +++ b/src/transforms/hexbin.js @@ -1,7 +1,7 @@ import {hexbin as Hexbin} from "d3-hexbin"; // TODO inline // import {sqrt3} from "../symbols.js"; import {sqrt4_3} from "../symbols.js"; -import {identity, maybeColorChannel, valueof} from "../options.js"; +import {identity, isNoneish, number} from "../options.js"; import {hasOutput, maybeGroup, maybeOutputs, maybeSubgroup} from "./group.js"; import {initialize} from "./initialize.js"; @@ -12,80 +12,76 @@ import {initialize} from "./initialize.js"; // we offset slightly to ⟨0.5,0⟩. The hexgrid mark uses the same origin. export const ox = 0.5, oy = 0; -export function hexbin(outputs = {fill: "count"}, options = {}) { - const {binWidth, ...rest} = outputs; - return hexbinn(rest, {binWidth, ...options}); -} - // TODO filter e.g. to show empty hexbins? // TODO disallow x, x1, x2, y, y1, y2 reducers? -function hexbinn(outputs, {binWidth = 20, z, fill, stroke, ...options}) { - binWidth = +binWidth; - const [vfill] = maybeColorChannel(fill); - const [vstroke] = maybeColorChannel(stroke); - outputs = maybeOutputs(outputs, {z, fill, stroke, ...options}); - return { - symbol: "hexagon", - ...!hasOutput(outputs, "r") && {r: binWidth / 2}, - ...!hasOutput(outputs, "fill") && {fill}, - ...((hasOutput(outputs, "fill") || vstroke != null) && stroke === undefined) ? {stroke: "none"} : {stroke}, - ...initialize(options, function(data, facets, {x: X, y: Y}, scales) { - if (X === undefined) throw new Error("missing channel: x"); - if (Y === undefined) throw new Error("missing channel: y"); - const x = X.scale !== undefined ? scales[X.scale] : identity.transform; - const y = Y.scale !== undefined ? scales[Y.scale] : identity.transform; - X = X.value; - Y = Y.value; - // X = X.value.map(x); // TODO What if value is e.g. Uint32Array? - // Y = Y.value.map(y); // TODO What if value is e.g. Uint32Array? - const binsof = Hexbin().x(i => x(X[i]) - ox).y(i => y(Y[i]) - oy).radius(binWidth / 2 * sqrt4_3); - const Z = valueof(data, z); - const F = valueof(data, vfill); - const S = valueof(data, vstroke); - const G = maybeSubgroup(outputs, Z, F, S); - if (Z && !outputs.find(r => r.name === "z")) { - outputs.push(...maybeOutputs({z: "first"}, {z: Z})); - } - if (F && !outputs.find(r => r.name === "fill")) { - outputs.push(...maybeOutputs({fill: "first"}, {fill: F})); - } - if (S && !outputs.find(r => r.name === "stroke")) { - outputs.push(...maybeOutputs({stroke: "first"}, {stroke: S})); - } - const binFacets = []; - const BX = []; - const BY = []; - let i = -1; - for (const o of outputs) o.initialize(data); - for (const facet of facets) { - const binFacet = []; - for (const o of outputs) o.scope("facet", facet); - for (const [, index] of maybeGroup(facet, G)) { - // for (const bin of hbin(index, X, Y, binWidth)) { - for (const bin of binsof(index)) { - binFacet.push(++i); - BX.push(bin.x + ox); - BY.push(bin.y + oy); - for (const o of outputs) o.reduce(bin); - } +export function hexbin(outputs = {fill: "count"}, inputs = {}) { + let {binWidth, ...options} = inputs; + binWidth = binWidth === undefined ? 20 : number(binWidth); + outputs = maybeOutputs(outputs, options); + + // A fill output means a fill channel, and hence the stroke should default to + // none (assuming a mark that defaults to fill and no stroke, such as dot). + // Note that it’s safe to mutate options here because we just created it with + // the rest operator above. + const {z, fill, stroke} = options; + if (stroke === undefined && isNoneish(fill) && hasOutput(outputs, "fill")) options.stroke = "none"; + + // Populate default values for the r and symbol options, as appropriate. + if (options.symbol === undefined) options.symbol = "hexagon"; + if (options.r === undefined && !hasOutput(outputs, "r")) options.r = binWidth / 2; + + return initialize(options, (data, facets, {x: X, y: Y, z: Z, fill: F, stroke: S}, scales) => { + if (X === undefined) throw new Error("missing channel: x"); + if (Y === undefined) throw new Error("missing channel: y"); + const x = X.scale !== undefined ? scales[X.scale] : identity.transform; + const y = Y.scale !== undefined ? scales[Y.scale] : identity.transform; + X = X.value; + Y = Y.value; + Z = Z?.value; + F = F?.value; + S = S?.value; + const binsof = Hexbin().x(i => x(X[i]) - ox).y(i => y(Y[i]) - oy).radius(binWidth / 2 * sqrt4_3); // TODO inline + const G = maybeSubgroup(outputs, z === null ? null : Z, fill === null ? null : F, stroke === null ? null : S); + const GZ = Z && []; + const GF = F && []; + const GS = S && []; + const binFacets = []; + const BX = []; + const BY = []; + let i = -1; + for (const o of outputs) o.initialize(data); + for (const facet of facets) { + const binFacet = []; + for (const o of outputs) o.scope("facet", facet); + for (const [f, I] of maybeGroup(facet, G)) { + for (const bin of binsof(I)) { // TODO inline hbin(index, X, Y, binWidth) + binFacet.push(++i); + BX.push(bin.x + ox); + BY.push(bin.y + oy); + if (Z) GZ.push(G === Z ? f : Z[bin[0]]); + if (F) GF.push(G === F ? f : F[bin[0]]); + if (S) GS.push(G === S ? f : S[bin[0]]); + for (const o of outputs) o.reduce(bin); } - binFacets.push(binFacet); } - const channels = { - x: {value: BX}, - y: {value: BY}, - ...Z && {z: {value: Z}}, - ...F && {fill: {value: F, scale: true}}, - ...S && {stroke: {value: S, scale: true}}, - ...Object.fromEntries(outputs.map(({name, output}) => [name, {scale: true, binWidth: name === "r" ? binWidth : undefined, value: output.transform()}])) - }; - if ("r" in channels) { - const R = channels.r.value; - binFacets.forEach(index => index.sort((i, j) => R[j] - R[i])); + binFacets.push(binFacet); + } + const channels = { + x: {value: BX}, + y: {value: BY}, + ...Z && {z: {value: GZ}}, + ...F && {fill: {value: GF, scale: true}}, + ...S && {stroke: {value: GS, scale: true}}, + ...Object.fromEntries(outputs.map(({name, output}) => [name, {scale: true, value: output.transform()}])) // TODO binWidth: name === "r" ? binWidth : undefined? + }; + if ("r" in channels) { + const R = channels.r.value; + for (const I of binFacets) { + I.sort((i, j) => R[j] - R[i]); // TODO make this configurable } - return {data, facets: binFacets, channels}; - }) - }; + } + return {data, facets: binFacets, channels}; + }); } // function hbin(I, X, Y, dx) { diff --git a/test/plots/hexbin-z-null.js b/test/plots/hexbin-z-null.js index 3680146c8a..8709485437 100644 --- a/test/plots/hexbin-z-null.js +++ b/test/plots/hexbin-z-null.js @@ -14,9 +14,9 @@ export default async function() { y: "culmen_length_mm", stroke: "species", fill: "island", - z: null, fillOpacity: 0.5, - symbol: "dot" + z: null, + symbol: "dot" // TODO Plot.circle })) ] }); diff --git a/test/plots/hexbin-z.js b/test/plots/hexbin-z.js index e6adc73976..b98084982b 100644 --- a/test/plots/hexbin-z.js +++ b/test/plots/hexbin-z.js @@ -6,17 +6,11 @@ export default async function() { return Plot.plot({ x: {inset: 10}, y: {inset: 10}, + color: {legend: true}, marks: [ Plot.frame(), Plot.hexgrid(), - Plot.dot( - penguins, - Plot.hexbin( - { r: "count" }, - { x: "culmen_length_mm", y: "body_mass_g", stroke: "species", strokeOpacity: 0.8 } - ) - ) - ], - color: {legend: true} + Plot.dot(penguins, Plot.hexbin({r: "count"}, {x: "culmen_length_mm", y: "body_mass_g", stroke: "species", strokeOpacity: 0.8})) // TOD remove strokeOpacity + ] }); } diff --git a/test/plots/penguin-dodge-hexbin.js b/test/plots/penguin-dodge-hexbin.js index c2b159f77b..4d0553f90c 100644 --- a/test/plots/penguin-dodge-hexbin.js +++ b/test/plots/penguin-dodge-hexbin.js @@ -19,7 +19,7 @@ export default async function() { marks: [ Plot.frame(), Plot.dot(penguins, Plot.dodgeY("bottom", {x: "body_mass_g", stroke: "red", r: 3})), - Plot.dot(penguins, Plot.hexbin({binWidth: 7}, Plot.dodgeY("bottom", {x: "body_mass_g", fill: "black", r: 3}))) + Plot.dot(penguins, Plot.hexbin({}, Plot.dodgeY("bottom", {x: "body_mass_g", fill: "black", r: 3, binWidth: 7}))) ], color: {legend: true} }); From 336704c62513175731cbb7146292fb6bff3403f4 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 17:18:12 -0700 Subject: [PATCH 10/36] simpler scale application --- src/transforms/hexbin.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js index cb34f5c22b..ad1d3588fb 100644 --- a/src/transforms/hexbin.js +++ b/src/transforms/hexbin.js @@ -1,7 +1,7 @@ import {hexbin as Hexbin} from "d3-hexbin"; // TODO inline // import {sqrt3} from "../symbols.js"; import {sqrt4_3} from "../symbols.js"; -import {identity, isNoneish, number} from "../options.js"; +import {identity, isNoneish, number, valueof} from "../options.js"; import {hasOutput, maybeGroup, maybeOutputs, maybeSubgroup} from "./group.js"; import {initialize} from "./initialize.js"; @@ -33,14 +33,12 @@ export function hexbin(outputs = {fill: "count"}, inputs = {}) { return initialize(options, (data, facets, {x: X, y: Y, z: Z, fill: F, stroke: S}, scales) => { if (X === undefined) throw new Error("missing channel: x"); if (Y === undefined) throw new Error("missing channel: y"); - const x = X.scale !== undefined ? scales[X.scale] : identity.transform; - const y = Y.scale !== undefined ? scales[Y.scale] : identity.transform; - X = X.value; - Y = Y.value; + X = valueof(X.value, X.scale !== undefined ? scales[X.scale] : identity); + Y = valueof(Y.value, Y.scale !== undefined ? scales[Y.scale] : identity); Z = Z?.value; F = F?.value; S = S?.value; - const binsof = Hexbin().x(i => x(X[i]) - ox).y(i => y(Y[i]) - oy).radius(binWidth / 2 * sqrt4_3); // TODO inline + const binsof = Hexbin().x(i => X[i] - ox).y(i => Y[i] - oy).radius(binWidth / 2 * sqrt4_3); // TODO inline const G = maybeSubgroup(outputs, z === null ? null : Z, fill === null ? null : F, stroke === null ? null : S); const GZ = Z && []; const GF = F && []; From 22d518394e3ff5f08375c4ffb5f78a16ad540c08 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 17:39:45 -0700 Subject: [PATCH 11/36] re-inline d3-hexbin --- package.json | 1 - src/transforms/hexbin.js | 69 +++++++------ test/output/hexbinOranges.svg | 178 +++++++++++++++++----------------- yarn.lock | 5 - 4 files changed, 122 insertions(+), 131 deletions(-) diff --git a/package.json b/package.json index ff5390901b..f9ce070949 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ }, "dependencies": { "d3": "^7.3.0", - "d3-hexbin": "^0.2.2", "interval-tree-1d": "1", "isoformat": "0.2" }, diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js index ad1d3588fb..a40974adec 100644 --- a/src/transforms/hexbin.js +++ b/src/transforms/hexbin.js @@ -1,6 +1,4 @@ -import {hexbin as Hexbin} from "d3-hexbin"; // TODO inline -// import {sqrt3} from "../symbols.js"; -import {sqrt4_3} from "../symbols.js"; +import {sqrt3} from "../symbols.js"; import {identity, isNoneish, number, valueof} from "../options.js"; import {hasOutput, maybeGroup, maybeOutputs, maybeSubgroup} from "./group.js"; import {initialize} from "./initialize.js"; @@ -38,7 +36,6 @@ export function hexbin(outputs = {fill: "count"}, inputs = {}) { Z = Z?.value; F = F?.value; S = S?.value; - const binsof = Hexbin().x(i => X[i] - ox).y(i => Y[i] - oy).radius(binWidth / 2 * sqrt4_3); // TODO inline const G = maybeSubgroup(outputs, z === null ? null : Z, fill === null ? null : F, stroke === null ? null : S); const GZ = Z && []; const GF = F && []; @@ -52,10 +49,10 @@ export function hexbin(outputs = {fill: "count"}, inputs = {}) { const binFacet = []; for (const o of outputs) o.scope("facet", facet); for (const [f, I] of maybeGroup(facet, G)) { - for (const bin of binsof(I)) { // TODO inline hbin(index, X, Y, binWidth) + for (const bin of hbin(I, X, Y, binWidth)) { binFacet.push(++i); - BX.push(bin.x + ox); - BY.push(bin.y + oy); + BX.push(bin.x); + BY.push(bin.y); if (Z) GZ.push(G === Z ? f : Z[bin[0]]); if (F) GF.push(G === F ? f : F[bin[0]]); if (S) GS.push(G === S ? f : S[bin[0]]); @@ -82,32 +79,32 @@ export function hexbin(outputs = {fill: "count"}, inputs = {}) { }); } -// function hbin(I, X, Y, dx) { -// const dy = dx * sqrt3 / 2; -// const bins = new Map(); -// for (const i of I) { -// let px = X[i] / dx; -// let py = Y[i] / dy; -// if (isNaN(px) || isNaN(py)) continue; -// let pj = Math.round(py), -// pi = Math.round(px = px - (pj & 1) / 2), -// py1 = py - pj; -// if (Math.abs(py1) * 3 > 1) { -// let px1 = px - pi, -// pi2 = pi + (px < pi ? -1 : 1) / 2, -// pj2 = pj + (py < pj ? -1 : 1), -// px2 = px - pi2, -// py2 = py - pj2; -// if (px1 * px1 + py1 * py1 > px2 * px2 + py2 * py2) pi = pi2 + (pj & 1 ? 1 : -1) / 2, pj = pj2; -// } -// const key = `${pi},${pj}`; -// let g = bins.get(key); -// if (g === undefined) { -// bins.set(key, g = []); -// g.x = (pi + (pj & 1) / 2) * dx; -// g.y = pj * dy; -// } -// g.push(i); -// } -// return bins.values(); -// } +function hbin(I, X, Y, dx) { + const dy = dx * (1.5 / sqrt3); + const bins = new Map(); + for (const i of I) { + let px = X[i], + py = Y[i]; + if (isNaN(px) || isNaN(py)) continue; + let pj = Math.round(py = (py - oy) / dy), + pi = Math.round(px = (px - ox) / dx - (pj & 1) / 2), + py1 = py - pj; + if (Math.abs(py1) * 3 > 1) { + let px1 = px - pi, + pi2 = pi + (px < pi ? -1 : 1) / 2, + pj2 = pj + (py < pj ? -1 : 1), + px2 = px - pi2, + py2 = py - pj2; + if (px1 * px1 + py1 * py1 > px2 * px2 + py2 * py2) pi = pi2 + (pj & 1 ? 1 : -1) / 2, pj = pj2; + } + const key = `${pi},${pj}`; + let g = bins.get(key); + if (g === undefined) { + bins.set(key, g = []); + g.x = (pi + (pj & 1) / 2) * dx + ox; + g.y = pj * dy + oy; + } + g.push(i); + } + return bins.values(); +} diff --git a/test/output/hexbinOranges.svg b/test/output/hexbinOranges.svg index a3e2ec48d4..2a7f5cec6d 100644 --- a/test/output/hexbinOranges.svg +++ b/test/output/hexbinOranges.svg @@ -58,94 +58,94 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 4f95aebb80..9ea4ae7f41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -571,11 +571,6 @@ d3-geo@3: dependencies: d3-array "2.5.0 - 3" -d3-hexbin@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/d3-hexbin/-/d3-hexbin-0.2.2.tgz#9c5837dacfd471ab05337a9e91ef10bfc4f98831" - integrity sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w== - d3-hierarchy@3: version "3.1.2" resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6" From fc2c18589ead8fb75dbf1a8962937ef7a666d657 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 17:43:25 -0700 Subject: [PATCH 12/36] use descendingDefined to sort --- src/transforms/hexbin.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js index a40974adec..12bfba8b25 100644 --- a/src/transforms/hexbin.js +++ b/src/transforms/hexbin.js @@ -1,3 +1,4 @@ +import {descendingDefined} from "../defined.js"; import {sqrt3} from "../symbols.js"; import {identity, isNoneish, number, valueof} from "../options.js"; import {hasOutput, maybeGroup, maybeOutputs, maybeSubgroup} from "./group.js"; @@ -71,8 +72,8 @@ export function hexbin(outputs = {fill: "count"}, inputs = {}) { }; if ("r" in channels) { const R = channels.r.value; - for (const I of binFacets) { - I.sort((i, j) => R[j] - R[i]); // TODO make this configurable + for (const binFacet of binFacets) { + binFacet.sort((i, j) => descendingDefined(R[i], R[j])); // TODO make this configurable } } return {data, facets: binFacets, channels}; From 2c576e2dc9b2cb63f0e6543377b37f095241796d Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 17:56:46 -0700 Subject: [PATCH 13/36] coerce X and Y to numbers --- src/scales.js | 2 +- src/transforms/hexbin.js | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/scales.js b/src/scales.js index 552d17e52b..b5133ab77b 100644 --- a/src/scales.js +++ b/src/scales.js @@ -354,7 +354,7 @@ function coerceDates(values) { } // If the values are specified as a typed array, no coercion is required. -function coerceNumbers(values) { +export function coerceNumbers(values) { return isTypedArray(values) ? values : map(values, coerceNumber, Float64Array); } diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js index 12bfba8b25..9fa6374b90 100644 --- a/src/transforms/hexbin.js +++ b/src/transforms/hexbin.js @@ -1,4 +1,5 @@ import {descendingDefined} from "../defined.js"; +import {coerceNumbers} from "../scales.js"; import {sqrt3} from "../symbols.js"; import {identity, isNoneish, number, valueof} from "../options.js"; import {hasOutput, maybeGroup, maybeOutputs, maybeSubgroup} from "./group.js"; @@ -32,8 +33,8 @@ export function hexbin(outputs = {fill: "count"}, inputs = {}) { return initialize(options, (data, facets, {x: X, y: Y, z: Z, fill: F, stroke: S}, scales) => { if (X === undefined) throw new Error("missing channel: x"); if (Y === undefined) throw new Error("missing channel: y"); - X = valueof(X.value, X.scale !== undefined ? scales[X.scale] : identity); - Y = valueof(Y.value, Y.scale !== undefined ? scales[Y.scale] : identity); + X = coerceNumbers(valueof(X.value, X.scale !== undefined ? scales[X.scale] : identity)); + Y = coerceNumbers(valueof(Y.value, Y.scale !== undefined ? scales[Y.scale] : identity)); Z = Z?.value; F = F?.value; S = S?.value; @@ -99,13 +100,13 @@ function hbin(I, X, Y, dx) { if (px1 * px1 + py1 * py1 > px2 * px2 + py2 * py2) pi = pi2 + (pj & 1 ? 1 : -1) / 2, pj = pj2; } const key = `${pi},${pj}`; - let g = bins.get(key); - if (g === undefined) { - bins.set(key, g = []); - g.x = (pi + (pj & 1) / 2) * dx + ox; - g.y = pj * dy + oy; + let bin = bins.get(key); + if (bin === undefined) { + bins.set(key, bin = []); + bin.x = (pi + (pj & 1) / 2) * dx + ox; + bin.y = pj * dy + oy; } - g.push(i); + bin.push(i); } return bins.values(); } From 1ad88019e68e336e176b2a56ef5f7f6eb45ddaf5 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 18:10:07 -0700 Subject: [PATCH 14/36] populate radius hint --- src/transforms/hexbin.js | 2 +- test/output/hexbin.svg | 380 ++++++++++----------- test/output/hexbinR.html | 625 ++++++++++++++++++---------------- test/output/hexbinSymbol.html | 178 +++++----- test/output/hexbinZ.html | 362 ++++++++++---------- test/output/hexbinZNull.svg | 378 ++++++++++---------- test/plots/hexbin-r.js | 12 +- 7 files changed, 988 insertions(+), 949 deletions(-) diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js index 9fa6374b90..6845ea0485 100644 --- a/src/transforms/hexbin.js +++ b/src/transforms/hexbin.js @@ -69,7 +69,7 @@ export function hexbin(outputs = {fill: "count"}, inputs = {}) { ...Z && {z: {value: GZ}}, ...F && {fill: {value: GF, scale: true}}, ...S && {stroke: {value: GS, scale: true}}, - ...Object.fromEntries(outputs.map(({name, output}) => [name, {scale: true, value: output.transform()}])) // TODO binWidth: name === "r" ? binWidth : undefined? + ...Object.fromEntries(outputs.map(({name, output}) => [name, {scale: true, radius: name === "r" ? binWidth / 2 : undefined, value: output.transform()}])) }; if ("r" in channels) { const R = channels.r.value; diff --git a/test/output/hexbin.svg b/test/output/hexbin.svg index 084dcad571..7ed5a83ecd 100644 --- a/test/output/hexbin.svg +++ b/test/output/hexbin.svg @@ -88,195 +88,195 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/hexbinR.html b/test/output/hexbinR.html index 37b3f86c0e..6d10c759ea 100644 --- a/test/output/hexbinR.html +++ b/test/output/hexbinR.html @@ -18,20 +18,17 @@ 0 - + 5 - + 10 - + 15 - - - 20 - Proportion of each facet (%) + Proportion of each sex (%) - + ↑ culmen_length_mm - + FEMALE - + MALE - + - sex + sex - + 14 - + 16 - + 18 - + 20 - - + + 14 - + 16 - + 18 - + 20 - - + + 14 - + 16 - + 18 - + 20 - culmen_depth_mm → + culmen_depth_mm → - + - - 0.067 + + 8 + + + 8 + + + 7 + + + 7 + + + 7 + + + 6 + + + 6 - - 0.061 + + 6 - - 0.048 + + 5 - - 0.048 + + 5 - - 0.042 + + 5 - - 0.042 + + 5 - - 0.042 + + 5 - - 0.036 + + 4 - - 0.036 + + 4 - - 0.03 + + 3 - - 0.03 + + 3 - - 0.03 + + 3 - - 0.024 + + 3 - - 0.024 + + 3 - - 0.024 + + 3 - - 0.018 + + 2 - - 0.018 + + 2 - - 0.018 + + 2 - - 0.018 + + 2 - - 0.018 + + 2 - - 0.018 + + 2 - - 0.012 + + 2 - - 0.012 + + 2 - - 0.012 + + 2 - - 0.012 + + 2 - - 0.012 + + 2 - - 0.012 + + 2 - - 0.012 + + 2 - - 0.012 + + 2 - - 0.012 + + 1 - - 0.012 + + 1 - - 0.012 + + 1 - - 0.012 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 + + + 1 - - + + - - 0.083 + + 11 + + + 10 + + + 9 + + + 8 + + + 6 + + + 6 - - 0.071 + + 5 - - 0.048 + + 5 - - 0.042 + + 4 - - 0.042 + + 4 - - 0.03 + + 3 - - 0.03 + + 3 - - 0.03 + + 3 - - 0.03 + + 3 - - 0.024 + + 3 - - 0.024 + + 3 - - 0.024 + + 3 - - 0.024 + + 3 - - 0.024 + + 3 - - 0.018 + + 3 - - 0.018 + + 3 - - 0.018 + + 3 - - 0.018 + + 3 - - 0.018 + + 2 - - 0.018 + + 2 - - 0.018 + + 2 - - 0.018 + + 2 - - 0.018 + + 2 - - 0.012 + + 2 - - 0.012 + + 2 - - 0.012 + + 2 - - 0.012 + + 2 - - 0.012 + + 2 - - 0.012 + + 2 - - 0.012 + + 2 - - 0.012 + + 2 - - 0.012 + + 2 - - 0.012 + + 1 - - 0.012 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - 0.006 + + 1 - - + + - - 0.182 + + 2 - - 0.091 + + 1 - - 0.091 + + 1 - - 0.091 + + 1 - - 0.091 + + 1 - - 0.091 + + 1 - - 0.091 + + 1 - - 0.091 + + 1 diff --git a/test/output/hexbinSymbol.html b/test/output/hexbinSymbol.html index 7092960b6e..3b465c65e6 100644 --- a/test/output/hexbinSymbol.html +++ b/test/output/hexbinSymbol.html @@ -144,95 +144,95 @@ culmen_depth_mm → - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/hexbinZ.html b/test/output/hexbinZ.html index 82e6980b2d..e9f5a3edae 100644 --- a/test/output/hexbinZ.html +++ b/test/output/hexbinZ.html @@ -95,187 +95,187 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/hexbinZNull.svg b/test/output/hexbinZNull.svg index ba15e0c6cd..f40772fe8c 100644 --- a/test/output/hexbinZNull.svg +++ b/test/output/hexbinZNull.svg @@ -88,194 +88,194 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/hexbin-r.js b/test/plots/hexbin-r.js index e63eb72ae5..335a9095f8 100644 --- a/test/plots/hexbin-r.js +++ b/test/plots/hexbin-r.js @@ -4,9 +4,15 @@ import * as d3 from "d3"; export default async function() { const penguins = await d3.csv("data/penguins.csv", d3.autoType); return Plot.plot({ - width: 820, + width: 960, height: 320, - color: {scheme: "reds", nice: true, tickFormat: d => 100 * d, label: "Proportion of each facet (%)", legend: true}, + color: { + scheme: "reds", + label: "Proportion of each sex (%)", + zero: true, + percent: true, + legend: true + }, facet: { data: penguins, x: "sex", @@ -14,7 +20,7 @@ export default async function() { }, marks: [ Plot.frame(), - Plot.dot(penguins, Plot.hexbin({title: "proportion-facet", r: "count", fill: "proportion-facet"}, {x: "culmen_depth_mm", y: "culmen_length_mm", strokeWidth: 1})) + Plot.dot(penguins, Plot.hexbin({title: "count", r: "count", fill: "proportion-facet"}, {x: "culmen_depth_mm", y: "culmen_length_mm"})) ] }); } From 3e6903cf196105da3cc18e47c359ffd696570a3d Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 19:04:57 -0700 Subject: [PATCH 15/36] fix hexbin z; implicit group on symbol --- src/defined.js | 8 - src/transforms/bin.js | 2 +- src/transforms/group.js | 17 +- src/transforms/hexbin.js | 32 +++- test/output/hexbinSymbol.html | 313 ++++++++++++++++++++++++---------- test/plots/hexbin-symbol.js | 13 +- 6 files changed, 265 insertions(+), 120 deletions(-) diff --git a/src/defined.js b/src/defined.js index 7536baa21d..fe821d6f60 100644 --- a/src/defined.js +++ b/src/defined.js @@ -27,11 +27,3 @@ export function positive(x) { export function negative(x) { return x < 0 && isFinite(x) ? x : NaN; } - -export function firstof(...values) { - for (const v of values) { - if (v !== undefined) { - return v; - } - } -} diff --git a/src/transforms/bin.js b/src/transforms/bin.js index ad3ccf6f6c..bd7f5971db 100644 --- a/src/transforms/bin.js +++ b/src/transforms/bin.js @@ -109,7 +109,7 @@ function binn( const Z = valueof(data, z); const F = valueof(data, vfill); const S = valueof(data, vstroke); - const G = maybeSubgroup(outputs, Z, F, S); + const G = maybeSubgroup(outputs, {z: Z, fill: F, stroke: S}); const groupFacets = []; const groupData = []; const GK = K && setGK([]); diff --git a/src/transforms/group.js b/src/transforms/group.js index 0f08165d8d..f0758b32e3 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.js @@ -1,5 +1,5 @@ import {group as grouper, sort, sum, deviation, min, max, mean, median, mode, variance, InternSet, minIndex, maxIndex, rollup} from "d3"; -import {ascendingDefined, firstof} from "../defined.js"; +import {ascendingDefined} from "../defined.js"; import {valueof, maybeColorChannel, maybeInput, maybeTuple, maybeColumn, column, first, identity, take, labelof, range, second, percentile} from "../options.js"; import {basic} from "./basic.js"; @@ -81,7 +81,7 @@ function groupn( const Z = valueof(data, z); const F = valueof(data, vfill); const S = valueof(data, vstroke); - const G = maybeSubgroup(outputs, Z, F, S); + const G = maybeSubgroup(outputs, {z: Z, fill: F, stroke: S}); const groupFacets = []; const groupData = []; const GX = X && setGX([]); @@ -226,12 +226,13 @@ export function maybeReduce(reduce, value) { throw new Error(`invalid reduce: ${reduce}`); } -export function maybeSubgroup(outputs, Z, F, S) { - return firstof( - outputs.some(o => o.name === "z") ? undefined : Z, - outputs.some(o => o.name === "fill") ? undefined : F, - outputs.some(o => o.name === "stroke") ? undefined : S - ); +export function maybeSubgroup(outputs, inputs) { + for (const name in inputs) { + const value = inputs[name]; + if (value !== undefined && !outputs.some(o => o.name === name)) { + return value; + } + } } export function maybeSort(facets, sort, reverse) { diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js index 6845ea0485..4a26da1bbf 100644 --- a/src/transforms/hexbin.js +++ b/src/transforms/hexbin.js @@ -23,25 +23,39 @@ export function hexbin(outputs = {fill: "count"}, inputs = {}) { // none (assuming a mark that defaults to fill and no stroke, such as dot). // Note that it’s safe to mutate options here because we just created it with // the rest operator above. - const {z, fill, stroke} = options; + const {z, fill, stroke, symbol} = options; if (stroke === undefined && isNoneish(fill) && hasOutput(outputs, "fill")) options.stroke = "none"; // Populate default values for the r and symbol options, as appropriate. if (options.symbol === undefined) options.symbol = "hexagon"; if (options.r === undefined && !hasOutput(outputs, "r")) options.r = binWidth / 2; - return initialize(options, (data, facets, {x: X, y: Y, z: Z, fill: F, stroke: S}, scales) => { + return initialize(options, (data, facets, {x: X, y: Y, z: Z, fill: F, stroke: S, symbol: Q}, scales) => { if (X === undefined) throw new Error("missing channel: x"); if (Y === undefined) throw new Error("missing channel: y"); + + // Coerce the X and Y channels to numbers (so that null is properly treated + // as an undefined value rather than being coerced to zero). X = coerceNumbers(valueof(X.value, X.scale !== undefined ? scales[X.scale] : identity)); Y = coerceNumbers(valueof(Y.value, Y.scale !== undefined ? scales[Y.scale] : identity)); - Z = Z?.value; + + // Extract the values for channels that are eligible for grouping; not all + // marks define a z channel, so compute one if it not already computed. If z + // was explicitly set to null, ensure that we don’t subdivide bins. + Z = Z ? Z.value : valueof(data, z); F = F?.value; S = S?.value; - const G = maybeSubgroup(outputs, z === null ? null : Z, fill === null ? null : F, stroke === null ? null : S); + Q = Q?.value; + + // Group on the first of z, fill, stroke, and symbol. Implicitly reduce + // these channels using the first corresponding value for each bin. + const G = maybeSubgroup(outputs, {z: Z, fill: F, stroke: S, symbol: Q}); const GZ = Z && []; const GF = F && []; const GS = S && []; + const GQ = Q && []; + + // Construct the hexbins and populate the output channels. const binFacets = []; const BX = []; const BY = []; @@ -58,25 +72,33 @@ export function hexbin(outputs = {fill: "count"}, inputs = {}) { if (Z) GZ.push(G === Z ? f : Z[bin[0]]); if (F) GF.push(G === F ? f : F[bin[0]]); if (S) GS.push(G === S ? f : S[bin[0]]); + if (Q) GQ.push(G === Q ? f : Q[bin[0]]); for (const o of outputs) o.reduce(bin); } } binFacets.push(binFacet); } + + // Construct the output channels, and populate the radius scale hint. const channels = { x: {value: BX}, y: {value: BY}, ...Z && {z: {value: GZ}}, ...F && {fill: {value: GF, scale: true}}, ...S && {stroke: {value: GS, scale: true}}, + ...Q && {symbol: {value: GQ, scale: true}}, ...Object.fromEntries(outputs.map(({name, output}) => [name, {scale: true, radius: name === "r" ? binWidth / 2 : undefined, value: output.transform()}])) }; + + // When producing a radius channel, implicitly sort by descending radius. + // TODO This should be configurable somehow. if ("r" in channels) { const R = channels.r.value; for (const binFacet of binFacets) { - binFacet.sort((i, j) => descendingDefined(R[i], R[j])); // TODO make this configurable + binFacet.sort((i, j) => descendingDefined(R[i], R[j])); } } + return {data, facets: binFacets, channels}; }); } diff --git a/test/output/hexbinSymbol.html b/test/output/hexbinSymbol.html index 3b465c65e6..82439318dd 100644 --- a/test/output/hexbinSymbol.html +++ b/test/output/hexbinSymbol.html @@ -39,7 +39,9 @@ FEMALE - MALE + MALE + + null culmen_depth_mm → - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/hexbin-symbol.js b/test/plots/hexbin-symbol.js index d26da69415..ca69c31cc9 100644 --- a/test/plots/hexbin-symbol.js +++ b/test/plots/hexbin-symbol.js @@ -5,14 +5,11 @@ export default async function() { const penguins = await d3.csv("data/penguins.csv", d3.autoType); return Plot.plot({ grid: true, + symbol: { + legend: true + }, marks: [ - Plot.dot(penguins, Plot.hexbin({r: "count", symbol: "mode"}, { - binWidth: 40, - symbol: "sex", - x: "culmen_depth_mm", - y: "culmen_length_mm" - })) - ], - symbol: {legend: true} + Plot.dot(penguins, Plot.hexbin({r: "count"}, {symbol: "sex", x: "culmen_depth_mm", y: "culmen_length_mm"})) + ] }); } From 5f116e980d4793130e7800d4d419f595451c074a Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 19:44:21 -0700 Subject: [PATCH 16/36] update tests --- src/scales.js | 2 +- src/transforms/hexbin.js | 2 +- test/output/hexbin.svg | 2 +- test/output/hexbinOranges.svg | 334 +++++++++++++++++++++++----------- test/plots/hexbin-oranges.js | 13 +- test/plots/hexbin.js | 2 +- 6 files changed, 238 insertions(+), 117 deletions(-) diff --git a/src/scales.js b/src/scales.js index b5133ab77b..cbd0925f1b 100644 --- a/src/scales.js +++ b/src/scales.js @@ -5,8 +5,8 @@ import {ScaleLinear, ScaleSqrt, ScalePow, ScaleLog, ScaleSymlog, ScaleQuantile, import {ScaleDiverging, ScaleDivergingSqrt, ScaleDivergingPow, ScaleDivergingLog, ScaleDivergingSymlog} from "./scales/diverging.js"; import {ScaleTime, ScaleUtc} from "./scales/temporal.js"; import {ScaleOrdinal, ScalePoint, ScaleBand, ordinalImplicit} from "./scales/ordinal.js"; -import {warn} from "./warnings.js"; import {isSymbol, maybeSymbol} from "./symbols.js"; +import {warn} from "./warnings.js"; export function Scales(channelsByScale, { inset: globalInset = 0, diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js index 4a26da1bbf..bc4c2c304d 100644 --- a/src/transforms/hexbin.js +++ b/src/transforms/hexbin.js @@ -23,7 +23,7 @@ export function hexbin(outputs = {fill: "count"}, inputs = {}) { // none (assuming a mark that defaults to fill and no stroke, such as dot). // Note that it’s safe to mutate options here because we just created it with // the rest operator above. - const {z, fill, stroke, symbol} = options; + const {z, fill, stroke} = options; if (stroke === undefined && isNoneish(fill) && hasOutput(outputs, "fill")) options.stroke = "none"; // Populate default values for the r and symbol options, as appropriate. diff --git a/test/output/hexbin.svg b/test/output/hexbin.svg index 7ed5a83ecd..ca403822cc 100644 --- a/test/output/hexbin.svg +++ b/test/output/hexbin.svg @@ -80,13 +80,13 @@ 21 culmen_depth_mm → - + diff --git a/test/output/hexbinOranges.svg b/test/output/hexbinOranges.svg index 2a7f5cec6d..357a281353 100644 --- a/test/output/hexbinOranges.svg +++ b/test/output/hexbinOranges.svg @@ -14,138 +14,262 @@ } - - 35 + + 34 - + + 36 + + + 38 + + 40 - - 45 + + 42 + + + 44 + + + 46 - + + 48 + + 50 - - 55 + + 52 + + + 54 + + + 56 + + + 58 ↑ culmen_length_mm - + 14 - + 15 - + 16 - + 17 - + 18 - + 19 - + 20 - + 21 culmen_depth_mm → - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/hexbin-oranges.js b/test/plots/hexbin-oranges.js index 29bb84bdeb..30b19e7a37 100644 --- a/test/plots/hexbin-oranges.js +++ b/test/plots/hexbin-oranges.js @@ -4,16 +4,13 @@ import * as d3 from "d3"; export default async function() { const penguins = await d3.csv("data/penguins.csv", d3.autoType); return Plot.plot({ - color: {scheme: "oranges"}, - inset: 30, + inset: 10, + color: { + scheme: "oranges" + }, marks: [ Plot.frame(), - Plot.circle(penguins, Plot.hexbin({fill: "count"}, { - x: "culmen_depth_mm", - y: "culmen_length_mm", - binWidth: 35, - strokeWidth: 1 - })) + Plot.circle(penguins, Plot.hexbin({fill: "count"}, {x: "culmen_depth_mm", y: "culmen_length_mm"})) ] }); } diff --git a/test/plots/hexbin.js b/test/plots/hexbin.js index 9b2e63872b..00f745ec34 100644 --- a/test/plots/hexbin.js +++ b/test/plots/hexbin.js @@ -5,8 +5,8 @@ export default async function() { const penguins = await d3.csv("data/penguins.csv", d3.autoType); return Plot.plot({ marks: [ - Plot.frame(), Plot.hexgrid(), + Plot.frame(), Plot.dot(penguins, Plot.hexbin({r: "count"}, {x: "culmen_depth_mm", y: "culmen_length_mm"})) ] }); From bb3d455abbdfae85b436d6a8a25c1f7366dbb04b Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 19:52:38 -0700 Subject: [PATCH 17/36] expose initialize; rewrite remap --- src/index.js | 1 + test/plots/cars-jiggle.js | 26 +------------------------- test/plots/darker-dodge.js | 26 +------------------------- test/transforms/remap.js | 22 ++++++++++++++++++++++ 4 files changed, 25 insertions(+), 50 deletions(-) create mode 100644 test/transforms/remap.js diff --git a/src/index.js b/src/index.js index 0c6e976309..70470832b4 100644 --- a/src/index.js +++ b/src/index.js @@ -22,6 +22,7 @@ export {bin, binX, binY} from "./transforms/bin.js"; export {dodgeX, dodgeY} from "./transforms/dodge.js"; export {group, groupX, groupY, groupZ} from "./transforms/group.js"; export {hexbin} from "./transforms/hexbin.js"; +export {initialize} from "./transforms/initialize.js"; export {normalize, normalizeX, normalizeY} from "./transforms/normalize.js"; export {map, mapX, mapY} from "./transforms/map.js"; export {window, windowX, windowY} from "./transforms/window.js"; diff --git a/test/plots/cars-jiggle.js b/test/plots/cars-jiggle.js index b20a2900e7..df65d369a9 100644 --- a/test/plots/cars-jiggle.js +++ b/test/plots/cars-jiggle.js @@ -1,30 +1,6 @@ import * as Plot from "@observablehq/plot"; import * as d3 from "d3"; -import {initialize} from "../../src/transforms/initialize.js"; - -function remap(outputs = {}, options) { - return initialize(options, (data, facets, channels, scales) => { - const newChannels = {}; - for (const [key, map] of Object.entries(outputs)) { - const input = channels[key]; - if (input == null) throw new Error(`missing channel: ${key}`); - const V = Array.from(input.value); - if (input.scale != null) { - const scale = scales[input.scale]; - if (scale != null) { - for (let i = 0; i < V.length; ++i) V[i] = scale(V[i]); - } - } - for (let i = 0; i < V.length; ++i) V[i] = map(V[i]); - newChannels[key] = {value: V}; - } - return { - data, - facets, - channels: newChannels - }; - }); -} +import {remap} from "../transforms/remap.js"; const random = d3.randomNormal.source(d3.randomLcg(42))(0, 7); diff --git a/test/plots/darker-dodge.js b/test/plots/darker-dodge.js index 4395562645..407164b4c1 100644 --- a/test/plots/darker-dodge.js +++ b/test/plots/darker-dodge.js @@ -1,30 +1,6 @@ import * as Plot from "@observablehq/plot"; import * as d3 from "d3"; -import {initialize} from "../../src/transforms/initialize.js"; - -function remap(outputs = {}, options) { - return initialize(options, (data, facets, channels, scales) => { - const newChannels = {}; - for (const [key, map] of Object.entries(outputs)) { - const input = channels[key]; - if (input == null) throw new Error(`missing channel: ${key}`); - const V = Array.from(input.value); - if (input.scale != null) { - const scale = scales[input.scale]; - if (scale != null) { - for (let i = 0; i < V.length; ++i) V[i] = scale(V[i]); - } - } - for (let i = 0; i < V.length; ++i) V[i] = map(V[i]); - newChannels[key] = {value: V}; - } - return { - data, - facets, - channels: newChannels - }; - }); -} +import {remap} from "../transforms/remap.js"; // In the following, darker and Plot.dodgeY are interchangeable export default async function() { diff --git a/test/transforms/remap.js b/test/transforms/remap.js new file mode 100644 index 0000000000..7658d21250 --- /dev/null +++ b/test/transforms/remap.js @@ -0,0 +1,22 @@ +import {initialize} from "@observablehq/plot"; + +export function remap(outputs = {}, options) { + return initialize(options, (data, facets, channels, scales) => { + return { + data, + facets, + channels: Object.fromEntries(Object.entries(outputs).map(([name, map]) => { + const channel = channels[name]; + if (!channel) throw new Error(`missing channel: ${name}`); + const V = Array.from(channel.value); + const n = V.length; + if (channel.scale !== undefined) { + const scale = scales[channel.scale]; + for (let i = 0; i < n; ++i) V[i] = scale(V[i]); + } + for (let i = 0; i < n; ++i) V[i] = map(V[i], i, V); + return [name, {value: V}]; + })) + }; + }); +} From 0d19e44be572dded347711b0f7cff39971ac8726 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 19:56:49 -0700 Subject: [PATCH 18/36] tweak tests --- .../{carsJiggle.html => carsJitter.html} | 802 +++++++++--------- test/plots/cars-jiggle.js | 24 - test/plots/cars-jitter.js | 23 + test/plots/index.js | 2 +- 4 files changed, 425 insertions(+), 426 deletions(-) rename test/output/{carsJiggle.html => carsJitter.html} (69%) delete mode 100644 test/plots/cars-jiggle.js create mode 100644 test/plots/cars-jitter.js diff --git a/test/output/carsJiggle.html b/test/output/carsJitter.html similarity index 69% rename from test/output/carsJiggle.html rename to test/output/carsJitter.html index e2b2d8cc82..cce7155429 100644 --- a/test/output/carsJiggle.html +++ b/test/output/carsJitter.html @@ -13,7 +13,7 @@ white-space: pre; } - + 50 @@ -95,405 +95,405 @@ weight (lb) → - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/cars-jiggle.js b/test/plots/cars-jiggle.js deleted file mode 100644 index df65d369a9..0000000000 --- a/test/plots/cars-jiggle.js +++ /dev/null @@ -1,24 +0,0 @@ -import * as Plot from "@observablehq/plot"; -import * as d3 from "d3"; -import {remap} from "../transforms/remap.js"; - -const random = d3.randomNormal.source(d3.randomLcg(42))(0, 7); - -export default async function() { - const data = await d3.csv("data/cars.csv", d3.autoType); - return Plot.plot({ - height: 350, - y: {type: "band", reverse: true, grid: true}, - color: {nice: true, scheme: "warm", reverse: true, legend: true}, - nice: true, - marks: [ - Plot.dot(data, remap({y: d => d + random()}, { - x: "weight (lb)", - y: "cylinders", - fill: "power (hp)", - stroke: "white", - strokeWidth: 0.5 - })) - ] - }); -} diff --git a/test/plots/cars-jitter.js b/test/plots/cars-jitter.js new file mode 100644 index 0000000000..dda811d361 --- /dev/null +++ b/test/plots/cars-jitter.js @@ -0,0 +1,23 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; +import {remap} from "../transforms/remap.js"; + +export default async function() { + const random = d3.randomNormal.source(d3.randomLcg(42))(0, 7); + const data = await d3.csv("data/cars.csv", d3.autoType); + return Plot.plot({ + height: 350, + nice: true, + y: { + type: "band", + grid: true, + reverse: true + }, + color: { + legend: true + }, + marks: [ + Plot.dot(data, remap({y: d => d + random()}, {x: "weight (lb)", y: "cylinders", fill: "power (hp)", stroke: "white", strokeWidth: 0.5})) + ] + }); +} diff --git a/test/plots/index.js b/test/plots/index.js index 84e5dc44a3..9230e1e907 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -28,7 +28,7 @@ export {default as binTimestamps} from "./bin-timestamps.js"; export {default as boxplot} from "./boxplot.js"; export {default as caltrain} from "./caltrain.js"; export {default as caltrainDirection} from "./caltrain-direction.js"; -export {default as carsJiggle} from "./cars-jiggle.js"; +export {default as carsJitter} from "./cars-jitter.js"; export {default as carsMpg} from "./cars-mpg.js"; export {default as carsParcoords} from "./cars-parcoords.js"; export {default as clamp} from "./clamp.js"; From f43d08d46ff2811a1229efd9326e24f58bbba819 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 20:11:35 -0700 Subject: [PATCH 19/36] tweak tests --- test/output/darkerDodge.svg | 302 ++++++++++++++++++------------------ test/plots/darker-dodge.js | 23 +-- 2 files changed, 163 insertions(+), 162 deletions(-) diff --git a/test/output/darkerDodge.svg b/test/output/darkerDodge.svg index 2946ee7fc0..038b00e89c 100644 --- a/test/output/darkerDodge.svg +++ b/test/output/darkerDodge.svg @@ -45,156 +45,156 @@ 9 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/darker-dodge.js b/test/plots/darker-dodge.js index 407164b4c1..9939414efb 100644 --- a/test/plots/darker-dodge.js +++ b/test/plots/darker-dodge.js @@ -2,22 +2,23 @@ import * as Plot from "@observablehq/plot"; import * as d3 from "d3"; import {remap} from "../transforms/remap.js"; -// In the following, darker and Plot.dodgeY are interchangeable +function darker(outputs, inputs) { + return remap( + Object.fromEntries(Object.entries(outputs).map(([name, value]) => [name, v => d3.lab(v).darker(value)])), + inputs + ); +} + export default async function() { + const random = d3.randomLogNormal.source(d3.randomLcg(42))(); return Plot.plot({ - marginTop: 10, + height: 170, nice: true, marks: [ Plot.dotX( - Array.from({ length: 150 }, d3.randomLogNormal.source(d3.randomLcg(42))()), - Plot.dodgeY("middle", remap({ - fill: v => d3.rgb(v).darker(0.7).formatHex() - }, { - x: (d) => d, - fill: (d) => d - })) + Array.from({length: 150}, random), + Plot.dodgeY({anchor: "middle"}, darker({stroke: 2}, {x: d => d, fill: d => d, stroke: d => d})) ) - ], - height: 170 + ] }); } From d4a286b8b90d2a6077bca369ed715530618f7419 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 20:36:31 -0700 Subject: [PATCH 20/36] tweak tests --- test/output/hexbinText.svg | 306 ++++++++++++++++------------- test/output/hexbinZNull.svg | 382 ++++++++++++++++++------------------ test/plots/hexbin-text.js | 4 +- test/plots/hexbin-z-null.js | 21 +- 4 files changed, 370 insertions(+), 343 deletions(-) diff --git a/test/output/hexbinText.svg b/test/output/hexbinText.svg index d0e0594137..b251c8c6f7 100644 --- a/test/output/hexbinText.svg +++ b/test/output/hexbinText.svg @@ -1,4 +1,4 @@ - + ↑ culmen_length_mm - + FEMALE - + MALE - + - sex + sex - - 15 + + 14 - + + 16 + + + 18 + + 20 - - - 15 + + + 14 + + + 16 - + + 18 + + 20 - - - 15 + + + 14 + + + 16 - + + 18 + + 20 - culmen_depth_mm → + culmen_depth_mm → - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 7291032566101312113114122712241131111611124158142111 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 7447546101148212213115315122311311111115712313414662111 - - + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1822213151417931221311121034321231131511348743111122111212 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 5141131318111414541111112111225522111138131466654241110113111211 - - + + - - - - - - - - - - 11211111 + + + + + + + + + + 11211111 \ No newline at end of file diff --git a/test/output/hexbinZNull.svg b/test/output/hexbinZNull.svg index f40772fe8c..90a829fc41 100644 --- a/test/output/hexbinZNull.svg +++ b/test/output/hexbinZNull.svg @@ -80,202 +80,202 @@ 21 culmen_depth_mm → - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/hexbin-text.js b/test/plots/hexbin-text.js index 8ab4086588..360e087022 100644 --- a/test/plots/hexbin-text.js +++ b/test/plots/hexbin-text.js @@ -4,14 +4,14 @@ import * as d3 from "d3"; export default async function() { const penguins = await d3.csv("data/penguins.csv", d3.autoType); return Plot.plot({ - width: 820, + width: 960, height: 320, + inset: 14, facet: { data: penguins, x: "sex", marginRight: 80 }, - inset: 14, marks: [ Plot.frame(), Plot.dot(penguins, Plot.hexbin({fillOpacity: "count"}, {x: "culmen_depth_mm", y: "culmen_length_mm", fill: "brown", stroke: "black", strokeWidth: 0.5})), diff --git a/test/plots/hexbin-z-null.js b/test/plots/hexbin-z-null.js index 8709485437..6be2b8e2ec 100644 --- a/test/plots/hexbin-z-null.js +++ b/test/plots/hexbin-z-null.js @@ -4,20 +4,17 @@ import * as d3 from "d3"; export default async function() { const penguins = await d3.csv("data/penguins.csv", d3.autoType); return Plot.plot({ - x: {inset: 10}, - y: {inset: 10}, + inset: 10, marks: [ - Plot.frame(), Plot.hexgrid(), - Plot.dot(penguins, Plot.hexbin({r: "count"}, { - x: "culmen_depth_mm", - y: "culmen_length_mm", - stroke: "species", - fill: "island", - fillOpacity: 0.5, - z: null, - symbol: "dot" // TODO Plot.circle - })) + Plot.frame(), + Plot.circle( + penguins, + Plot.hexbin( + {r: "count", stroke: "mode", fill: "mode"}, + {x: "culmen_depth_mm", y: "culmen_length_mm", z: null, fill: "island", stroke: "species"} + ) + ) ] }); } From d0129307c672336578a0d3dc5e3c51f1a4baeeb1 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 20:38:02 -0700 Subject: [PATCH 21/36] tweak tests --- test/output/hexbinZ.html | 4 ++-- test/plots/hexbin-z.js | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/test/output/hexbinZ.html b/test/output/hexbinZ.html index e9f5a3edae..588aba446e 100644 --- a/test/output/hexbinZ.html +++ b/test/output/hexbinZ.html @@ -87,14 +87,14 @@ 55 culmen_length_mm → - - + + diff --git a/test/plots/hexbin-z.js b/test/plots/hexbin-z.js index b98084982b..d0f17a5597 100644 --- a/test/plots/hexbin-z.js +++ b/test/plots/hexbin-z.js @@ -4,13 +4,14 @@ import * as d3 from "d3"; export default async function() { const penguins = await d3.csv("data/penguins.csv", d3.autoType); return Plot.plot({ - x: {inset: 10}, - y: {inset: 10}, - color: {legend: true}, + inset: 10, + color: { + legend: true + }, marks: [ - Plot.frame(), Plot.hexgrid(), - Plot.dot(penguins, Plot.hexbin({r: "count"}, {x: "culmen_length_mm", y: "body_mass_g", stroke: "species", strokeOpacity: 0.8})) // TOD remove strokeOpacity + Plot.frame(), + Plot.dot(penguins, Plot.hexbin({r: "count"}, {x: "culmen_length_mm", y: "body_mass_g", stroke: "species"})) ] }); } From 7c4fc40c528407c8c6832d28d009cb0c912e5d48 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 20:40:38 -0700 Subject: [PATCH 22/36] tweak tests --- test/plots/penguin-dodge-hexbin.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/plots/penguin-dodge-hexbin.js b/test/plots/penguin-dodge-hexbin.js index 4d0553f90c..e1c79d85cd 100644 --- a/test/plots/penguin-dodge-hexbin.js +++ b/test/plots/penguin-dodge-hexbin.js @@ -1,7 +1,7 @@ import * as Plot from "@observablehq/plot"; import * as d3 from "d3"; -// test channel transform composition +// Test channel transform composition. export default async function() { const penguins = await d3.csv("data/penguins.csv", d3.autoType); return Plot.plot({ @@ -10,6 +10,9 @@ export default async function() { grid: true, inset: 7 }, + color: { + legend: true + }, facet: { data: penguins, y: "species", @@ -20,7 +23,6 @@ export default async function() { Plot.frame(), Plot.dot(penguins, Plot.dodgeY("bottom", {x: "body_mass_g", stroke: "red", r: 3})), Plot.dot(penguins, Plot.hexbin({}, Plot.dodgeY("bottom", {x: "body_mass_g", fill: "black", r: 3, binWidth: 7}))) - ], - color: {legend: true} + ] }); } From cb37aeb0aa7ed440deac02218338fe05859a1464 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 22:12:30 -0700 Subject: [PATCH 23/36] tweak tests --- test/plots/penguin-facet-dodge-island.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/plots/penguin-facet-dodge-island.js b/test/plots/penguin-facet-dodge-island.js index 4c40151192..fe46e93965 100644 --- a/test/plots/penguin-facet-dodge-island.js +++ b/test/plots/penguin-facet-dodge-island.js @@ -8,6 +8,9 @@ export default async function() { x: { grid: true }, + color: { + legend: true + }, facet: { data: penguins, y: "species", @@ -16,7 +19,6 @@ export default async function() { }, marks: [ Plot.dot(penguins, Plot.dodgeY("middle", {x: "body_mass_g", fill: "island"})) - ], - color: {legend: true} + ] }); } From cda1bdac6ff4bb68aeeeebe54650bc6a9aaaeea0 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 22:19:54 -0700 Subject: [PATCH 24/36] tweak tests --- test/output/penguinFacetDodgeSymbol.html | 722 +++++++++++------------ test/plots/penguin-facet-dodge-symbol.js | 12 +- 2 files changed, 367 insertions(+), 367 deletions(-) diff --git a/test/output/penguinFacetDodgeSymbol.html b/test/output/penguinFacetDodgeSymbol.html index 0b45525c2e..c45deba482 100644 --- a/test/output/penguinFacetDodgeSymbol.html +++ b/test/output/penguinFacetDodgeSymbol.html @@ -42,7 +42,7 @@ Chinstrap Gentoo - + - + - 2,500 + 2,500 - + - 3,000 + 3,000 - + - 3,500 + 3,500 - + - 4,000 + 4,000 - + - 4,500 + 4,500 - + - 5,000 + 5,000 - + - 5,500 + 5,500 - + - 6,000 + 6,000 - 6,500 + 6,500 ↑ body_mass_g - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/penguin-facet-dodge-symbol.js b/test/plots/penguin-facet-dodge-symbol.js index a16dcb5737..446eab271e 100644 --- a/test/plots/penguin-facet-dodge-symbol.js +++ b/test/plots/penguin-facet-dodge-symbol.js @@ -4,13 +4,13 @@ import * as d3 from "d3"; export default async function() { const penguins = await d3.csv("data/penguins.csv", d3.autoType); return Plot.plot({ - height: 450, - width: 300, - marginRight: 60, - y: {grid: true, nice: true}, - symbol: {legend: true}, + grid: true, + nice: true, + symbol: { + legend: true + }, marks: [ - Plot.dot(penguins, Plot.dodgeX("left", {y: "body_mass_g", symbol: "species", stroke: "species", dx: 2})) + Plot.dot(penguins, Plot.dodgeX("left", {y: "body_mass_g", symbol: "species", stroke: "species"})) ] }); } From 02d01aeb58c51975310a3d96692f195c1abb1fb2 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 22:21:13 -0700 Subject: [PATCH 25/36] tweak tests --- test/plots/penguin-facet-dodge.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/plots/penguin-facet-dodge.js b/test/plots/penguin-facet-dodge.js index 77ed20a433..96a18246e0 100644 --- a/test/plots/penguin-facet-dodge.js +++ b/test/plots/penguin-facet-dodge.js @@ -5,9 +5,7 @@ export default async function() { const penguins = await d3.csv("data/penguins.csv", d3.autoType); return Plot.plot({ height: 300, - x: { - grid: true - }, + grid: true, facet: { data: penguins, y: "species", From a7718cf1a99b955931568462f6ee75179138f3e7 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 22:30:47 -0700 Subject: [PATCH 26/36] fix scale association, numeric coercion --- src/transforms/dodge.js | 17 ++++++++--------- src/transforms/hexbin.js | 2 ++ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/transforms/dodge.js b/src/transforms/dodge.js index 770a051816..9701362173 100644 --- a/src/transforms/dodge.js +++ b/src/transforms/dodge.js @@ -1,6 +1,8 @@ import {max} from "d3"; import IntervalTree from "interval-tree-1d"; import {finite, positive} from "../defined.js"; +import {identity, number, valueof} from "../options.js"; +import {coerceNumbers} from "../scales.js"; import {initialize} from "./initialize.js"; const anchorXLeft = ({marginLeft}) => [1, marginLeft]; @@ -39,12 +41,11 @@ export function dodgeY(dodgeOptions = {}, options = {}) { } function dodge(y, x, anchor, padding, options) { - return initialize(options, function(data, facets, {[x]: X, r: R}, {[x]: xscale, r: rscale}, dimensions) { - if (!X) throw new Error(`missing channel ${x}`); - X = X.value.map(xscale); - const r = R ? undefined : this.r !== undefined ? this.r : options.r !== undefined ? +options.r : 3; - if (R) R = R.value.map(rscale); - if (X == null) throw new Error(`missing channel: ${x}`); + return initialize(options, function(data, facets, {[x]: X, r: R}, scales, dimensions) { + if (!X) throw new Error(`missing channel: ${x}`); + X = coerceNumbers(valueof(X.value, X.scale !== undefined ? scales[X.scale] : identity)); + const r = R ? undefined : this.r !== undefined ? this.r : options.r !== undefined ? number(options.r) : 3; + if (R) R = coerceNumbers(valueof(R.value, R.scale !== undefined ? scales[R.scale] : identity)); let [ky, ty] = anchor(dimensions); const compare = ky ? compareAscending : compareSymmetric; if (ky) ty += ky * ((R ? max(facets.flat(), i => R[i]) : r) + padding); else ky = 1; @@ -52,9 +53,7 @@ function dodge(y, x, anchor, padding, options) { const radius = R ? i => R[i] : () => r; for (let I of facets) { const tree = IntervalTree(); - I = I.filter(R - ? i => finite(X[i]) && positive(R[i]) - : i => finite(X[i])); + I = I.filter(R ? i => finite(X[i]) && positive(R[i]) : i => finite(X[i])); for (const i of I) { const intervals = []; const l = X[i] - radius(i); diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js index bc4c2c304d..c35bcb7dfb 100644 --- a/src/transforms/hexbin.js +++ b/src/transforms/hexbin.js @@ -36,6 +36,8 @@ export function hexbin(outputs = {fill: "count"}, inputs = {}) { // Coerce the X and Y channels to numbers (so that null is properly treated // as an undefined value rather than being coerced to zero). + // X = coerceNumbers(valueof(X.value, X.scale !== undefined ? scales[X.scale] : identity)); + // Y = coerceNumbers(valueof(Y.value, Y.scale !== undefined ? scales[Y.scale] : identity)); X = coerceNumbers(valueof(X.value, X.scale !== undefined ? scales[X.scale] : identity)); Y = coerceNumbers(valueof(Y.value, Y.scale !== undefined ? scales[Y.scale] : identity)); From eb28af83406ed0f1203193e7b6c468cfe000f6df Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 22:31:55 -0700 Subject: [PATCH 27/36] fix numeric coercion --- src/transforms/dodge.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/transforms/dodge.js b/src/transforms/dodge.js index 9701362173..26d7ef337a 100644 --- a/src/transforms/dodge.js +++ b/src/transforms/dodge.js @@ -25,7 +25,7 @@ export function dodgeX(dodgeOptions = {}, options = {}) { case "middle": anchor = anchorXMiddle; break; default: throw new Error(`unknown dodge anchor: ${anchor}`); } - return dodge("x", "y", anchor, +padding, options); + return dodge("x", "y", anchor, number(padding), options); } export function dodgeY(dodgeOptions = {}, options = {}) { @@ -37,7 +37,7 @@ export function dodgeY(dodgeOptions = {}, options = {}) { case "middle": anchor = anchorYMiddle; break; default: throw new Error(`unknown dodge anchor: ${anchor}`); } - return dodge("y", "x", anchor, +padding, options); + return dodge("y", "x", anchor, number(padding), options); } function dodge(y, x, anchor, padding, options) { From f3919cbad78f74ddca4c727e51a4dd3debe408ad Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 22:32:48 -0700 Subject: [PATCH 28/36] remove comment --- src/transforms/hexbin.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js index c35bcb7dfb..bc4c2c304d 100644 --- a/src/transforms/hexbin.js +++ b/src/transforms/hexbin.js @@ -36,8 +36,6 @@ export function hexbin(outputs = {fill: "count"}, inputs = {}) { // Coerce the X and Y channels to numbers (so that null is properly treated // as an undefined value rather than being coerced to zero). - // X = coerceNumbers(valueof(X.value, X.scale !== undefined ? scales[X.scale] : identity)); - // Y = coerceNumbers(valueof(Y.value, Y.scale !== undefined ? scales[Y.scale] : identity)); X = coerceNumbers(valueof(X.value, X.scale !== undefined ? scales[X.scale] : identity)); Y = coerceNumbers(valueof(Y.value, Y.scale !== undefined ? scales[Y.scale] : identity)); From 1d82f2829c87b5e134209b2f991f3dca80f52acb Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 22:46:24 -0700 Subject: [PATCH 29/36] initializers --- README.md | 8 ++++---- src/index.js | 2 +- src/plot.js | 6 +++--- src/transforms/basic.js | 2 +- src/transforms/dodge.js | 4 ++-- src/transforms/hexbin.js | 4 ++-- src/transforms/{initialize.js => initializer.js} | 6 +++--- test/transforms/remap.js | 4 ++-- 8 files changed, 18 insertions(+), 18 deletions(-) rename src/transforms/{initialize.js => initializer.js} (79%) diff --git a/README.md b/README.md index 6e7fb49c5d..4fc138e6f8 100644 --- a/README.md +++ b/README.md @@ -2153,9 +2153,9 @@ This helper for constructing derived columns returns a [*column*, *setColumn*] a Plot.column is typically used by options transforms to define new channels; the associated columns are populated (derived) when the **transform** option function is invoked. -## Scale-aware transforms +## Initializers -Some transforms need to operate in visual representation space such as pixel coordinates and colors rather than abstract data space. Such a transform might, for example, modify the marks’ positions to avoid occlusion. These scale-aware transforms are applied *after* the initial scales are constructed and can modify the channels or derive new channels; these in turn may (or may not, as desired) be passed to scales. +Initializers can be used to transform and derive new channels prior to rendering. Unlike transforms which operate in abstract data space, initializers can operate in screen space such as pixel coordinates and colors. For example, initializers can modify a marks’ positions to avoid occlusion. Initializers are invoked *after* the initial scales are constructed and can modify the channels or derive new channels; these in turn may (or may not, as desired) be passed to scales. ### Dodge @@ -2215,9 +2215,9 @@ When the hexbin transform has an *r* output, the bins are returned in decreasing See also the [hexgrid](#hexgrid) mark. -### Custom scale-aware transforms +### Custom initializers -When its *options* have an *initialize* property, the initialize function is called after the scales have been computed. It receives as inputs the (possibly transformed) data array, the index of elements of this array that belong to each facet, the input channels (as a key: array object), the scales, and the dimensions, with the mark as this. It must return the data, index, and the channels that need to be scaled in a second pass. +You can specify a custom initializer by specifying a function as the mark *initializer* option. This function is called after the scales have been computed, and receives as inputs the (possibly transformed) array of *data*, the *facets* index of elements of this array that belong to each facet, the input *channels* (as an object of named channels), the *scales*, and the *dimensions*. The mark itself is the *this* context. The initializer function must return an object with *data*, *facets*, and new *channels*. Any new channels are merged with existing channels, replacing channels of the same name. ## Curves diff --git a/src/index.js b/src/index.js index 70470832b4..fb0d9d2214 100644 --- a/src/index.js +++ b/src/index.js @@ -22,7 +22,7 @@ export {bin, binX, binY} from "./transforms/bin.js"; export {dodgeX, dodgeY} from "./transforms/dodge.js"; export {group, groupX, groupY, groupZ} from "./transforms/group.js"; export {hexbin} from "./transforms/hexbin.js"; -export {initialize} from "./transforms/initialize.js"; +export {initializer} from "./transforms/initializer.js"; export {normalize, normalizeX, normalizeY} from "./transforms/normalize.js"; export {map, mapX, mapY} from "./transforms/map.js"; export {window, windowX, windowY} from "./transforms/window.js"; diff --git a/src/plot.js b/src/plot.js index b3f2d7b1dd..6eb75cab65 100644 --- a/src/plot.js +++ b/src/plot.js @@ -98,8 +98,8 @@ export function plot(options = {}) { // Reinitialize; for deriving channels dependent on other channels. const newByScale = new Set(); for (const [mark, state] of stateByMark) { - if (mark.reinitialize != null) { - const {facets, channels} = mark.reinitialize(state.data, state.facets, state.channels, scales, subdimensions); + if (mark.initializer != null) { + const {facets, channels} = mark.initializer(state.data, state.facets, state.channels, scales, subdimensions); if (facets !== undefined) state.facets = facets; if (channels !== undefined) { inferChannelScale(channels, mark); @@ -250,7 +250,7 @@ export class Mark { const {facet = "auto", sort, dx, dy, clip} = options; const names = new Set(); this.data = data; - this.reinitialize = options.initialize; + this.initializer = options.initializer; this.sort = isOptions(sort) ? sort : null; this.facet = facet == null || facet === false ? null : keyword(facet === true ? "include" : facet, "facet", ["auto", "include", "exclude"]); const {transform} = basic(options); diff --git a/src/transforms/basic.js b/src/transforms/basic.js index e3271e0cce..5fc849b5ce 100644 --- a/src/transforms/basic.js +++ b/src/transforms/basic.js @@ -9,7 +9,7 @@ export function basic({ sort: s1, reverse: r1, transform: t1, - initialize: i1, + initializer: i1, ...options } = {}, t2) { if (t1 === undefined) { // explicit transform overrides filter, sort, and reverse diff --git a/src/transforms/dodge.js b/src/transforms/dodge.js index 26d7ef337a..e711ed42fe 100644 --- a/src/transforms/dodge.js +++ b/src/transforms/dodge.js @@ -3,7 +3,7 @@ import IntervalTree from "interval-tree-1d"; import {finite, positive} from "../defined.js"; import {identity, number, valueof} from "../options.js"; import {coerceNumbers} from "../scales.js"; -import {initialize} from "./initialize.js"; +import {initializer} from "./initializer.js"; const anchorXLeft = ({marginLeft}) => [1, marginLeft]; const anchorXRight = ({width, marginRight}) => [-1, width - marginRight]; @@ -41,7 +41,7 @@ export function dodgeY(dodgeOptions = {}, options = {}) { } function dodge(y, x, anchor, padding, options) { - return initialize(options, function(data, facets, {[x]: X, r: R}, scales, dimensions) { + return initializer(options, function(data, facets, {[x]: X, r: R}, scales, dimensions) { if (!X) throw new Error(`missing channel: ${x}`); X = coerceNumbers(valueof(X.value, X.scale !== undefined ? scales[X.scale] : identity)); const r = R ? undefined : this.r !== undefined ? this.r : options.r !== undefined ? number(options.r) : 3; diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js index bc4c2c304d..d301041d60 100644 --- a/src/transforms/hexbin.js +++ b/src/transforms/hexbin.js @@ -3,7 +3,7 @@ import {coerceNumbers} from "../scales.js"; import {sqrt3} from "../symbols.js"; import {identity, isNoneish, number, valueof} from "../options.js"; import {hasOutput, maybeGroup, maybeOutputs, maybeSubgroup} from "./group.js"; -import {initialize} from "./initialize.js"; +import {initializer} from "./initializer.js"; // We don’t want the hexagons to align with the edges of the plot frame, as that // would cause extreme x-values (the upper bound of the default x-scale domain) @@ -30,7 +30,7 @@ export function hexbin(outputs = {fill: "count"}, inputs = {}) { if (options.symbol === undefined) options.symbol = "hexagon"; if (options.r === undefined && !hasOutput(outputs, "r")) options.r = binWidth / 2; - return initialize(options, (data, facets, {x: X, y: Y, z: Z, fill: F, stroke: S, symbol: Q}, scales) => { + return initializer(options, (data, facets, {x: X, y: Y, z: Z, fill: F, stroke: S, symbol: Q}, scales) => { if (X === undefined) throw new Error("missing channel: x"); if (Y === undefined) throw new Error("missing channel: y"); diff --git a/src/transforms/initialize.js b/src/transforms/initializer.js similarity index 79% rename from src/transforms/initialize.js rename to src/transforms/initializer.js index a89a8cfdca..8fe9127153 100644 --- a/src/transforms/initialize.js +++ b/src/transforms/initializer.js @@ -1,13 +1,13 @@ // If both i1 and i2 are defined, returns a composite initializer that first // applies i1 and then applies i2. -export function initialize({initialize: i1, ...options} = {}, i2) { +export function initializer({initializer: i1, ...options} = {}, i2) { return { ...options, - initialize: composeInitialize(i1, i2) + initializer: composeInitializer(i1, i2) }; } -function composeInitialize(i1, i2) { +function composeInitializer(i1, i2) { if (i1 == null) return i2 === null ? undefined : i2; if (i2 == null) return i1 === null ? undefined : i1; return function(data, facets, channels, scales, dimensions) { diff --git a/test/transforms/remap.js b/test/transforms/remap.js index 7658d21250..b208dccd9d 100644 --- a/test/transforms/remap.js +++ b/test/transforms/remap.js @@ -1,7 +1,7 @@ -import {initialize} from "@observablehq/plot"; +import {initializer} from "@observablehq/plot"; export function remap(outputs = {}, options) { - return initialize(options, (data, facets, channels, scales) => { + return initializer(options, (data, facets, channels, scales) => { return { data, facets, From 9e8f5365afffa63ad6570e46d255f59f1de0ba9f Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Fri, 27 May 2022 22:50:41 -0700 Subject: [PATCH 30/36] Update README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4fc138e6f8..0b3f7d863c 100644 --- a/README.md +++ b/README.md @@ -2159,7 +2159,7 @@ Initializers can be used to transform and derive new channels prior to rendering ### Dodge -The dodge transform can be applied to any mark that consumes *x* or *y* channels, such as the [dot](#dot), [image](#image), [text](#text), and [vector](#vector) marks. The dodge transforms accept the following options: +[Source](./src/transforms/dodge.js) · [Examples](https://observablehq.com/@observablehq/plot-dodge) · The dodge transform can be applied to any mark that consumes *x* or *y* channels, such as the [dot](#dot), [image](#image), [text](#text), and [vector](#vector) marks. The dodge transforms accept the following options: * **padding** — a number of pixels added to the radius of the mark to estimate its size * **anchor** - the dodge anchor; defaults to *left* for dodgeX, or *bottom* for dodgeY @@ -2184,11 +2184,11 @@ Equivalent to Plot.dodgeY, but piling horizontally, creating a new *x* position ### Hexbin -The hexbin transform can be applied to any mark that consumes *x* and *y*, such as the [dot](#dot), [image](#image), [text](#text), and [vector](#vector) marks. It aggregates values into hexagonal bins of the given **binWidth** (in pixels) and computes new position channels *x* and *y* as the centers of each bin. It can also create new channels by applying a specified reducer to each bin, such as the *count* of elements in the bin. +[Source](./src/transforms/hexbin.js) · [Examples](https://observablehq.com/@observablehq/plot-hexbin) · The hexbin transform can be applied to any mark that consumes *x* and *y*, such as the [dot](#dot), [image](#image), [text](#text), and [vector](#vector) marks. It aggregates values into hexagonal bins of the given **binWidth** (in pixels) and computes new position channels *x* and *y* as the centers of each bin. It can also create new channels by applying a specified reducer to each bin, such as the *count* of elements in the bin. #### Plot.hexbin(*outputs*, *options*) -[Source](./src/transforms/hexbin.js) · [Examples](https://observablehq.com/@observablehq/plot-hexbin) · Aggregates the given input channels into hexagonal bins, creating output channels with the reduced data. The *options* must specify the **x** and **y** channels and can optionally indicate the **binWidth** (defaults to 20), defined as the distance between the centers of two neighboring hexagons in pixels. If any of **z**, **fill**, or **stroke** is a channel, the first of these channels will be used to subdivide bins. The *outputs* options are similar to the [bin transform](#bin); each output channel receives as input, for each hexagon, the subset of the data which has been matched to its center. The outputs object specifies the aggregation method for each output channel. +Aggregates the given input channels into hexagonal bins, creating output channels with the reduced data. The *options* must specify the **x** and **y** channels. The **binWidth** option (default 20) defines the distance between centers of neighboring hexagons in pixels. If any of **z**, **fill**, or **stroke** is a channel, the first of these channels will be used to subdivide bins. The *outputs* options are similar to the [bin transform](#bin); each output channel receives as input, for each hexagon, the subset of the data which has been matched to its center. The outputs object specifies the aggregation method for each output channel. The following aggregation methods are supported: @@ -2211,7 +2211,7 @@ The following aggregation methods are supported: * a function to be passed the array of values for each bin and the extent of the bin * an object with a *reduce* method -When the hexbin transform has an *r* output, the bins are returned in decreasing size order. +When the hexbin transform has an *r* output channel, bins are returned in order of descending radius. See also the [hexgrid](#hexgrid) mark. From 6f0f10d36864c14b1a7819ee84c653d807f4157b Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 28 May 2022 08:27:29 -0700 Subject: [PATCH 31/36] preserve this with composed transforms --- src/transforms/basic.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/transforms/basic.js b/src/transforms/basic.js index 5fc849b5ce..61a61730ac 100644 --- a/src/transforms/basic.js +++ b/src/transforms/basic.js @@ -28,9 +28,9 @@ export function basic({ function composeTransform(t1, t2) { if (t1 == null) return t2 === null ? undefined : t2; if (t2 == null) return t1 === null ? undefined : t1; - return (data, facets) => { - ({data, facets} = t1(data, facets)); - return t2(arrayify(data), facets); + return function(data, facets) { + ({data, facets} = t1.call(this, data, facets)); + return t2.call(this, arrayify(data), facets); }; } From 4e8230bef8d8c3cfbe2a7b0f214c06af4fc59832 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 28 May 2022 08:27:55 -0700 Subject: [PATCH 32/36] no default sort for hexbin --- src/transforms/hexbin.js | 10 - test/output/hexbin.svg | 168 +++++++-------- test/output/hexbinR.html | 392 +++++++++++++++++----------------- test/output/hexbinSymbol.html | 146 ++++++------- test/output/hexbinZ.html | 154 ++++++------- test/output/hexbinZNull.svg | 168 +++++++-------- 6 files changed, 514 insertions(+), 524 deletions(-) diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js index d301041d60..0550044aaf 100644 --- a/src/transforms/hexbin.js +++ b/src/transforms/hexbin.js @@ -1,4 +1,3 @@ -import {descendingDefined} from "../defined.js"; import {coerceNumbers} from "../scales.js"; import {sqrt3} from "../symbols.js"; import {identity, isNoneish, number, valueof} from "../options.js"; @@ -90,15 +89,6 @@ export function hexbin(outputs = {fill: "count"}, inputs = {}) { ...Object.fromEntries(outputs.map(({name, output}) => [name, {scale: true, radius: name === "r" ? binWidth / 2 : undefined, value: output.transform()}])) }; - // When producing a radius channel, implicitly sort by descending radius. - // TODO This should be configurable somehow. - if ("r" in channels) { - const R = channels.r.value; - for (const binFacet of binFacets) { - binFacet.sort((i, j) => descendingDefined(R[i], R[j])); - } - } - return {data, facets: binFacets, channels}; }); } diff --git a/test/output/hexbin.svg b/test/output/hexbin.svg index ca403822cc..7072850287 100644 --- a/test/output/hexbin.svg +++ b/test/output/hexbin.svg @@ -88,191 +88,191 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/output/hexbinR.html b/test/output/hexbinR.html index 6d10c759ea..be583f3ae8 100644 --- a/test/output/hexbinR.html +++ b/test/output/hexbinR.html @@ -116,128 +116,71 @@ - - 8 - - - 8 - 7 - - 7 - - - 7 - - - 6 - - - 6 + + 1 - - 6 + + 2 5 - - 5 - - - 5 - - - 5 - - - 5 - - - 4 - - - 4 + + 2 3 - - 3 - - - 3 - - - 3 - - - 3 + + 1 - - 3 + + 1 - - 2 + + 5 - - 2 + + 1 2 - - 2 - - - 2 - - - 2 - - - 2 - - - 2 + + 4 - + 2 - - 2 + + 6 - - 2 + + 1 - - 2 + + 6 - + 2 - + 2 - - 1 - - - 1 - - - 1 + + 3 - + 1 - - 1 + + 7 - - 1 + + 3 1 @@ -257,12 +200,30 @@ 1 + + 5 + + + 3 + 1 + + 5 + 1 + + 2 + + + 2 + + + 2 + 1 @@ -272,9 +233,15 @@ 1 + + 2 + 1 + + 2 + 1 @@ -290,18 +257,51 @@ 1 + + 2 + + + 6 + + + 8 + + + 4 + 1 + + 8 + + + 5 + 1 + + 3 + 1 + + 7 + 1 + + 2 + + + 3 + + + 2 + 1 @@ -319,120 +319,18 @@ - - 11 - - - 10 - - - 9 - - - 8 - - - 6 - - - 6 - 5 - - 5 - - - 4 - - - 4 + + 1 3 - - 3 - - - 3 - - - 3 - - - 3 - - - 3 - - - 3 - - - 3 - - - 3 - - - 3 - - - 3 - - - 3 - - - 3 - 2 - - 2 - - - 2 - - - 2 - - - 2 - - - 2 - - - 2 - - - 2 - - - 2 - - - 2 - - - 2 - - - 2 - - - 2 - - - 2 - - - 1 - 1 @@ -442,15 +340,36 @@ 1 + + 2 + + + 3 + 1 + + 10 + + + 9 + + + 3 + + + 2 + 1 1 + + 6 + 1 @@ -460,12 +379,24 @@ 1 + + 3 + + + 3 + 1 1 + + 3 + + + 2 + 1 @@ -475,30 +406,90 @@ 1 + + 2 + + + 3 + + + 8 + + + 3 + + + 2 + + + 2 + 1 + + 4 + + + 2 + 1 1 + + 3 + 1 + + 2 + 1 + + 11 + + + 6 + + + 3 + + + 5 + + + 4 + 1 + + 2 + 1 + + 2 + 1 + + 3 + + + 3 + + + 2 + 1 @@ -511,15 +502,24 @@ 1 + + 3 + 1 + + 2 + 1 1 + + 2 + 1 @@ -534,15 +534,15 @@ - - 2 - 1 1 + + 2 + 1 diff --git a/test/output/hexbinSymbol.html b/test/output/hexbinSymbol.html index 82439318dd..4badaf42c4 100644 --- a/test/output/hexbinSymbol.html +++ b/test/output/hexbinSymbol.html @@ -146,113 +146,60 @@ culmen_depth_mm → - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + @@ -272,16 +219,30 @@ + + + + + + + + + + + + + + @@ -290,31 +251,46 @@ + + + + + + + + + + + + + + + @@ -322,38 +298,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/output/hexbinZ.html b/test/output/hexbinZ.html index 588aba446e..2ed1230a13 100644 --- a/test/output/hexbinZ.html +++ b/test/output/hexbinZ.html @@ -95,106 +95,60 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + @@ -202,10 +156,12 @@ + + @@ -213,26 +169,40 @@ + + + + + + + + + + + + + + @@ -243,29 +213,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/output/hexbinZNull.svg b/test/output/hexbinZNull.svg index 90a829fc41..0b15f2e56f 100644 --- a/test/output/hexbinZNull.svg +++ b/test/output/hexbinZNull.svg @@ -88,114 +88,59 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -207,12 +152,18 @@ + + + + + + @@ -220,17 +171,30 @@ + + + + + + + + + + + + + @@ -238,8 +202,12 @@ + + + + @@ -248,26 +216,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From fa305907c2336cafc2742c283e3afe211de549d1 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 28 May 2022 09:51:12 -0700 Subject: [PATCH 33/36] channel sorting; default sort by descending r --- src/channel.js | 19 +- src/marks/dot.js | 2 +- src/plot.js | 8 +- src/transforms/initializer.js | 2 +- test/output/aaplChangeVolume.svg | 2328 ++++++++++---------- test/output/carsMpg.svg | 200 +- test/output/diamondsCaratPriceDots.svg | 2748 ++++++++++++------------ 7 files changed, 2661 insertions(+), 2646 deletions(-) diff --git a/src/channel.js b/src/channel.js index f11684847b..9f013238cd 100644 --- a/src/channel.js +++ b/src/channel.js @@ -1,7 +1,9 @@ import {ascending, descending, rollup, sort} from "d3"; +import {ascendingDefined, descendingDefined} from "./defined.js"; import {first, labelof, map, maybeValue, range, valueof} from "./options.js"; import {registry} from "./scales/index.js"; import {maybeReduce} from "./transforms/group.js"; +import {composeInitializer} from "./transforms/initializer.js"; // TODO Type coercion? export function Channel(data, {scale, type, value, filter, hint}) { @@ -37,10 +39,10 @@ export function valueObject(channels, scales) { // Note: mutates channel.domain! This is set to a function so that it is lazily // computed; i.e., if the scale’s domain is set explicitly, that takes priority // over the sort option, and we don’t need to do additional work. -export function channelSort(channels, facetChannels, data, options) { +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 + if (!registry.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 @@ -71,6 +73,19 @@ export function channelSort(channels, facetChannels, data, options) { } } +function sortInitializer(name, compare = ascendingDefined) { + return (data, facets, {[name]: V}) => { + if (!V) throw new Error(`missing channel: ${name}`); + V = V.value; + const compareValue = (i, j) => compare(V[i], V[j]); + return {facets: facets.map(I => I.slice().sort(compareValue))}; + }; +} + +export function channelSort(initializer, {channel, reverse}) { + return composeInitializer(initializer, sortInitializer(channel, reverse ? descendingDefined : ascendingDefined)); +} + function findScaleChannel(channels, scale) { for (const name in channels) { const channel = channels[name]; diff --git a/src/marks/dot.js b/src/marks/dot.js index aee2ec734f..5d87b11370 100644 --- a/src/marks/dot.js +++ b/src/marks/dot.js @@ -27,7 +27,7 @@ export class Dot extends Mark { {name: "rotate", value: vrotate, optional: true}, {name: "symbol", value: vsymbol, scale: "symbol", optional: true} ], - options, + vr === undefined || options.sort !== undefined ? options : {...options, sort: {channel: "r", reverse: true}}, defaults ); this.r = cr; diff --git a/src/plot.js b/src/plot.js index 6eb75cab65..89f0b55f09 100644 --- a/src/plot.js +++ b/src/plot.js @@ -1,6 +1,6 @@ import {create, cross, difference, groups, InternMap, select} from "d3"; import {Axes, autoAxisTicks, autoScaleLabels} from "./axes.js"; -import {Channel, channelObject, channelSort, valueObject} from "./channel.js"; +import {Channel, channelObject, channelDomain, channelSort, valueObject} from "./channel.js"; import {defined} from "./defined.js"; import {Dimensions} from "./dimensions.js"; import {Legends, exposeLegends} from "./legends.js"; @@ -247,11 +247,11 @@ export function plot(options = {}) { export class Mark { constructor(data, channels = [], options = {}, defaults) { - const {facet = "auto", sort, dx, dy, clip} = options; + const {facet = "auto", sort, dx, dy, clip, initializer} = options; const names = new Set(); this.data = data; - this.initializer = options.initializer; this.sort = isOptions(sort) ? sort : null; + this.initializer = this.sort?.channel == null ? initializer : channelSort(initializer, this.sort); this.facet = facet == null || facet === false ? null : keyword(facet === true ? "include" : facet, "facet", ["auto", "include", "exclude"]); const {transform} = basic(options); this.transform = transform; @@ -278,7 +278,7 @@ export class Mark { if (facets === undefined && data != null) facets = [range(data)]; if (this.transform != null) ({facets, data} = this.transform(data, facets)), data = arrayify(data); const channels = channelObject(this.channels, data); - if (this.sort != null) channelSort(channels, facetChannels, data, this.sort); + if (this.sort != null) channelDomain(channels, facetChannels, data, this.sort); return {data, facets, channels}; } filter(index, channels, values) { diff --git a/src/transforms/initializer.js b/src/transforms/initializer.js index 8fe9127153..f5646d2808 100644 --- a/src/transforms/initializer.js +++ b/src/transforms/initializer.js @@ -7,7 +7,7 @@ export function initializer({initializer: i1, ...options} = {}, i2) { }; } -function composeInitializer(i1, i2) { +export function composeInitializer(i1, i2) { if (i1 == null) return i2 === null ? undefined : i2; if (i2 == null) return i1 === null ? undefined : i1; return function(data, facets, channels, scales, dimensions) { diff --git a/test/output/aaplChangeVolume.svg b/test/output/aaplChangeVolume.svg index 7055f1a5eb..c578b5d48c 100644 --- a/test/output/aaplChangeVolume.svg +++ b/test/output/aaplChangeVolume.svg @@ -109,1265 +109,1265 @@ - - + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + - - - + + - + + + - - - - - - - - - + + + + + + + + + + + + + - - + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - + + - - - - + + + - - - - - - - - - - - - - - - - - - + + + + - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + - + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - + + + - - - + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - - - - - - - - - - + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + - - - + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + - - - - + - - + + + + + + + + + + + + + + + + + + + + + + + - + + + - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - + + + - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - + - - + + + + + + + + + + - - - + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + - - - - - - - + + + + + + + + + - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - + + - - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - + + + + + + + + - - + + + + + + + - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + \ No newline at end of file diff --git a/test/output/carsMpg.svg b/test/output/carsMpg.svg index 4ed068ea38..0a977f3307 100644 --- a/test/output/carsMpg.svg +++ b/test/output/carsMpg.svg @@ -104,180 +104,180 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/test/output/diamondsCaratPriceDots.svg b/test/output/diamondsCaratPriceDots.svg index 15e1c462e5..fcd86c0ab4 100644 --- a/test/output/diamondsCaratPriceDots.svg +++ b/test/output/diamondsCaratPriceDots.svg @@ -130,420 +130,1392 @@ Carats → - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -552,410 +1524,75 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -963,7 +1600,6 @@ - @@ -973,32 +1609,16 @@ - - - - - - - - - - - - - - - - @@ -1008,269 +1628,49 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1278,148 +1678,40 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1427,19 +1719,14 @@ - - - - - @@ -1450,373 +1737,127 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1825,31 +1866,17 @@ - - - - - - - - - - - - - - @@ -1860,37 +1887,19 @@ - - - - - - - - - - - - - - - - - - @@ -1900,20 +1909,17 @@ - - - @@ -1921,7 +1927,6 @@ - @@ -1933,10 +1938,6 @@ - - - - @@ -1946,7 +1947,6 @@ - From 2a9cdf0ffaec33d3a5c96fe3e8ff5172cad3b585 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 28 May 2022 10:07:04 -0700 Subject: [PATCH 34/36] =?UTF-8?q?don=E2=80=99t=20consume=20null=20sort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/transforms/basic.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/basic.js b/src/transforms/basic.js index 61a61730ac..36ce7b5886 100644 --- a/src/transforms/basic.js +++ b/src/transforms/basic.js @@ -20,7 +20,7 @@ export function basic({ if (t2 != null && i1 != null) throw new Error("transforms cannot be applied after initializers"); return { ...options, - ...isOptions(s1) && {sort: s1}, + ...(s1 == null || isOptions(s1)) && {sort: s1}, transform: composeTransform(t1, t2) }; } From e92cd419a9df49ade927434bb2d4093be96a5dae Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 28 May 2022 10:07:49 -0700 Subject: [PATCH 35/36] =?UTF-8?q?don=E2=80=99t=20consume=20null=20sort,=20?= =?UTF-8?q?strictly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/transforms/basic.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transforms/basic.js b/src/transforms/basic.js index 36ce7b5886..008e86a266 100644 --- a/src/transforms/basic.js +++ b/src/transforms/basic.js @@ -20,7 +20,7 @@ export function basic({ if (t2 != null && i1 != null) throw new Error("transforms cannot be applied after initializers"); return { ...options, - ...(s1 == null || isOptions(s1)) && {sort: s1}, + ...(s1 === null || isOptions(s1)) && {sort: s1}, transform: composeTransform(t1, t2) }; } From b83fed7cc673da0fc22edc0d34632fb2dc1bb8c0 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 28 May 2022 10:15:44 -0700 Subject: [PATCH 36/36] Update README --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0b3f7d863c..b720387d50 100644 --- a/README.md +++ b/README.md @@ -424,6 +424,8 @@ Plot.plot({ ### Sort options +#### Ordinal domain sorting + If an ordinal scale’s domain is not set, it defaults to natural ascending order; to order the domain by associated values in another dimension, either compute the domain manually (consider [d3.groupSort](https://github.com/d3/d3-array/blob/main/README.md#groupSort)) or use an associated mark’s **sort** option. For example, to sort bars by ascending frequency rather than alphabetically by letter: ```js @@ -462,6 +464,20 @@ If the input channel is *data*, then the reducer is passed groups of the mark’ Note: when the value of the sort option is a string or a function, it is interpreted as a [basic sort transform](#transforms). To use both sort options and a sort transform, use [Plot.sort](#plotsortorder-options). +#### Index sorting + +In addition to the [sort transform](#transforms) which allow sorting by data, you can use the **sort** option to sort marks by some other channel value. For example, to sort dots by descending radius: + +```js +Plot.dot(earthquakes, {x: "longitude", y: "latitude", r: "intensity", sort: {channel: "r", reverse: true}}) +``` + +In fact, sorting by descending radius is the default behavior of the dot mark when an *r* channel is specified. You can disable this by setting the sort explicitly to null: + +```js +Plot.dot(earthquakes, {x: "longitude", y: "latitude", r: "intensity", sort: null}) +``` + ### Facet options The *facet* option enables [faceting](https://observablehq.com/@observablehq/plot-facets). When faceting, two additional band scales may be configured: @@ -1459,7 +1475,7 @@ The *filter*, *sort* and *reverse* transforms are also available as functions, a Plot.sort("body_mass_g", options) // show data in ascending body mass order ``` -Sorts the data by the specified *order*, which can be an accessor function, a comparator function, or a channel value definition such as a field name. +Sorts the data by the specified *order*, which can be an accessor function, a comparator function, or a channel value definition such as a field name. See also [index sorting](#index-sorting), which allows marks to be sorted by a named channel, such as *r* for radius. #### Plot.shuffle(*options*)