Skip to content

z hexbins #810

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

Closed
43 changes: 36 additions & 7 deletions src/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,41 @@ export function Channel(data, {scale, type, value, filter, hint}) {
};
}

export function channelObject(channelDescriptors, data) {
const channels = {};
for (const channel of channelDescriptors) {
channels[channel.name] = Channel(data, channel);
}
return channels;
}

// TODO Use Float64Array for scales with numeric ranges, e.g. position?
export function valueObject(channels, scales) {
const values = {};
for (const channelName in channels) {
const {scale: scaleName, value} = channels[channelName];
const scale = scales[scaleName];
values[channelName] = scale === undefined ? value : Array.from(value, scale);
}
return values;
}

// Note: mutates channel.domain! This is set to a function so that it is lazily
// computed; i.e., if the scale’s domain is set explicitly, that takes priority
// over the sort option, and we don’t need to do additional work.
export function channelSort(channels, facetChannels, data, options) {
const {reverse: defaultReverse, reduce: defaultReduce = true, limit: defaultLimit} = options;
for (const x in options) {
if (!registry.has(x)) continue; // ignore unknown scale keys
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 +61,7 @@ export function channelSort(channels, facetChannels, data, options) {
: y === "width" ? difference(channels, "x1", "x2")
: values(channels, y, y === "y" ? "y2" : y === "x" ? "x2" : undefined);
const reducer = maybeReduce(reduce === true ? "max" : reduce, YV);
X[1].domain = () => {
X.domain = () => {
let domain = rollup(range(XV), I => reducer.reduce(I, YV), i => XV[i]);
domain = sort(domain, reverse ? descendingGroup : ascendingGroup);
if (lo !== 0 || hi !== Infinity) domain = domain.slice(lo, hi);
Expand All @@ -49,16 +71,23 @@ export function channelSort(channels, facetChannels, data, options) {
}
}

function findScaleChannel(channels, scale) {
for (const name in channels) {
const channel = channels[name];
if (channel.scale === scale) return channel;
}
}

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

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
4 changes: 3 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,6 +19,7 @@ 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 {group, groupX, groupY, groupZ} from "./transforms/group.js";
export {hexbin} from "./transforms/hexbin.js";
export {normalize, normalizeX, normalizeY} from "./transforms/normalize.js";
export {map, mapX, mapY} from "./transforms/map.js";
export {window, windowX, windowY} from "./transforms/window.js";
Expand Down
11 changes: 10 additions & 1 deletion 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 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 @@ -22,6 +20,7 @@ export const field = name => d => d[name];
export const indexOf = (d, i) => i;
export const identity = {transform: d => d};
export const zero = () => 0;
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 @@ -305,48 +304,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