diff --git a/README.md b/README.md index 2deed0c1f77..c496cbc527c 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 59947bb84d2..baeaeedf076 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,7 +51,7 @@ }, "dependencies": { "d3": "^7.3.0", - "d3-hexbin": "^0.2.2", + "interval-tree-1d": "1", "isoformat": "0.2" }, "engines": { diff --git a/rollup.config.js b/rollup.config.js index f2cded11051..ae8f8778b5a 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/index.js b/src/index.js index b7e1a755397..0c6e976309f 100644 --- a/src/index.js +++ b/src/index.js @@ -19,6 +19,7 @@ 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"; diff --git a/src/marks/hexgrid.js b/src/marks/hexgrid.js index c18483f3a0b..4fbb9f2244f 100644 --- a/src/marks/hexgrid.js +++ b/src/marks/hexgrid.js @@ -17,15 +17,15 @@ export function hexgrid(options) { } export class Hexgrid extends Mark { - constructor({radius = 10, clip = true, ...options} = {}) { + constructor({binWidth = 20, clip = true, ...options} = {}) { super(undefined, undefined, {clip, ...options}, defaults); - this.radius = number(radius); + this.binWidth = number(binWidth); } render(index, scales, channels, dimensions) { - const {dx, dy, radius: rx} = this; + 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 ry = rx * sqrt4_3, hy = ry / 2, wx = rx * 2, wy = ry * 1.5; + 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; diff --git a/src/plot.js b/src/plot.js index 7e6eea516d2..b3f2d7b1dd8 100644 --- a/src/plot.js +++ b/src/plot.js @@ -90,11 +90,16 @@ 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); + 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); @@ -148,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)); @@ -158,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); diff --git a/src/symbols.js b/src/symbols.js index f501631b4cc..e35aaec6945 100644 --- a/src/symbols.js +++ b/src/symbols.js @@ -1,7 +1,8 @@ import {symbolAsterisk, symbolDiamond2, symbolPlus, symbolSquare2, symbolTriangle2, symbolX as symbolTimes} from "d3"; import {symbolCircle, symbolCross, symbolDiamond, symbolSquare, symbolStar, symbolTriangle, symbolWye} from "d3"; -export const sqrt4_3 = 2 / Math.sqrt(3); +export const sqrt3 = Math.sqrt(3); +export const sqrt4_3 = 2 / sqrt3; const symbolHexagon = { draw(context, size) { diff --git a/src/transforms/basic.js b/src/transforms/basic.js index 2f99f3b2e31..cbf8bf5444a 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}, @@ -32,6 +34,20 @@ function composeTransform(t1, t2) { }; } +export function composeInitialize({initialize: i1, ...options} = {}, i2) { + return i1 == null + ? {...options, initialize: i2} + : { + ...options, + initialize(data, facets, channels, scales, dimensions) { + let newChannels; + ({data, facets, channels: newChannels} = i1.call(this, data, facets, channels, scales, dimensions)); + Object.assign(channels, newChannels); + return i2.call(this, data, facets, channels, scales, dimensions); + } + }; +} + export function filter(value, options) { return basic(options, filterTransform(value)); } diff --git a/src/transforms/dodge.js b/src/transforms/dodge.js new file mode 100644 index 00000000000..8847daf0971 --- /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 {composeInitialize} from "./basic.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 composeInitialize(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); +} \ No newline at end of file diff --git a/src/transforms/hexbin.js b/src/transforms/hexbin.js index 7a6c4503bc1..e1880fd3a27 100644 --- a/src/transforms/hexbin.js +++ b/src/transforms/hexbin.js @@ -1,6 +1,8 @@ -import {hexbin as Hexbin} from "d3-hexbin"; // TODO inline -import {sqrt4_3} from "../symbols.js"; +import {group} from "d3"; +import {sqrt3} from "../symbols.js"; +import {identity, maybeChannel, maybeColorChannel, valueof} from "../options.js"; import {hasOutput, maybeOutputs} from "./group.js"; +import {composeInitialize} from "./basic.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) @@ -10,52 +12,100 @@ import {hasOutput, maybeOutputs} from "./group.js"; export const ox = 0.5, oy = 0; export function hexbin(outputs = {fill: "count"}, options = {}) { - const {radius, ...rest} = outputs; - return hexbinn(rest, {radius, ...options}); + const {binWidth, ...rest} = outputs; + return hexbinn(rest, {binWidth, ...options}); } -// TODO group by (implicit) z // TODO filter e.g. to show empty hexbins? -// TODO data output with sort and reverse? // TODO disallow x, x1, x2, y, y1, y2 reducers? -function hexbinn(outputs, {radius = 10, ...options}) { - radius = +radius; - outputs = maybeOutputs(outputs, options); +function hexbinn(outputs, {binWidth = 20, fill, stroke, z, ...options}) { + binWidth = +binWidth; + const [GZ, setGZ] = maybeChannel(z); + const [vfill] = maybeColorChannel(fill); + const [vstroke] = maybeColorChannel(stroke); + const [GF = fill, setGF] = maybeChannel(vfill); + const [GS = stroke, setGS] = maybeChannel(vstroke); + outputs = maybeOutputs({ + ...setGF && {fill: "first"}, + ...setGS && {stroke: "first"}, + ...outputs + }, {fill, stroke, ...options}); return { symbol: "hexagon", - ...!hasOutput(outputs, "r") && {r: radius}, - ...hasOutput(outputs, "fill") && {stroke: "none"}, - ...options, - initialize(data, facets, {x: X, y: Y}, {x, y}) { + ...!hasOutput(outputs, "r") && {r: binWidth / 2}, + ...!setGF && {fill}, + ...((hasOutput(outputs, "fill") || setGF) && stroke === undefined) ? {stroke: "none"} : {stroke}, + ...composeInitialize(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"); - X = X.value; - Y = Y.value; - const binsof = Hexbin().x(i => x(X[i]) - ox).y(i => y(Y[i]) - oy).radius(radius * sqrt4_3); + 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 = 0; - for (const o of outputs) o.initialize(data); + let i = -1; for (const facet of facets) { const binFacet = []; for (const o of outputs) o.scope("facet", facet); - for (const bin of binsof(facet)) { - binFacet.push(i++); - BX.push(bin.x + ox); - BY.push(bin.y + oy); - for (const o of outputs) o.reduce(bin); + 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); } - return { - facets: binFacets, - channels: { - x: {value: BX}, - y: {value: BY}, - ...Object.fromEntries(outputs.map(({name, output}) => [name, {scale: true, radius: name === "r" ? radius : undefined, value: output.transform()}])) - } + 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/test/output/carsJiggle.html b/test/output/carsJiggle.html new file mode 100644 index 00000000000..e2b2d8cc828 --- /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 00000000000..2946ee7fc07 --- /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 index 11657765390..b600b89934e 100644 --- a/test/output/hexbin.svg +++ b/test/output/hexbin.svg @@ -88,195 +88,197 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/hexbinOranges.svg b/test/output/hexbinOranges.svg new file mode 100644 index 00000000000..a855e5439e6 --- /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 00000000000..be11949cf5f --- /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 00000000000..11c1d0bb82f --- /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 00000000000..0e70e704da4 --- /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 00000000000..d2de7515453 --- /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 00000000000..2292f7bb814 --- /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 00000000000..7af2735b031 --- /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 00000000000..b81b2ede606 --- /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 00000000000..8a0a5c05dfe --- /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 00000000000..0b45525c2e6 --- /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 00000000000..bf36ee90b55 --- /dev/null +++ b/test/plots/cars-jiggle.js @@ -0,0 +1,48 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; +import {composeInitialize} from "../../src/transforms/basic.js"; + +function remap(outputs = {}, options) { + return composeInitialize(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 00000000000..4e3ac16870b --- /dev/null +++ b/test/plots/darker-dodge.js @@ -0,0 +1,47 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; +import {composeInitialize} from "../../src/transforms/basic.js"; + +function remap(outputs = {}, options) { + return composeInitialize(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 00000000000..29bb84bdeba --- /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 00000000000..e63eb72ae53 --- /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 00000000000..d26da69415f --- /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 00000000000..8ab40865883 --- /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 00000000000..0ac30caff30 --- /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/index.js b/test/plots/index.js index 289ee4c5aff..12f9611a395 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"; @@ -68,6 +70,11 @@ 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"; @@ -108,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 00000000000..c2b159f77b9 --- /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 00000000000..47fc6ce8926 --- /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 00000000000..4c40151192b --- /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 00000000000..a16dcb57372 --- /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 00000000000..77ed20a433b --- /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/yarn.lock b/yarn.lock index 38fb53e9359..30f224d73be 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" @@ -536,11 +564,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 sha1-nFg32s/UcasFM3qeke8Qv8T5iDE= - d3-hierarchy@3: version "3.1.2" resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6" @@ -1044,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" @@ -1174,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== @@ -1302,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" @@ -1353,6 +1388,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" @@ -1493,6 +1535,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" @@ -1824,7 +1873,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== @@ -1979,6 +2028,11 @@ source-map@~0.7.2: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +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"