Skip to content

Commit 6f8a77c

Browse files
committed
group aeshetics
1 parent 08118dd commit 6f8a77c

File tree

9 files changed

+123
-45
lines changed

9 files changed

+123
-45
lines changed

src/axis.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import {boolean, take, number, string, keyword, maybeKeyword, constant, isTempor
33
import {formatIsoDate} from "./format.js";
44
import {radians} from "./math.js";
55
import {applyAttr, impliedString} from "./style.js";
6+
import {Decoration} from "./decoration.js";
67

7-
export class AxisX {
8+
export class AxisX extends Decoration {
89
constructor({
910
name = "x",
1011
axis,
@@ -22,6 +23,7 @@ export class AxisX {
2223
ariaLabel,
2324
ariaDescription
2425
} = {}) {
26+
super();
2527
this.name = name;
2628
this.axis = keyword(axis, "axis", ["top", "bottom"]);
2729
this.ticks = ticks;
@@ -97,7 +99,7 @@ export class AxisX {
9799
}
98100
}
99101

100-
export class AxisY {
102+
export class AxisY extends Decoration {
101103
constructor({
102104
name = "y",
103105
axis,
@@ -115,6 +117,7 @@ export class AxisY {
115117
ariaLabel,
116118
ariaDescription
117119
} = {}) {
120+
super();
118121
this.name = name;
119122
this.axis = keyword(axis, "axis", ["left", "right"]);
120123
this.ticks = ticks;
@@ -131,6 +134,9 @@ export class AxisY {
131134
this.ariaLabel = string(ariaLabel);
132135
this.ariaDescription = string(ariaDescription);
133136
}
137+
filter(I) {
138+
return I;
139+
}
134140
render(
135141
index,
136142
{[this.name]: y, fx},

src/decoration.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {defined} from "./defined.js";
2+
3+
export class Decoration {
4+
filter(index, channels, values) {
5+
for (const [name, {filter = defined}] of channels) {
6+
if (name !== undefined && filter !== null) {
7+
const value = values[name];
8+
index = index.filter(i => filter(value[i]));
9+
}
10+
}
11+
return index;
12+
}
13+
}

src/marks/area.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import {area as shapeArea, create, group} from "d3";
1+
import {area as shapeArea, create} from "d3";
22
import {Curve} from "../curve.js";
3-
import {defined} from "../defined.js";
43
import {Mark} from "../plot.js";
54
import {indexOf, maybeZ} from "../options.js";
6-
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} from "../style.js";
5+
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, groupIndex} from "../style.js";
76
import {maybeIdentityX, maybeIdentityY} from "../transforms/identity.js";
87
import {maybeStackX, maybeStackY} from "../transforms/stack.js";
98

@@ -19,31 +18,33 @@ export class Area extends Mark {
1918
super(
2019
data,
2120
[
22-
{name: "x1", value: x1, filter: null, scale: "x"},
23-
{name: "y1", value: y1, filter: null, scale: "y"},
24-
{name: "x2", value: x2, filter: null, scale: "x", optional: true},
25-
{name: "y2", value: y2, filter: null, scale: "y", optional: true},
21+
{name: "x1", value: x1, scale: "x"},
22+
{name: "y1", value: y1, scale: "y"},
23+
{name: "x2", value: x2, scale: "x", optional: true},
24+
{name: "y2", value: y2, scale: "y", optional: true},
2625
{name: "z", value: maybeZ(options), optional: true}
2726
],
2827
options,
2928
defaults
3029
);
3130
this.curve = Curve(curve, tension);
3231
}
32+
filter(I) {
33+
return I;
34+
}
3335
render(I, {x, y}, channels, dimensions) {
34-
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, z: Z} = channels;
36+
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels;
3537
const {dx, dy} = this;
3638
return create("svg:g")
3739
.call(applyIndirectStyles, this, dimensions)
3840
.call(applyTransform, x, y, dx, dy)
3941
.call(g => g.selectAll()
40-
.data(Z ? group(I, i => Z[i]).values() : [I])
42+
.data(groupIndex(I, [X1, Y1, X2, Y2], channels))
4143
.join("path")
4244
.call(applyDirectStyles, this)
4345
.call(applyGroupedChannelStyles, this, channels)
4446
.attr("d", shapeArea()
4547
.curve(this.curve)
46-
.defined(i => defined(X1[i]) && defined(Y1[i]) && defined(X2[i]) && defined(Y2[i]))
4748
.x0(i => X1[i])
4849
.y0(i => Y1[i])
4950
.x1(i => X2[i])

src/marks/line.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import {create, group, line as shapeLine} from "d3";
1+
import {create, line as shapeLine} from "d3";
22
import {Curve} from "../curve.js";
3-
import {defined} from "../defined.js";
43
import {Mark} from "../plot.js";
54
import {indexOf, identity, maybeTuple, maybeZ} from "../options.js";
6-
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, offset} from "../style.js";
5+
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, offset, groupIndex} from "../style.js";
76
import {applyGroupedMarkers, markers} from "./marker.js";
87

98
const defaults = {
@@ -20,8 +19,8 @@ export class Line extends Mark {
2019
super(
2120
data,
2221
[
23-
{name: "x", value: x, filter: null, scale: "x"},
24-
{name: "y", value: y, filter: null, scale: "y"},
22+
{name: "x", value: x, scale: "x"},
23+
{name: "y", value: y, scale: "y"},
2524
{name: "z", value: maybeZ(options), optional: true}
2625
],
2726
options,
@@ -30,21 +29,23 @@ export class Line extends Mark {
3029
this.curve = Curve(curve, tension);
3130
markers(this, options);
3231
}
32+
filter(I) {
33+
return I;
34+
}
3335
render(I, {x, y}, channels, dimensions) {
34-
const {x: X, y: Y, z: Z} = channels;
36+
const {x: X, y: Y} = channels;
3537
const {dx, dy} = this;
3638
return create("svg:g")
3739
.call(applyIndirectStyles, this, dimensions)
3840
.call(applyTransform, x, y, offset + dx, offset + dy)
3941
.call(g => g.selectAll()
40-
.data(Z ? group(I, i => Z[i]).values() : [I])
42+
.data(groupIndex(I, [X, Y], channels))
4143
.join("path")
4244
.call(applyDirectStyles, this)
4345
.call(applyGroupedChannelStyles, this, channels)
4446
.call(applyGroupedMarkers, this, channels)
4547
.attr("d", shapeLine()
4648
.curve(this.curve)
47-
.defined(i => defined(X[i]) && defined(Y[i]))
4849
.x(i => X[i])
4950
.y(i => Y[i])))
5051
.node();

src/options.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ export function take(values, index) {
123123
return Array.from(index, i => values[i]);
124124
}
125125

126+
// Based on InternMap (d3.group).
127+
export function keyof(value) {
128+
return value !== null && typeof value === "object" ? value.valueOf() : value;
129+
}
130+
126131
export function maybeInput(key, options) {
127132
if (options[key] !== undefined) return options[key];
128133
switch (key) {

src/plot.js

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {create, cross, difference, groups, InternMap, select} from "d3";
22
import {Axes, autoAxisTicks, autoScaleLabels} from "./axes.js";
33
import {Channel, channelSort} from "./channel.js";
4-
import {defined} from "./defined.js";
4+
import {Decoration} from "./decoration.js";
55
import {Dimensions} from "./dimensions.js";
66
import {Legends, exposeLegends} from "./legends.js";
77
import {arrayify, isOptions, keyword, range, first, second, where} from "./options.js";
@@ -99,7 +99,7 @@ export function plot(options = {}) {
9999
for (const mark of marks) {
100100
const channels = markChannels.get(mark) ?? [];
101101
const values = applyScales(channels, scales);
102-
const index = filter(markIndex.get(mark), channels, values);
102+
const index = mark.filter(markIndex.get(mark), channels, values);
103103
const node = mark.render(index, scales, values, dimensions, axes);
104104
if (node != null) svg.appendChild(node);
105105
}
@@ -136,18 +136,9 @@ export function plot(options = {}) {
136136
return figure;
137137
}
138138

139-
function filter(index, channels, values) {
140-
for (const [name, {filter = defined}] of channels) {
141-
if (name !== undefined && filter !== null) {
142-
const value = values[name];
143-
index = index.filter(i => filter(value[i]));
144-
}
145-
}
146-
return index;
147-
}
148-
149-
export class Mark {
139+
export class Mark extends Decoration {
150140
constructor(data, channels = [], options = {}, defaults) {
141+
super();
151142
const {facet = "auto", sort, dx, dy, clip} = options;
152143
const names = new Set();
153144
this.data = data;
@@ -328,9 +319,10 @@ class Facet extends Mark {
328319
.each(function(key) {
329320
const marksFacetIndex = marksIndexByFacet.get(key);
330321
for (let i = 0; i < marks.length; ++i) {
322+
const mark = marks[i];
331323
const values = marksValues[i];
332-
const index = filter(marksFacetIndex[i], marksChannels[i], values);
333-
const node = marks[i].render(index, scales, values, subdimensions);
324+
const index = mark.filter(marksFacetIndex[i], marksChannels[i], values);
325+
const node = mark.render(index, scales, values, subdimensions);
334326
if (node != null) this.appendChild(node);
335327
}
336328
}))

src/style.js

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import {isoFormat, namespaces} from "d3";
2-
import {nonempty} from "./defined.js";
1+
import {group, isoFormat, namespaces} from "d3";
2+
import {defined, nonempty} from "./defined.js";
33
import {formatNumber} from "./format.js";
4-
import {string, number, maybeColorChannel, maybeNumberChannel, isTemporal, isNumeric} from "./options.js";
4+
import {string, number, maybeColorChannel, maybeNumberChannel, isTemporal, isNumeric, keyof} from "./options.js";
55

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

@@ -176,6 +176,58 @@ export function applyGroupedChannelStyles(selection, {target}, {ariaLabel: AL, t
176176
applyTitleGroup(selection, T);
177177
}
178178

179+
function groupAesthetics({ariaLabel: AL, title: T, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO, strokeWidth: SW, opacity: O, href: H}) {
180+
return [AL, T, F, FO, S, SO, SW, O, H].filter(c => c !== undefined);
181+
}
182+
183+
export function* groupIndex(I, position, channels) {
184+
const {z: Z} = channels; // group channel
185+
const A = groupAesthetics(channels); // aesthetic channels
186+
const C = [...position, ...A]; // all channels
187+
188+
// Group the current index by Z (if any).
189+
for (const G of Z ? group(I, i => Z[i]).values() : [I]) {
190+
let Ag; // the A-values (aesthetics) of the current group, if any
191+
let Gg; // the current group index (a subset of G, and I), if any
192+
out: for (const i of G) {
193+
194+
// If any channel has an undefined value for this index, yield the current
195+
// group and start a new empty group, skipping this index.
196+
for (const c of C) {
197+
if (!defined(c[i])) {
198+
if (Gg) yield Gg;
199+
Ag = Gg = undefined;
200+
continue out;
201+
}
202+
}
203+
204+
// Otherwise, if this is a new group, record the aesthetics for this
205+
// group. Yield the current group and start a new one.
206+
if (Ag === undefined) {
207+
if (Gg) yield Gg;
208+
Ag = A.map(c => keyof(c[i])), Gg = [i];
209+
continue;
210+
}
211+
212+
// Otherwise, add the current index to the current group. Then, if any of
213+
// the aesthetics don’t match the current group, yield the current group
214+
// and start a new group of the current index.
215+
Gg.push(i);
216+
for (let j = 0; j < A.length; ++j) {
217+
const k = keyof(A[j][i]);
218+
if (k !== Ag[j]) {
219+
yield Gg;
220+
Ag = A.map(c => keyof(c[i])), Gg = [i];
221+
continue out;
222+
}
223+
}
224+
}
225+
226+
// Yield the current group, if any.
227+
if (Gg) yield Gg;
228+
}
229+
}
230+
179231
// clip: true clips to the frame
180232
// TODO: accept other types of clips (paths, urls, x, y, other marks?…)
181233
// https://github.com/observablehq/plot/issues/181

test/output/availability.svg

Lines changed: 3 additions & 1 deletion
Loading

test/output/carsParcoords.svg

Lines changed: 12 additions & 6 deletions
Loading

0 commit comments

Comments
 (0)