Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mark initializers #801

Merged
merged 36 commits into from
May 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d95c618
mark initializers
mbostock Mar 10, 2022
5aac780
update dependencies
mbostock May 26, 2022
8608981
Update README
mbostock May 26, 2022
6db2ebd
Update README
mbostock May 26, 2022
5460c58
tweak error message
mbostock May 26, 2022
027cdeb
Fix Plot.hexbin default reducer, and simplify (#884)
Fil May 27, 2022
e362ff3
sort tests
mbostock May 27, 2022
156c2a2
revert inlined hexbin implementation
mbostock May 27, 2022
2dc40c0
simpler z
mbostock May 28, 2022
336704c
simpler scale application
mbostock May 28, 2022
22d5183
re-inline d3-hexbin
mbostock May 28, 2022
fc2c185
use descendingDefined to sort
mbostock May 28, 2022
2c576e2
coerce X and Y to numbers
mbostock May 28, 2022
1ad8801
populate radius hint
mbostock May 28, 2022
3e6903c
fix hexbin z; implicit group on symbol
mbostock May 28, 2022
5f116e9
update tests
mbostock May 28, 2022
bb3d455
expose initialize; rewrite remap
mbostock May 28, 2022
0d19e44
tweak tests
mbostock May 28, 2022
f43d08d
tweak tests
mbostock May 28, 2022
d4a286b
tweak tests
mbostock May 28, 2022
d012930
tweak tests
mbostock May 28, 2022
7c4fc40
tweak tests
mbostock May 28, 2022
cb37aeb
tweak tests
mbostock May 28, 2022
cda1bda
tweak tests
mbostock May 28, 2022
02d01ae
tweak tests
mbostock May 28, 2022
a7718cf
fix scale association, numeric coercion
mbostock May 28, 2022
eb28af8
fix numeric coercion
mbostock May 28, 2022
f3919cb
remove comment
mbostock May 28, 2022
1d82f28
initializers
mbostock May 28, 2022
9e8f536
Update README
mbostock May 28, 2022
6f0f10d
preserve this with composed transforms
mbostock May 28, 2022
4e8230b
no default sort for hexbin
mbostock May 28, 2022
fa30590
channel sorting; default sort by descending r
mbostock May 28, 2022
2a9cdf0
don’t consume null sort
mbostock May 28, 2022
e92cd41
don’t consume null sort, strictly
mbostock May 28, 2022
b83fed7
Update README
mbostock May 28, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 104 additions & 14 deletions README.md

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,23 @@
},
"sideEffects": false,
"devDependencies": {
"@rollup/plugin-commonjs": "22",
"@rollup/plugin-json": "4",
"@rollup/plugin-node-resolve": "13",
"canvas": "2",
"eslint": "8",
"htl": "0.3",
"js-beautify": "1",
"jsdom": "19",
"mocha": "9",
"mocha": "10",
"module-alias": "2",
"rollup": "2",
"rollup-plugin-terser": "7",
"vite": "2"
},
"dependencies": {
"d3": "^7.3.0",
"interval-tree-1d": "1",
"isoformat": "0.2"
},
"engines": {
Expand Down
2 changes: 2 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -25,6 +26,7 @@ const config = {
banner: `// ${meta.name} v${meta.version} Copyright ${copyrights.join(", ")}`
},
plugins: [
commonjs(),
json(),
node()
]
Expand Down
62 changes: 53 additions & 9 deletions src/channel.js
Original file line number Diff line number Diff line change
@@ -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}) {
Expand All @@ -15,19 +17,41 @@ export function Channel(data, {scale, type, value, filter, hint}) {
};
}

export function channelSort(channels, facetChannels, data, options) {
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 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
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);
Expand All @@ -39,7 +63,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);
Expand All @@ -49,16 +73,36 @@ 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];
if (channel.scale === scale) return channel;
}
}

function difference(channels, k1, k2) {
const X1 = values(channels, k1);
const X2 = values(channels, k2);
return map(X2, (x2, i) => Math.abs(x2 - X1[i]), Float64Array);
}

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}`);
}

Expand Down
8 changes: 0 additions & 8 deletions src/defined.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
6 changes: 5 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -18,7 +19,10 @@ 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 {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";
Expand Down
13 changes: 11 additions & 2 deletions src/marks/dot.js
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -26,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;
Expand Down Expand Up @@ -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"});
}
46 changes: 46 additions & 0 deletions src/marks/hexgrid.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
45 changes: 1 addition & 44 deletions src/options.js
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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"]);
}
Expand Down
Loading