Skip to content

Commit

Permalink
top-level style!
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Dec 3, 2020
1 parent 63aa19c commit 2b797cc
Show file tree
Hide file tree
Showing 20 changed files with 205 additions and 180 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"dependencies": {
"d3-array": "^2.8.0",
"d3-axis": "^2.0.0",
"d3-color": "^2.0.0",
"d3-interpolate": "^2.0.1",
"d3-scale": "^3.2.3",
"d3-scale-chromatic": "^2.0.0",
Expand Down
26 changes: 26 additions & 0 deletions src/mark.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {color} from "d3-color";

export class Mark {
constructor(data, channels = [], transform = identity) {
const names = new Set();
Expand Down Expand Up @@ -46,3 +48,27 @@ export const string = x => x == null ? undefined : x + "";
export const number = x => x == null ? undefined : +x;
export const first = d => d[0];
export const second = d => d[1];

// Some channels may allow a string constant to be specified; to differentiate
// string constants (e.g., "red") from named fields (e.g., "date"), this
// function tests whether the given value is a CSS color string and returns a
// tuple [channel, constant] where one of the two is undefined, and the other is
// the given value. If you wish to reference a named field that is also a valid
// CSS color, use an accessor (d => d.red) instead.
export function maybeColor(value) {
return typeof value === "string" && (value === "currentColor" || color(value)) ? [undefined, value] : [value, undefined];
}

// Similar to maybeColor, this tests whether the given value is a number
// indicating a constant, and otherwise assumes that it’s a channel value.
export function maybeNumber(value) {
return typeof value === "number" ? [undefined, value] : [value, undefined];
}

export function applyAttr(selection, name, value) {
if (value != null) selection.attr(name, value);
}

export function applyStyle(selection, name, value) {
if (value != null) selection.style(name, value);
}
13 changes: 6 additions & 7 deletions src/marks/area.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {create} from "d3-selection";
import {area as shapeArea} from "d3-shape";
import {Curve} from "../curve.js";
import {Mark, identity, indexOf, zero} from "../mark.js";
import {defined} from "../defined.js";
import {Mark, identity, indexOf, zero} from "../mark.js";
import {Style, applyStyles} from "../style.js";

export class Area extends Mark {
Expand All @@ -14,8 +14,8 @@ export class Area extends Mark {
x2,
y2,
curve,
style,
transform
transform,
...style
} = {}
) {
super(
Expand All @@ -29,14 +29,13 @@ export class Area extends Mark {
transform
);
this.curve = Curve(curve);
this.style = Style(style);
Object.assign(this, Style(style));
}
render(I, {x, y}, {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1}) {
const {curve, style} = this;
return create("svg:path")
.call(applyStyles, style)
.call(applyStyles, this)
.attr("d", shapeArea()
.curve(curve)
.curve(this.curve)
.defined(i => defined(X1[i]) && defined(Y1[i]) && defined(X2[i]) && defined(Y2[i]))
.x0(i => x(X1[i]))
.y0(i => y(Y1[i]))
Expand Down
21 changes: 11 additions & 10 deletions src/marks/bar.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {ascending} from "d3-array";
import {create} from "d3-selection";
import {defined} from "../defined.js";
import {Mark, number, identity, indexOf, first, second} from "../mark.js";
import {Style, applyIndirectStyles, applyDirectStyles} from "../style.js";
import {Mark, number, identity, indexOf, first, second, maybeColor} from "../mark.js";
import {Style, applyDirectStyles, applyIndirectStyles} from "../style.js";

class AbstractBar extends Mark {
constructor(
Expand All @@ -12,25 +12,27 @@ class AbstractBar extends Mark {
z,
fill,
stroke,
style,
insetTop = 0,
insetRight = 0,
insetBottom = 0,
insetLeft = 0,
transform
transform,
...style
} = {}
) {
const [vfill, cfill] = maybeColor(fill);
const [vstroke, cstroke] = maybeColor(stroke);
super(
data,
[
...channels,
{name: "z", value: z, optional: true},
{name: "fill", value: fill, scale: "color", optional: true},
{name: "stroke", value: stroke, scale: "color", optional: true}
{name: "fill", value: vfill, scale: "color", optional: true},
{name: "stroke", value: vstroke, scale: "color", optional: true}
],
transform
);
this.style = Style(style);
Object.assign(this, Style({fill: cfill, stroke: cstroke, ...style}));
this.insetTop = number(insetTop);
this.insetRight = number(insetRight);
this.insetBottom = number(insetBottom);
Expand All @@ -39,17 +41,16 @@ class AbstractBar extends Mark {
render(I, scales, channels) {
const {color} = scales;
const {x: X, y: Y, z: Z, fill: F, stroke: S} = channels;
const {style} = this;
let index = I.filter(i => defined(X[i]) && defined(Y[i]));
if (F) index = index.filter(i => defined(F[i]));
if (S) index = index.filter(i => defined(S[i]));
if (Z) index.sort((i, j) => ascending(Z[i], Z[j]));
return create("svg:g")
.call(applyIndirectStyles, style)
.call(applyIndirectStyles, this)
.call(g => g.selectAll()
.data(index)
.join("rect")
.call(applyDirectStyles, style)
.call(applyDirectStyles, this)
.attr("x", this._x(scales, channels))
.attr("width", this._width(scales, channels))
.attr("y", this._y(scales, channels))
Expand Down
36 changes: 19 additions & 17 deletions src/marks/dot.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {ascending} from "d3-array";
import {create} from "d3-selection";
import {defined, nonempty} from "../defined.js";
import {Mark, indexOf, identity, first, second, number} from "../mark.js";
import {applyDirectStyles, applyIndirectStyles, Style} from "../style.js";
import {Mark, indexOf, identity, first, second, maybeColor, maybeNumber} from "../mark.js";
import {Style, applyDirectStyles, applyIndirectStyles} from "../style.js";

export class Dot extends Mark {
constructor(
Expand All @@ -15,51 +15,53 @@ export class Dot extends Mark {
title,
fill,
stroke,
style = {},
transform
transform,
...style
} = {}
) {
const [vr, cr = vr == null ? 3 : undefined] = maybeNumber(r);
const [vfill, cfill = vfill == null ? "none" : undefined] = maybeColor(fill);
const [vstroke, cstroke = vstroke == null && cfill === "none" ? "currentColor" : undefined] = maybeColor(stroke);
super(
data,
[
{name: "x", value: x, scale: "x"},
{name: "y", value: y, scale: "y"},
{name: "z", value: z, optional: true},
{name: "r", value: r, scale: "r", optional: true},
{name: "r", value: vr, scale: "r", optional: true},
{name: "title", value: title, optional: true},
{name: "fill", value: fill, scale: "color", optional: true},
{name: "stroke", value: stroke, scale: "color", optional: true}
{name: "fill", value: vfill, scale: "color", optional: true},
{name: "stroke", value: vstroke, scale: "color", optional: true}
],
transform
);
this.style = Style({
fill: fill === undefined ? "none" : undefined,
stroke: stroke === undefined && fill === undefined && style.fill === undefined ? "currentColor" : undefined,
strokeWidth: fill === undefined && style.fill === undefined ? 1.5 : undefined,
this.r = cr;
Object.assign(this, Style({
fill: cfill,
stroke: cstroke,
strokeWidth: cstroke != null || vstroke != null ? 1.5 : undefined,
...style
});
this.style.r = style.r === undefined ? 3 : number(style.r);
}));
}
render(
I,
{x, y, r, color},
{x: X, y: Y, z: Z, r: R, title: L, fill: F, stroke: S}
) {
const {style} = this;
let index = I.filter(i => defined(X[i]) && defined(Y[i]));
if (R) index = index.filter(i => defined(R[i]));
if (F) index = index.filter(i => defined(F[i]));
if (S) index = index.filter(i => defined(S[i]));
if (Z) index.sort((i, j) => ascending(Z[i], Z[j]));
return create("svg:g")
.call(applyIndirectStyles, style)
.call(applyIndirectStyles, this)
.call(g => g.selectAll()
.data(index)
.join("circle")
.call(applyDirectStyles, style)
.call(applyDirectStyles, this)
.attr("cx", i => x(X[i]))
.attr("cy", i => y(Y[i]))
.attr("r", R ? i => r(R[i]) : style.r)
.attr("r", R ? i => r(R[i]) : this.r)
.attr("fill", F && (i => color(F[i])))
.attr("stroke", S && (i => color(S[i])))
.call(L ? text => text
Expand Down
17 changes: 8 additions & 9 deletions src/marks/line.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {line as shapeLine} from "d3-shape";
import {Curve} from "../curve.js";
import {defined} from "../defined.js";
import {Mark, indexOf, identity, first, second} from "../mark.js";
import {Style, applyIndirectStyles, applyDirectStyles} from "../style.js";
import {Style, applyDirectStyles, applyIndirectStyles} from "../style.js";

export class Line extends Mark {
constructor(
Expand All @@ -14,8 +14,8 @@ export class Line extends Mark {
y,
z, // optional grouping for multiple series
curve,
style,
transform
transform,
...style
} = {}
) {
super(
Expand All @@ -28,23 +28,22 @@ export class Line extends Mark {
transform
);
this.curve = Curve(curve);
this.style = Style({
Object.assign(this, Style({
fill: "none",
stroke: "currentColor",
strokeWidth: z ? 1 : 1.5,
...style
});
}));
}
render(I, {x, y}, {x: X, y: Y, z: Z}) {
const {curve, style} = this;
return create("svg:g")
.call(applyIndirectStyles, style)
.call(applyIndirectStyles, this)
.call(g => g.selectAll()
.data(Z ? group(I, i => Z[i]).values() : [I])
.join("path")
.call(applyDirectStyles, style)
.call(applyDirectStyles, this)
.attr("d", shapeLine()
.curve(curve)
.curve(this.curve)
.defined(i => defined(X[i]) && defined(Y[i]))
.x(i => x(X[i]))
.y(i => y(Y[i]))))
Expand Down
18 changes: 9 additions & 9 deletions src/marks/link.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {ascending} from "d3-array";
import {create} from "d3-selection";
import {defined} from "../defined.js";
import {Mark} from "../mark.js";
import {Style, applyIndirectStyles, applyDirectStyles} from "../style.js";
import {Mark, maybeColor} from "../mark.js";
import {Style, applyDirectStyles, applyIndirectStyles} from "../style.js";

export class Link extends Mark {
constructor(
Expand All @@ -14,10 +14,11 @@ export class Link extends Mark {
y2,
z,
stroke,
style,
transform
transform,
...style
} = {}
) {
const [vstroke, cstroke = vstroke == null ? "currentColor" : undefined] = maybeColor(stroke);
super(
data,
[
Expand All @@ -26,27 +27,26 @@ export class Link extends Mark {
{name: "x2", value: x2, scale: "x", label: x2.label},
{name: "y2", value: y2, scale: "y", label: y2.label},
{name: "z", value: z, optional: true},
{name: "stroke", value: stroke, scale: "color", optional: true}
{name: "stroke", value: vstroke, scale: "color", optional: true}
],
transform
);
this.style = Style({stroke: "currentColor", ...style});
Object.assign(this, Style({stroke: cstroke, ...style}));
}
render(
I,
{x, y, color},
{x1: X1, y1: Y1, x2: X2, y2: Y2, z: Z, stroke: S}
) {
const {style} = this;
let index = I.filter(i => defined(X1[i]) && defined(Y1[i]) && defined(X2[i]) && defined(Y2[i]));
if (S) index = index.filter(i => defined(S[i]));
if (Z) index.sort((i, j) => ascending(Z[i], Z[j]));
return create("svg:g")
.call(applyIndirectStyles, style)
.call(applyIndirectStyles, this)
.call(g => g.selectAll()
.data(index)
.join("line")
.call(applyDirectStyles, style)
.call(applyDirectStyles, this)
.attr("x1", i => x(X1[i]))
.attr("y1", i => y(Y1[i]))
.attr("x2", i => x(X2[i]))
Expand Down
Loading

0 comments on commit 2b797cc

Please sign in to comment.