Skip to content

Commit 87a0deb

Browse files
mbostockFil
andauthored
mark initializers (#801)
* 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 42ac4f0 * 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 * update dependencies * Update README * Update README * tweak error message * Fix Plot.hexbin default reducer, and simplify (#884) * sort tests * revert inlined hexbin implementation * simpler z * simpler scale application * re-inline d3-hexbin * use descendingDefined to sort * coerce X and Y to numbers * populate radius hint * fix hexbin z; implicit group on symbol * update tests * expose initialize; rewrite remap * tweak tests * tweak tests * tweak tests * tweak tests * tweak tests * tweak tests * tweak tests * tweak tests * fix scale association, numeric coercion * fix numeric coercion * remove comment * initializers * Update README * preserve this with composed transforms * no default sort for hexbin * channel sorting; default sort by descending r * don’t consume null sort * don’t consume null sort, strictly * Update README Co-authored-by: Philippe Rivière <[email protected]>
1 parent 8757c02 commit 87a0deb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+9374
-3069
lines changed

README.md

Lines changed: 104 additions & 14 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,23 @@
3535
},
3636
"sideEffects": false,
3737
"devDependencies": {
38+
"@rollup/plugin-commonjs": "22",
3839
"@rollup/plugin-json": "4",
3940
"@rollup/plugin-node-resolve": "13",
4041
"canvas": "2",
4142
"eslint": "8",
4243
"htl": "0.3",
4344
"js-beautify": "1",
4445
"jsdom": "19",
45-
"mocha": "9",
46+
"mocha": "10",
4647
"module-alias": "2",
4748
"rollup": "2",
4849
"rollup-plugin-terser": "7",
4950
"vite": "2"
5051
},
5152
"dependencies": {
5253
"d3": "^7.3.0",
54+
"interval-tree-1d": "1",
5355
"isoformat": "0.2"
5456
},
5557
"engines": {

rollup.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from "fs";
22
import {terser} from "rollup-plugin-terser";
3+
import commonjs from "@rollup/plugin-commonjs";
34
import json from "@rollup/plugin-json";
45
import node from "@rollup/plugin-node-resolve";
56
import * as meta from "./package.json";
@@ -25,6 +26,7 @@ const config = {
2526
banner: `// ${meta.name} v${meta.version} Copyright ${copyrights.join(", ")}`
2627
},
2728
plugins: [
29+
commonjs(),
2830
json(),
2931
node()
3032
]

src/channel.js

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import {ascending, descending, rollup, sort} from "d3";
2+
import {ascendingDefined, descendingDefined} from "./defined.js";
23
import {first, labelof, map, maybeValue, range, valueof} from "./options.js";
34
import {registry} from "./scales/index.js";
45
import {maybeReduce} from "./transforms/group.js";
6+
import {composeInitializer} from "./transforms/initializer.js";
57

68
// TODO Type coercion?
79
export function Channel(data, {scale, type, value, filter, hint}) {
@@ -15,19 +17,41 @@ export function Channel(data, {scale, type, value, filter, hint}) {
1517
};
1618
}
1719

18-
export function channelSort(channels, facetChannels, data, options) {
20+
export function channelObject(channelDescriptors, data) {
21+
const channels = {};
22+
for (const channel of channelDescriptors) {
23+
channels[channel.name] = Channel(data, channel);
24+
}
25+
return channels;
26+
}
27+
28+
// TODO Use Float64Array for scales with numeric ranges, e.g. position?
29+
export function valueObject(channels, scales) {
30+
const values = {};
31+
for (const channelName in channels) {
32+
const {scale: scaleName, value} = channels[channelName];
33+
const scale = scales[scaleName];
34+
values[channelName] = scale === undefined ? value : Array.from(value, scale);
35+
}
36+
return values;
37+
}
38+
39+
// Note: mutates channel.domain! This is set to a function so that it is lazily
40+
// computed; i.e., if the scale’s domain is set explicitly, that takes priority
41+
// over the sort option, and we don’t need to do additional work.
42+
export function channelDomain(channels, facetChannels, data, options) {
1943
const {reverse: defaultReverse, reduce: defaultReduce = true, limit: defaultLimit} = options;
2044
for (const x in options) {
21-
if (!registry.has(x)) continue; // ignore unknown scale keys
45+
if (!registry.has(x)) continue; // ignore unknown scale keys (including generic options)
2246
let {value: y, reverse = defaultReverse, reduce = defaultReduce, limit = defaultLimit} = maybeValue(options[x]);
2347
if (reverse === undefined) reverse = y === "width" || y === "height"; // default to descending for lengths
2448
if (reduce == null || reduce === false) continue; // disabled reducer
25-
const X = channels.find(([, {scale}]) => scale === x) || facetChannels && facetChannels.find(([, {scale}]) => scale === x);
49+
const X = findScaleChannel(channels, x) || facetChannels && findScaleChannel(facetChannels, x);
2650
if (!X) throw new Error(`missing channel for scale: ${x}`);
27-
const XV = X[1].value;
51+
const XV = X.value;
2852
const [lo = 0, hi = Infinity] = limit && typeof limit[Symbol.iterator] === "function" ? limit : limit < 0 ? [limit] : [0, limit];
2953
if (y == null) {
30-
X[1].domain = () => {
54+
X.domain = () => {
3155
let domain = XV;
3256
if (reverse) domain = domain.slice().reverse();
3357
if (lo !== 0 || hi !== Infinity) domain = domain.slice(lo, hi);
@@ -39,7 +63,7 @@ export function channelSort(channels, facetChannels, data, options) {
3963
: y === "width" ? difference(channels, "x1", "x2")
4064
: values(channels, y, y === "y" ? "y2" : y === "x" ? "x2" : undefined);
4165
const reducer = maybeReduce(reduce === true ? "max" : reduce, YV);
42-
X[1].domain = () => {
66+
X.domain = () => {
4367
let domain = rollup(range(XV), I => reducer.reduce(I, YV), i => XV[i]);
4468
domain = sort(domain, reverse ? descendingGroup : ascendingGroup);
4569
if (lo !== 0 || hi !== Infinity) domain = domain.slice(lo, hi);
@@ -49,16 +73,36 @@ export function channelSort(channels, facetChannels, data, options) {
4973
}
5074
}
5175

76+
function sortInitializer(name, compare = ascendingDefined) {
77+
return (data, facets, {[name]: V}) => {
78+
if (!V) throw new Error(`missing channel: ${name}`);
79+
V = V.value;
80+
const compareValue = (i, j) => compare(V[i], V[j]);
81+
return {facets: facets.map(I => I.slice().sort(compareValue))};
82+
};
83+
}
84+
85+
export function channelSort(initializer, {channel, reverse}) {
86+
return composeInitializer(initializer, sortInitializer(channel, reverse ? descendingDefined : ascendingDefined));
87+
}
88+
89+
function findScaleChannel(channels, scale) {
90+
for (const name in channels) {
91+
const channel = channels[name];
92+
if (channel.scale === scale) return channel;
93+
}
94+
}
95+
5296
function difference(channels, k1, k2) {
5397
const X1 = values(channels, k1);
5498
const X2 = values(channels, k2);
5599
return map(X2, (x2, i) => Math.abs(x2 - X1[i]), Float64Array);
56100
}
57101

58102
function values(channels, name, alias) {
59-
let channel = channels.find(([n]) => n === name);
60-
if (!channel && alias !== undefined) channel = channels.find(([n]) => n === alias);
61-
if (channel) return channel[1].value;
103+
let channel = channels[name];
104+
if (!channel && alias !== undefined) channel = channels[alias];
105+
if (channel) return channel.value;
62106
throw new Error(`missing channel: ${name}`);
63107
}
64108

src/defined.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,3 @@ export function positive(x) {
2727
export function negative(x) {
2828
return x < 0 && isFinite(x) ? x : NaN;
2929
}
30-
31-
export function firstof(...values) {
32-
for (const v of values) {
33-
if (v !== undefined) {
34-
return v;
35-
}
36-
}
37-
}

src/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ export {Arrow, arrow} from "./marks/arrow.js";
44
export {BarX, BarY, barX, barY} from "./marks/bar.js";
55
export {boxX, boxY} from "./marks/box.js";
66
export {Cell, cell, cellX, cellY} from "./marks/cell.js";
7-
export {Dot, dot, dotX, dotY} from "./marks/dot.js";
7+
export {Dot, dot, dotX, dotY, circle, hexagon} from "./marks/dot.js";
88
export {Frame, frame} from "./marks/frame.js";
9+
export {Hexgrid, hexgrid} from "./marks/hexgrid.js";
910
export {Image, image} from "./marks/image.js";
1011
export {Line, line, lineX, lineY} from "./marks/line.js";
1112
export {Link, link} from "./marks/link.js";
@@ -18,7 +19,10 @@ export {Vector, vector, vectorX, vectorY} from "./marks/vector.js";
1819
export {valueof, column} from "./options.js";
1920
export {filter, reverse, sort, shuffle, basic as transform} from "./transforms/basic.js";
2021
export {bin, binX, binY} from "./transforms/bin.js";
22+
export {dodgeX, dodgeY} from "./transforms/dodge.js";
2123
export {group, groupX, groupY, groupZ} from "./transforms/group.js";
24+
export {hexbin} from "./transforms/hexbin.js";
25+
export {initializer} from "./transforms/initializer.js";
2226
export {normalize, normalizeX, normalizeY} from "./transforms/normalize.js";
2327
export {map, mapX, mapY} from "./transforms/map.js";
2428
export {window, windowX, windowY} from "./transforms/window.js";

src/marks/dot.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import {create, path, symbolCircle} from "d3";
22
import {positive} from "../defined.js";
3-
import {identity, maybeFrameAnchor, maybeNumberChannel, maybeSymbolChannel, maybeTuple} from "../options.js";
3+
import {identity, maybeFrameAnchor, maybeNumberChannel, maybeTuple} from "../options.js";
44
import {Mark} from "../plot.js";
55
import {applyChannelStyles, applyDirectStyles, applyFrameAnchor, applyIndirectStyles, applyTransform, offset} from "../style.js";
6+
import {maybeSymbolChannel} from "../symbols.js";
67

78
const defaults = {
89
ariaLabel: "dot",
@@ -26,7 +27,7 @@ export class Dot extends Mark {
2627
{name: "rotate", value: vrotate, optional: true},
2728
{name: "symbol", value: vsymbol, scale: "symbol", optional: true}
2829
],
29-
options,
30+
vr === undefined || options.sort !== undefined ? options : {...options, sort: {channel: "r", reverse: true}},
3031
defaults
3132
);
3233
this.r = cr;
@@ -100,3 +101,11 @@ export function dotX(data, {x = identity, ...options} = {}) {
100101
export function dotY(data, {y = identity, ...options} = {}) {
101102
return new Dot(data, {...options, y});
102103
}
104+
105+
export function circle(data, options) {
106+
return dot(data, {...options, symbol: "circle"});
107+
}
108+
109+
export function hexagon(data, options) {
110+
return dot(data, {...options, symbol: "hexagon"});
111+
}

src/marks/hexgrid.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {create} from "d3";
2+
import {Mark} from "../plot.js";
3+
import {number} from "../options.js";
4+
import {applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js";
5+
import {sqrt4_3} from "../symbols.js";
6+
import {ox, oy} from "../transforms/hexbin.js";
7+
8+
const defaults = {
9+
ariaLabel: "hexgrid",
10+
fill: "none",
11+
stroke: "currentColor",
12+
strokeOpacity: 0.1
13+
};
14+
15+
export function hexgrid(options) {
16+
return new Hexgrid(options);
17+
}
18+
19+
export class Hexgrid extends Mark {
20+
constructor({binWidth = 20, clip = true, ...options} = {}) {
21+
super(undefined, undefined, {clip, ...options}, defaults);
22+
this.binWidth = number(binWidth);
23+
}
24+
render(index, scales, channels, dimensions) {
25+
const {dx, dy, binWidth} = this;
26+
const {marginTop, marginRight, marginBottom, marginLeft, width, height} = dimensions;
27+
const x0 = marginLeft - ox, x1 = width - marginRight - ox, y0 = marginTop - oy, y1 = height - marginBottom - oy;
28+
const rx = binWidth / 2, ry = rx * sqrt4_3, hy = ry / 2, wx = rx * 2, wy = ry * 1.5;
29+
const path = `m0,${-ry}l${rx},${hy}v${ry}l${-rx},${hy}`;
30+
const i0 = Math.floor(x0 / wx), i1 = Math.ceil(x1 / wx);
31+
const j0 = Math.floor((y0 + hy) / wy), j1 = Math.ceil((y1 - hy) / wy) + 1;
32+
const m = [];
33+
for (let j = j0; j < j1; ++j) {
34+
for (let i = i0; i < i1; ++i) {
35+
m.push(`M${i * wx + (j & 1) * rx},${j * wy}${path}`);
36+
}
37+
}
38+
return create("svg:g")
39+
.call(applyIndirectStyles, this, dimensions)
40+
.call(g => g.append("path")
41+
.call(applyDirectStyles, this)
42+
.call(applyTransform, null, null, offset + dx + ox, offset + dy + oy)
43+
.attr("d", m.join("")))
44+
.node();
45+
}
46+
}

src/options.js

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import {parse as isoParse} from "isoformat";
22
import {color, descending, quantile} from "d3";
3-
import {symbolAsterisk, symbolDiamond2, symbolPlus, symbolSquare2, symbolTriangle2, symbolX as symbolTimes} from "d3";
4-
import {symbolCircle, symbolCross, symbolDiamond, symbolSquare, symbolStar, symbolTriangle, symbolWye} from "d3";
53

64
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
75
const TypedArray = Object.getPrototypeOf(Uint8Array);
@@ -23,6 +21,7 @@ export const indexOf = (d, i) => i;
2321
export const identity = {transform: d => d};
2422
export const zero = () => 0;
2523
export const one = () => 1;
24+
export const yes = () => true;
2625
export const string = x => x == null ? x : `${x}`;
2726
export const number = x => x == null ? x : +x;
2827
export const boolean = x => x == null ? x : !!x;
@@ -319,48 +318,6 @@ export function isRound(value) {
319318
return /^\s*round\s*$/i.test(value);
320319
}
321320

322-
const symbols = new Map([
323-
["asterisk", symbolAsterisk],
324-
["circle", symbolCircle],
325-
["cross", symbolCross],
326-
["diamond", symbolDiamond],
327-
["diamond2", symbolDiamond2],
328-
["plus", symbolPlus],
329-
["square", symbolSquare],
330-
["square2", symbolSquare2],
331-
["star", symbolStar],
332-
["times", symbolTimes],
333-
["triangle", symbolTriangle],
334-
["triangle2", symbolTriangle2],
335-
["wye", symbolWye]
336-
]);
337-
338-
function isSymbolObject(value) {
339-
return value && typeof value.draw === "function";
340-
}
341-
342-
export function isSymbol(value) {
343-
if (isSymbolObject(value)) return true;
344-
if (typeof value !== "string") return false;
345-
return symbols.has(value.toLowerCase());
346-
}
347-
348-
export function maybeSymbol(symbol) {
349-
if (symbol == null || isSymbolObject(symbol)) return symbol;
350-
const value = symbols.get(`${symbol}`.toLowerCase());
351-
if (value) return value;
352-
throw new Error(`invalid symbol: ${symbol}`);
353-
}
354-
355-
export function maybeSymbolChannel(symbol) {
356-
if (symbol == null || isSymbolObject(symbol)) return [undefined, symbol];
357-
if (typeof symbol === "string") {
358-
const value = symbols.get(`${symbol}`.toLowerCase());
359-
if (value) return [undefined, value];
360-
}
361-
return [symbol, undefined];
362-
}
363-
364321
export function maybeFrameAnchor(value = "middle") {
365322
return keyword(value, "frameAnchor", ["middle", "top-left", "top", "top-right", "right", "bottom-right", "bottom", "bottom-left", "left"]);
366323
}

0 commit comments

Comments
 (0)