Skip to content

Commit

Permalink
Common style channels (#490)
Browse files Browse the repository at this point in the history
* common style channels

* common styles via mark constructor

* common styles for lines

* common styles for bars and cells

* common styles for rects

* common styles for ticks

* common styles for rules

* common styles for links

* test frame

* common styles for frames

* common styles for dots

* common styles for texts

* remove obsolete Style function

* filter common styles

* values ↦ applyScales

* prettier

* filter facets (and centralize the filtering of common styles in style.js)

* normalize fill and stroke defaults

* move applyScales

* test frame stroke and fill

Co-authored-by: Philippe Rivière <fil@rezo.net>
  • Loading branch information
mbostock and Fil authored Aug 9, 2021
1 parent e7879d7 commit b1885ad
Show file tree
Hide file tree
Showing 16 changed files with 344 additions and 473 deletions.
12 changes: 8 additions & 4 deletions src/facet.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {cross, difference, groups, InternMap} from "d3";
import {create} from "d3";
import {Mark, values, first, second} from "./mark.js";
import {Mark, first, second} from "./mark.js";
import {applyScales} from "./scales.js";
import {filterStyles} from "./style.js";

export function facets(data, {x, y, ...options}, marks) {
return x === undefined && y === undefined
Expand Down Expand Up @@ -76,7 +78,7 @@ class Facet extends Mark {
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 marksValues = marksChannels.map(channels => values(channels, scales));
const marksValues = marksChannels.map(channels => applyScales(channels, scales));
return create("svg:g")
.call(g => {
if (fy && axes.y) {
Expand Down Expand Up @@ -110,10 +112,12 @@ class Facet extends Mark {
.each(function(key) {
const marksFacetIndex = marksIndexByFacet.get(key) || marksIndex;
for (let i = 0; i < marks.length; ++i) {
const values = marksValues[i];
const index = filterStyles(marksFacetIndex[i], values);
const node = marks[i].render(
marksFacetIndex[i],
index,
scales,
marksValues[i],
values,
subdimensions
);
if (node != null) this.appendChild(node);
Expand Down
22 changes: 4 additions & 18 deletions src/mark.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import {color} from "d3";
import {ascendingDefined, nonempty} from "./defined.js";
import {plot} from "./plot.js";
import {styles} from "./style.js";

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
const TypedArray = Object.getPrototypeOf(Uint8Array);
const objectToString = Object.prototype.toString;

export class Mark {
constructor(data, channels = [], {facet = "auto", ...options} = {}) {
constructor(data, channels = [], options = {}, defaults) {
const {facet = "auto"} = options;
const names = new Set();
this.data = data;
this.facet = facet ? keyword(facet === true ? "include" : facet, "facet", ["auto", "include", "exclude"]) : null;
const {transform} = maybeTransform(options);
this.transform = transform;
if (defaults !== undefined) channels = styles(this, options, channels, defaults);
this.channels = channels.filter(channel => {
const {name, value, optional} = channel;
if (value == null) {
Expand Down Expand Up @@ -309,23 +312,6 @@ export function numberChannel(source) {
};
}

// TODO use Float64Array.from for position and radius scales?
export function values(channels = [], scales) {
const values = Object.create(null);
for (let [name, {value, scale}] of channels) {
if (name !== undefined) {
if (scale !== undefined) {
scale = scales[scale];
if (scale !== undefined) {
value = Array.from(value, scale);
}
}
values[name] = value;
}
}
return values;
}

export function isOrdinal(values) {
for (const value of values) {
if (value == null) continue;
Expand Down
68 changes: 17 additions & 51 deletions src/marks/area.js
Original file line number Diff line number Diff line change
@@ -1,83 +1,49 @@
import {group} from "d3";
import {create} from "d3";
import {area as shapeArea} from "d3";
import {area as shapeArea, create, group} from "d3";
import {Curve} from "../curve.js";
import {defined} from "../defined.js";
import {Mark, indexOf, maybeColor, titleGroup, maybeNumber} from "../mark.js";
import {Style, applyDirectStyles, applyIndirectStyles, applyTransform, applyAttr} from "../style.js";
import {Mark, indexOf, maybeZ} from "../mark.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} from "../style.js";
import {maybeStackX, maybeStackY} from "../transforms/stack.js";

const defaults = {
strokeWidth: 1,
strokeMiterlimit: 1
};

export class Area extends Mark {
constructor(
data,
{
x1,
y1,
x2,
y2,
z, // optional grouping for multiple series
title,
fill,
fillOpacity,
stroke,
strokeOpacity,
curve,
tension,
...options
} = {}
) {
const [vstroke, cstroke] = maybeColor(stroke, "none");
const [vstrokeOpacity, cstrokeOpacity] = maybeNumber(strokeOpacity);
const [vfill, cfill] = maybeColor(fill, cstroke === "none" ? "currentColor" : "none");
const [vfillOpacity, cfillOpacity] = maybeNumber(fillOpacity);
if (z === undefined && vfill != null) z = vfill;
if (z === undefined && vstroke != null) z = vstroke;
constructor(data, options = {}) {
const {x1, y1, x2, y2, curve, tension} = options;
super(
data,
[
{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: z, optional: true},
{name: "title", value: title, optional: true},
{name: "fill", value: vfill, scale: "color", optional: true},
{name: "fillOpacity", value: vfillOpacity, scale: "opacity", optional: true},
{name: "stroke", value: vstroke, scale: "color", optional: true},
{name: "strokeOpacity", value: vstrokeOpacity, scale: "opacity", optional: true}
{name: "z", value: maybeZ(options), optional: true}
],
options
options,
defaults
);
this.curve = Curve(curve, tension);
Style(this, {
fill: cfill,
fillOpacity: cfillOpacity,
stroke: cstroke,
strokeMiterlimit: cstroke === "none" ? undefined : 1,
strokeOpacity: cstrokeOpacity,
...options
});
}
render(I, {x, y}, {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, z: Z, title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO}) {
render(I, {x, y}, channels) {
const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, z: Z} = channels;
return create("svg:g")
.call(applyIndirectStyles, this)
.call(applyTransform, x, y)
.call(g => g.selectAll()
.data(Z ? group(I, i => Z[i]).values() : [I])
.join("path")
.call(applyDirectStyles, this)
.call(applyAttr, "fill", F && (([i]) => F[i]))
.call(applyAttr, "fill-opacity", FO && (([i]) => FO[i]))
.call(applyAttr, "stroke", S && (([i]) => S[i]))
.call(applyAttr, "stroke-opacity", SO && (([i]) => SO[i]))
.call(applyGroupedChannelStyles, 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]))
.call(titleGroup(L)))
.y1(i => Y2[i])))
.node();
}
}
Expand Down
66 changes: 13 additions & 53 deletions src/marks/bar.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,15 @@
import {create} from "d3";
import {filter} from "../defined.js";
import {Mark, number, maybeColor, title, maybeNumber} from "../mark.js";
import {Style, applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr} from "../style.js";
import {Mark, number} from "../mark.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr, applyChannelStyles} from "../style.js";
import {maybeStackX, maybeStackY} from "../transforms/stack.js";

const defaults = {};

export class AbstractBar extends Mark {
constructor(
data,
channels,
{
title,
fill,
fillOpacity,
stroke,
strokeOpacity,
inset = 0,
insetTop = inset,
insetRight = inset,
insetBottom = inset,
insetLeft = inset,
rx,
ry,
...options
} = {}
) {
const [vstroke, cstroke] = maybeColor(stroke, "none");
const [vstrokeOpacity, cstrokeOpacity] = maybeNumber(strokeOpacity);
const [vfill, cfill] = maybeColor(fill, cstroke === "none" ? "currentColor" : "none");
const [vfillOpacity, cfillOpacity] = maybeNumber(fillOpacity);
super(
data,
[
...channels,
{name: "title", value: title, optional: true},
{name: "fill", value: vfill, scale: "color", optional: true},
{name: "fillOpacity", value: vfillOpacity, scale: "opacity", optional: true},
{name: "stroke", value: vstroke, scale: "color", optional: true},
{name: "strokeOpacity", value: vstrokeOpacity, scale: "opacity", optional: true}
],
options
);
Style(this, {
fill: cfill,
fillOpacity: cfillOpacity,
stroke: cstroke,
strokeOpacity: cstrokeOpacity,
...options
});
constructor(data, channels, options = {}) {
super(data, channels, options, defaults);
const {inset = 0, insetTop = inset, insetRight = inset, insetBottom = inset, insetLeft = inset, rx, ry} = options;
this.insetTop = number(insetTop);
this.insetRight = number(insetRight);
this.insetBottom = number(insetBottom);
Expand All @@ -56,8 +19,7 @@ export class AbstractBar extends Mark {
}
render(I, scales, channels, dimensions) {
const {rx, ry} = this;
const {title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO} = channels;
const index = filter(I, ...this._positions(channels), F, FO, S, SO);
const index = filter(I, ...this._positions(channels));
return create("svg:g")
.call(applyIndirectStyles, this)
.call(this._transform, scales)
Expand All @@ -69,13 +31,9 @@ export class AbstractBar extends Mark {
.attr("width", this._width(scales, channels, dimensions))
.attr("y", this._y(scales, channels, dimensions))
.attr("height", this._height(scales, channels, dimensions))
.call(applyAttr, "fill", F && (i => F[i]))
.call(applyAttr, "fill-opacity", FO && (i => FO[i]))
.call(applyAttr, "stroke", S && (i => S[i]))
.call(applyAttr, "stroke-opacity", SO && (i => SO[i]))
.call(applyAttr, "rx", rx)
.call(applyAttr, "ry", ry)
.call(title(L)))
.call(applyChannelStyles, channels))
.node();
}
_x(scales, {x: X}, {marginLeft}) {
Expand All @@ -99,7 +57,8 @@ export class AbstractBar extends Mark {
}

export class BarX extends AbstractBar {
constructor(data, {x1, x2, y, ...options} = {}) {
constructor(data, options = {}) {
const {x1, x2, y} = options;
super(
data,
[
Expand Down Expand Up @@ -127,7 +86,8 @@ export class BarX extends AbstractBar {
}

export class BarY extends AbstractBar {
constructor(data, {x, y1, y2, ...options} = {}) {
constructor(data, options = {}) {
const {x, y1, y2} = options;
super(
data,
[
Expand Down
59 changes: 17 additions & 42 deletions src/marks/dot.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,38 @@
import {create} from "d3";
import {filter, positive} from "../defined.js";
import {Mark, identity, maybeColor, maybeNumber, maybeTuple, title} from "../mark.js";
import {Style, applyDirectStyles, applyIndirectStyles, applyTransform, applyAttr} from "../style.js";
import {Mark, identity, maybeNumber, maybeTuple} from "../mark.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js";

const defaults = {
fill: "none",
stroke: "currentColor",
strokeWidth: 1.5
};

export class Dot extends Mark {
constructor(
data,
{
x,
y,
r,
title,
fill,
fillOpacity,
stroke,
strokeOpacity,
...options
} = {}
) {
constructor(data, options = {}) {
const {x, y, r} = options;
const [vr, cr] = maybeNumber(r, 3);
const [vfill, cfill] = maybeColor(fill, "none");
const [vfillOpacity, cfillOpacity] = maybeNumber(fillOpacity);
const [vstroke, cstroke] = maybeColor(stroke, cfill === "none" ? "currentColor" : "none");
const [vstrokeOpacity, cstrokeOpacity] = maybeNumber(strokeOpacity);
super(
data,
[
{name: "x", value: x, scale: "x", optional: true},
{name: "y", value: y, scale: "y", optional: true},
{name: "r", value: vr, scale: "r", optional: true},
{name: "title", value: title, optional: true},
{name: "fill", value: vfill, scale: "color", optional: true},
{name: "fillOpacity", value: vfillOpacity, scale: "opacity", optional: true},
{name: "stroke", value: vstroke, scale: "color", optional: true},
{name: "strokeOpacity", value: vstrokeOpacity, scale: "opacity", optional: true}
{name: "r", value: vr, scale: "r", optional: true}
],
options
options,
defaults
);
this.r = cr;
Style(this, {
fill: cfill,
fillOpacity: cfillOpacity,
stroke: cstroke,
strokeOpacity: cstrokeOpacity,
strokeWidth: cstroke === "none" ? undefined : 1.5,
...options
});
}
render(
I,
{x, y},
{x: X, y: Y, r: R, title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO},
channels,
{width, height, marginTop, marginRight, marginBottom, marginLeft}
) {
let index = filter(I, X, Y, F, FO, S, SO);
const {x: X, y: Y, r: R} = channels;
let index = filter(I, X, Y);
if (R) index = index.filter(i => positive(R[i]));
return create("svg:g")
.call(applyIndirectStyles, this)
Expand All @@ -65,11 +44,7 @@ export class Dot extends Mark {
.attr("cx", X ? i => X[i] : (marginLeft + width - marginRight) / 2)
.attr("cy", Y ? i => Y[i] : (marginTop + height - marginBottom) / 2)
.attr("r", R ? i => R[i] : this.r)
.call(applyAttr, "fill", F && (i => F[i]))
.call(applyAttr, "fill-opacity", FO && (i => FO[i]))
.call(applyAttr, "stroke", S && (i => S[i]))
.call(applyAttr, "stroke-opacity", SO && (i => SO[i]))
.call(title(L)))
.call(applyChannelStyles, channels))
.node();
}
}
Expand Down
Loading

0 comments on commit b1885ad

Please sign in to comment.