Skip to content

Commit

Permalink
dodge
Browse files Browse the repository at this point in the history
rebased on mbostock/reinitialize
  • Loading branch information
Fil committed Mar 24, 2022
1 parent 3e70461 commit 2c228d2
Show file tree
Hide file tree
Showing 15 changed files with 1,933 additions and 7 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
},
"sideEffects": false,
"devDependencies": {
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-json": "4",
"@rollup/plugin-node-resolve": "13",
"canvas": "2",
Expand All @@ -50,6 +51,7 @@
},
"dependencies": {
"d3": "^7.3.0",
"interval-tree-1d": "1",
"d3-hexbin": "^0.2.2",
"isoformat": "0.2"
},
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
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export {Vector, vector, vectorX, vectorY} from "./marks/vector.js";
export {valueof, channel} 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";
Expand Down
11 changes: 6 additions & 5 deletions src/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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));
Expand All @@ -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);
Expand Down
103 changes: 103 additions & 0 deletions src/transforms/dodge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {max} from "d3";
import IntervalTree from "interval-tree-1d";
import {finite, positive} from "../defined.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(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 {facets, channels: {
[x]: {value: X},
[y]: {value: Y},
...R && {r: {value: R}}
}};
},
...options
};
}

function compareSymmetric(a, b) {
return Math.abs(a) - Math.abs(b);
}

function compareAscending(a, b) {
return (a < 0) - (b < 0) || (a - b);
}
Loading

0 comments on commit 2c228d2

Please sign in to comment.