Skip to content

group segments by z #764

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
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 22 additions & 19 deletions src/marks/area.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {area as shapeArea, create, group} from "d3";
import {area as shapeArea, create} from "d3";
import {Curve} from "../curve.js";
import {defined} from "../defined.js";
import {Mark} from "../plot.js";
import {indexOf, maybeZ} from "../options.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} from "../style.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, groupIndex} from "../style.js";
import {maybeIdentityX, maybeIdentityY} from "../transforms/identity.js";
import {maybeStackX, maybeStackY} from "../transforms/stack.js";

const defaults = {
filter: null,
ariaLabel: "area",
strokeWidth: 1,
strokeMiterlimit: 1
Expand All @@ -19,10 +19,10 @@ export class Area extends Mark {
super(
data,
[
{name: "x1", value: x1, filter: null, scale: "x"},
{name: "y1", value: y1, filter: null, scale: "y"},
{name: "x2", value: x2, filter: null, scale: "x", optional: true},
{name: "y2", value: y2, filter: null, scale: "y", optional: true},
{name: "x1", value: x1, scale: "x"},
{name: "y1", value: y1, scale: "y"},
{name: "x2", value: x2, scale: "x", optional: true},
{name: "y2", value: y2, scale: "y", optional: true},
{name: "z", value: maybeZ(options), optional: true}
],
options,
Expand All @@ -31,23 +31,26 @@ export class Area extends Mark {
this.curve = Curve(curve, tension);
}
render(I, {x, y}, channels, dimensions) {
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, z: Z} = channels;
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels;
const {dx, dy} = this;
return create("svg:g")
.call(applyIndirectStyles, this, dimensions)
.call(applyTransform, x, y, dx, dy)
.call(g => g.selectAll()
.data(Z ? group(I, i => Z[i]).values() : [I])
.join("path")
.call(applyDirectStyles, this)
.call(applyGroupedChannelStyles, this, channels)
.attr("d", shapeArea()
.curve(this.curve)
.defined(i => defined(X1[i]) && defined(Y1[i]) && defined(X2[i]) && defined(Y2[i]))
.x0(i => X1[i])
.y0(i => Y1[i])
.x1(i => X2[i])
.y1(i => Y2[i])))
.data(groupIndex(I, [X1, Y1, X2, Y2], channels))
.join("g")
.selectAll()
.data(segments => segments)
.join("path")
.call(applyDirectStyles, this)
.call(applyGroupedChannelStyles, this, channels)
.attr("d", shapeArea()
.curve(this.curve)
.defined(i => i >= 0)
.x0(i => X1[i])
.y0(i => Y1[i])
.x1(i => X2[i])
.y1(i => Y2[i])))
.node();
}
}
Expand Down
35 changes: 19 additions & 16 deletions src/marks/line.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {create, group, line as shapeLine} from "d3";
import {create, line as shapeLine} from "d3";
import {Curve} from "../curve.js";
import {defined} from "../defined.js";
import {Mark} from "../plot.js";
import {indexOf, identity, maybeTuple, maybeZ} from "../options.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, offset} from "../style.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, offset, groupIndex} from "../style.js";
import {applyGroupedMarkers, markers} from "./marker.js";

const defaults = {
filter: null,
ariaLabel: "line",
fill: "none",
stroke: "currentColor",
Expand All @@ -20,8 +20,8 @@ export class Line extends Mark {
super(
data,
[
{name: "x", value: x, filter: null, scale: "x"},
{name: "y", value: y, filter: null, scale: "y"},
{name: "x", value: x, scale: "x"},
{name: "y", value: y, scale: "y"},
{name: "z", value: maybeZ(options), optional: true}
],
options,
Expand All @@ -31,22 +31,25 @@ export class Line extends Mark {
markers(this, options);
}
render(I, {x, y}, channels, dimensions) {
const {x: X, y: Y, z: Z} = channels;
const {x: X, y: Y} = channels;
const {dx, dy} = this;
return create("svg:g")
.call(applyIndirectStyles, this, dimensions)
.call(applyTransform, x, y, offset + dx, offset + dy)
.call(g => g.selectAll()
.data(Z ? group(I, i => Z[i]).values() : [I])
.join("path")
.call(applyDirectStyles, this)
.call(applyGroupedChannelStyles, this, channels)
.call(applyGroupedMarkers, this, channels)
.attr("d", shapeLine()
.curve(this.curve)
.defined(i => defined(X[i]) && defined(Y[i]))
.x(i => X[i])
.y(i => Y[i])))
.data(groupIndex(I, [X, Y], channels))
.join("g")
.selectAll()
.data(segments => segments)
.join("path")
.call(applyDirectStyles, this)
.call(applyGroupedChannelStyles, this, channels)
.call(applyGroupedMarkers, this, channels)
.attr("d", shapeLine()
.curve(this.curve)
.defined(i => i >= 0)
.x(i => X[i])
.y(i => Y[i])))
.node();
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ export function take(values, index) {
return Array.from(index, i => values[i]);
}

// Based on InternMap (d3.group).
export function keyof(value) {
return value !== null && typeof value === "object" ? value.valueOf() : value;
}

export function maybeInput(key, options) {
if (options[key] !== undefined) return options[key];
switch (key) {
Expand Down
12 changes: 8 additions & 4 deletions src/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ export function plot(options = {}) {
for (const mark of marks) {
const channels = markChannels.get(mark) ?? [];
const values = applyScales(channels, scales);
const index = filter(markIndex.get(mark), channels, values);
let index = markIndex.get(mark);
if (mark.filter != null) index = mark.filter(index, channels, values);
const node = mark.render(index, scales, values, dimensions, axes);
if (node != null) svg.appendChild(node);
}
Expand Down Expand Up @@ -136,7 +137,7 @@ export function plot(options = {}) {
return figure;
}

function filter(index, channels, values) {
function defaultFilter(index, channels, values) {
for (const [name, {filter = defined}] of channels) {
if (name !== undefined && filter !== null) {
const value = values[name];
Expand All @@ -154,6 +155,7 @@ export class Mark {
this.sort = isOptions(sort) ? sort : null;
this.facet = facet == null || facet === false ? null : keyword(facet === true ? "include" : facet, "facet", ["auto", "include", "exclude"]);
const {transform} = basic(options);
this.filter = defaults?.filter === undefined ? defaultFilter : defaults.filter;
this.transform = transform;
if (defaults !== undefined) channels = styles(this, options, channels, defaults);
this.channels = channels.filter(channel => {
Expand Down Expand Up @@ -328,9 +330,11 @@ class Facet extends Mark {
.each(function(key) {
const marksFacetIndex = marksIndexByFacet.get(key);
for (let i = 0; i < marks.length; ++i) {
const mark = marks[i];
const values = marksValues[i];
const index = filter(marksFacetIndex[i], marksChannels[i], values);
const node = marks[i].render(index, scales, values, subdimensions);
let index = marksFacetIndex[i];
if (mark.filter != null) index = mark.filter(index, marksChannels[i], values);
const node = mark.render(index, scales, values, subdimensions);
if (node != null) this.appendChild(node);
}
}))
Expand Down
54 changes: 51 additions & 3 deletions src/style.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {isoFormat, namespaces} from "d3";
import {nonempty} from "./defined.js";
import {group, isoFormat, namespaces} from "d3";
import {defined, nonempty} from "./defined.js";
import {formatNumber} from "./format.js";
import {string, number, maybeColorChannel, maybeNumberChannel, isTemporal, isNumeric} from "./options.js";
import {string, number, maybeColorChannel, maybeNumberChannel, isTemporal, isNumeric, keyof} from "./options.js";

export const offset = typeof window !== "undefined" && window.devicePixelRatio > 1 ? 0 : 0.5;

Expand Down Expand Up @@ -176,6 +176,54 @@ export function applyGroupedChannelStyles(selection, {target}, {ariaLabel: AL, t
applyTitleGroup(selection, T);
}

function groupAesthetics({ariaLabel: AL, title: T, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO, strokeWidth: SW, opacity: O, href: H}) {
return [AL, T, F, FO, S, SO, SW, O, H].filter(c => c !== undefined);
}

export function groupIndex(I, position, channels) {
const {z: Z} = channels; // group channel
const A = groupAesthetics(channels); // aesthetic channels
const C = [...position, ...A]; // all channels

// Group the current index by Z (if any).
return Array.from(Z ? group(I, i => Z[i]).values() : [I], G => [...groupSubIndex(G, A, C)]);
}

function* groupSubIndex(G, A, C) {
let Ag; // the A-values (aesthetics) of the current group, if any
let Gg; // the current group index (a subset of G, and I), if any
out: for (const i of G) {
// If any channel has an undefined value for this index, skip it.
for (const c of C) {
if (!defined(c[i])) {
if (Gg) Gg.push(-1);
continue out;
}
}
// Otherwise, if this is a new group, record the aesthetics for this
// group. Yield the current group and start a new one.
if (Ag === undefined) {
if (Gg) yield Gg;
Ag = A.map(c => keyof(c[i])), Gg = [i];
continue;
}
// Otherwise, add the current index to the current group. Then, if any of
// the aesthetics don’t match the current group, yield the current group
// and start a new group of the current index.
Gg.push(i);
for (let j = 0; j < A.length; ++j) {
const k = keyof(A[j][i]);
if (k !== Ag[j]) {
yield Gg;
Ag = A.map(c => keyof(c[i])), Gg = [i];
continue out;
}
}
}
// Yield the current group, if any.
if (Gg) yield Gg;
}

// clip: true clips to the frame
// TODO: accept other types of clips (paths, urls, x, y, other marks?…)
// https://github.com/observablehq/plot/issues/181
Expand Down