From efd4f3ec51c6b9dd2624a36a357d85ecd6bb8bef Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 8 Aug 2021 09:56:37 -0700 Subject: [PATCH 01/20] common style channels --- src/marks/area.js | 59 +++++++++++--------------------------------- src/style.js | 63 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 45 deletions(-) diff --git a/src/marks/area.js b/src/marks/area.js index bd274c41a8..0ec4758037 100644 --- a/src/marks/area.js +++ b/src/marks/area.js @@ -1,37 +1,22 @@ -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} from "../mark.js"; +import {styles, findStyle, applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} from "../style.js"; import {maybeStackX, maybeStackY} from "../transforms/stack.js"; export class Area extends Mark { - constructor( - data, - { + constructor(data, options = {}) { + const [constants, channels] = styles(options); + const { x1, y1, x2, y2, - z, // optional grouping for multiple series - title, - fill, - fillOpacity, - stroke, - strokeOpacity, + z = findStyle(channels, "fill", "stroke"), // optional grouping for multiple series 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; + tension + } = options; super( data, [ @@ -40,25 +25,15 @@ export class Area extends Mark { {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} + ...channels ], options ); + Object.assign(this, constants); 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) @@ -66,18 +41,14 @@ export class Area extends Mark { .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(); } } diff --git a/src/style.js b/src/style.js index 7979bc7f53..53d9c08c69 100644 --- a/src/style.js +++ b/src/style.js @@ -1,7 +1,59 @@ -import {string, number} from "./mark.js"; +import {string, number, maybeColor, maybeNumber, titleGroup} from "./mark.js"; export const offset = typeof window !== "undefined" && window.devicePixelRatio > 1 ? 0 : 0.5; +// TODO This works for Area, but Line has different defaults (primary fill vs. primary stroke). +export function styles({ + title, + fill, + fillOpacity, + stroke, + strokeWidth, + strokeOpacity, + strokeLinejoin, + strokeLinecap, + strokeMiterlimit, + strokeDasharray, + mixBlendMode, + shapeRendering +} = {}) { + 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 (strokeMiterlimit === undefined) strokeMiterlimit = cstroke === "none" ? undefined : 1; + return [ + { + fill: impliedString(cfill, "currentColor"), + fillOpacity: impliedNumber(cfillOpacity, 1), + stroke: impliedString(cstroke, "none"), + strokeWidth: impliedNumber(strokeWidth, 1), + strokeOpacity: impliedNumber(cstrokeOpacity, 1), + strokeLinejoin: impliedString(strokeLinejoin, "miter"), + strokeLinecap: impliedString(strokeLinecap, "butt"), + strokeMiterlimit: impliedNumber(strokeMiterlimit, 4), + strokeDasharray: string(strokeDasharray), + mixBlendMode: impliedString(mixBlendMode, "normal"), + shapeRendering: impliedString(shapeRendering, "auto") + }, + [ + {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} + ] + ]; +} + +export function findStyle(channels, ...names) { + for (const name of names) { + const channel = channels.find(d => d.name === name); + if (channel && channel.value != null) return channel.value; + } +} + +// TODO remove me export function Style(mark, { fill, fillOpacity, @@ -28,6 +80,15 @@ export function Style(mark, { mark.shapeRendering = impliedString(shapeRendering, "auto"); } +// TODO This works for Area and Line, but Dot needs to be applied to individual elements. +export function applyGroupedChannelStyles(selection, {title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO}) { + applyAttr(selection, "fill", F && (([i]) => F[i])); + applyAttr(selection, "fill-opacity", FO && (([i]) => FO[i])); + applyAttr(selection, "stroke", S && (([i]) => S[i])); + applyAttr(selection, "stroke-opacity", SO && (([i]) => SO[i])); + titleGroup(L)(selection); +} + export function applyIndirectStyles(selection, mark) { applyAttr(selection, "fill", mark.fill); applyAttr(selection, "fill-opacity", mark.fillOpacity); From 5c8f009ba9f8c9c51d773d6af308c152fad75855 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 8 Aug 2021 10:12:14 -0700 Subject: [PATCH 02/20] common styles via mark constructor --- src/mark.js | 4 +++- src/marks/area.js | 22 ++++++---------------- src/style.js | 48 +++++++++++++++++++---------------------------- 3 files changed, 28 insertions(+), 46 deletions(-) diff --git a/src/mark.js b/src/mark.js index 7af27702a5..f513aa0005 100644 --- a/src/mark.js +++ b/src/mark.js @@ -1,18 +1,20 @@ 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 = [], {facet = "auto", ...options} = {}, commonStyles) { // TODO always support common styles 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 (commonStyles) channels = styles(this, options, channels); this.channels = channels.filter(channel => { const {name, value, optional} = channel; if (value == null) { diff --git a/src/marks/area.js b/src/marks/area.js index 0ec4758037..0d0e41386d 100644 --- a/src/marks/area.js +++ b/src/marks/area.js @@ -1,22 +1,13 @@ import {area as shapeArea, create, group} from "d3"; import {Curve} from "../curve.js"; import {defined} from "../defined.js"; -import {Mark, indexOf} from "../mark.js"; -import {styles, findStyle, applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} 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"; export class Area extends Mark { constructor(data, options = {}) { - const [constants, channels] = styles(options); - const { - x1, - y1, - x2, - y2, - z = findStyle(channels, "fill", "stroke"), // optional grouping for multiple series - curve, - tension - } = options; + const {x1, y1, x2, y2, curve, tension} = options; super( data, [ @@ -24,12 +15,11 @@ export class Area extends Mark { {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}, - ...channels + {name: "z", value: maybeZ(options), optional: true} ], - options + options, + true // TODO always support common styles ); - Object.assign(this, constants); this.curve = Curve(curve, tension); } render(I, {x, y}, channels) { diff --git a/src/style.js b/src/style.js index 53d9c08c69..7a5c366cef 100644 --- a/src/style.js +++ b/src/style.js @@ -3,7 +3,7 @@ import {string, number, maybeColor, maybeNumber, titleGroup} from "./mark.js"; export const offset = typeof window !== "undefined" && window.devicePixelRatio > 1 ? 0 : 0.5; // TODO This works for Area, but Line has different defaults (primary fill vs. primary stroke). -export function styles({ +export function styles(mark, { title, fill, fillOpacity, @@ -16,43 +16,33 @@ export function styles({ strokeDasharray, mixBlendMode, shapeRendering -} = {}) { +} = {}, channels) { 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 (strokeMiterlimit === undefined) strokeMiterlimit = cstroke === "none" ? undefined : 1; + mark.fill = impliedString(cfill, "currentColor"); + mark.fillOpacity = impliedNumber(cfillOpacity, 1); + mark.stroke = impliedString(cstroke, "none"); + mark.strokeWidth = impliedNumber(strokeWidth, 1); + mark.strokeOpacity = impliedNumber(cstrokeOpacity, 1); + mark.strokeLinejoin = impliedString(strokeLinejoin, "miter"); + mark.strokeLinecap = impliedString(strokeLinecap, "butt"); + mark.strokeMiterlimit = impliedNumber(strokeMiterlimit, 4); + mark.strokeDasharray = string(strokeDasharray); + mark.mixBlendMode = impliedString(mixBlendMode, "normal"); + mark.shapeRendering = impliedString(shapeRendering, "auto"); return [ - { - fill: impliedString(cfill, "currentColor"), - fillOpacity: impliedNumber(cfillOpacity, 1), - stroke: impliedString(cstroke, "none"), - strokeWidth: impliedNumber(strokeWidth, 1), - strokeOpacity: impliedNumber(cstrokeOpacity, 1), - strokeLinejoin: impliedString(strokeLinejoin, "miter"), - strokeLinecap: impliedString(strokeLinecap, "butt"), - strokeMiterlimit: impliedNumber(strokeMiterlimit, 4), - strokeDasharray: string(strokeDasharray), - mixBlendMode: impliedString(mixBlendMode, "normal"), - shapeRendering: impliedString(shapeRendering, "auto") - }, - [ - {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: "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}, + ...channels ]; } -export function findStyle(channels, ...names) { - for (const name of names) { - const channel = channels.find(d => d.name === name); - if (channel && channel.value != null) return channel.value; - } -} - // TODO remove me export function Style(mark, { fill, From c0595bb43beb6e4fe5e26e7c5459641199ace049 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 8 Aug 2021 13:04:41 -0700 Subject: [PATCH 03/20] common styles for lines --- src/mark.js | 4 +-- src/marks/area.js | 4 +-- src/marks/line.js | 62 +++++++++-------------------------------------- src/style.js | 58 ++++++++++++++++++++++++++++++-------------- 4 files changed, 56 insertions(+), 72 deletions(-) diff --git a/src/mark.js b/src/mark.js index f513aa0005..31f8f1ae87 100644 --- a/src/mark.js +++ b/src/mark.js @@ -8,13 +8,13 @@ const TypedArray = Object.getPrototypeOf(Uint8Array); const objectToString = Object.prototype.toString; export class Mark { - constructor(data, channels = [], {facet = "auto", ...options} = {}, commonStyles) { // TODO always support common styles + constructor(data, channels = [], {facet = "auto", ...options} = {}, primary) { 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 (commonStyles) channels = styles(this, options, channels); + if (primary !== undefined) channels = styles(this, options, channels, primary); this.channels = channels.filter(channel => { const {name, value, optional} = channel; if (value == null) { diff --git a/src/marks/area.js b/src/marks/area.js index 0d0e41386d..0b138a7a15 100644 --- a/src/marks/area.js +++ b/src/marks/area.js @@ -2,7 +2,7 @@ import {area as shapeArea, create, group} from "d3"; import {Curve} from "../curve.js"; import {defined} from "../defined.js"; import {Mark, indexOf, maybeZ} from "../mark.js"; -import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} from "../style.js"; +import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, primaryFill} from "../style.js"; import {maybeStackX, maybeStackY} from "../transforms/stack.js"; export class Area extends Mark { @@ -18,7 +18,7 @@ export class Area extends Mark { {name: "z", value: maybeZ(options), optional: true} ], options, - true // TODO always support common styles + primaryFill ); this.curve = Curve(curve, tension); } diff --git a/src/marks/line.js b/src/marks/line.js index 62094ffe4a..2b4b5e867d 100644 --- a/src/marks/line.js +++ b/src/marks/line.js @@ -1,60 +1,26 @@ -import {group} from "d3"; -import {create} from "d3"; -import {line as shapeLine} from "d3"; +import {create, group, line as shapeLine} from "d3"; import {Curve} from "../curve.js"; import {defined} from "../defined.js"; -import {Mark, indexOf, identity, maybeColor, maybeTuple, titleGroup, maybeNumber} from "../mark.js"; -import {Style, applyDirectStyles, applyIndirectStyles, applyTransform, applyAttr} from "../style.js"; +import {Mark, indexOf, identity, maybeTuple, maybeZ} from "../mark.js"; +import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, primaryStroke} from "../style.js"; export class Line extends Mark { - constructor( - data, - { - x, - y, - z, // optional grouping for multiple series - title, - fill, - fillOpacity, - stroke, - strokeOpacity, - curve, - tension, - ...options - } = {} - ) { - const [vfill, cfill] = maybeColor(fill, "none"); - const [vfillOpacity, cfillOpacity] = maybeNumber(fillOpacity); - const [vstroke, cstroke] = maybeColor(stroke, "currentColor"); - const [vstrokeOpacity, cstrokeOpacity] = maybeNumber(strokeOpacity); - if (z === undefined && vstroke != null) z = vstroke; - if (z === undefined && vfill != null) z = vfill; + constructor(data, options = {}) { + const {x, y, curve, tension} = options; super( data, [ {name: "x", value: x, scale: "x"}, {name: "y", value: y, scale: "y"}, - {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, + primaryStroke ); this.curve = Curve(curve, tension); - Style(this, { - fill: cfill, - fillOpacity: cfillOpacity, - stroke: cstroke, - strokeMiterlimit: cstroke === "none" ? undefined : 1, - strokeOpacity: cstrokeOpacity, - strokeWidth: cstroke === "none" ? undefined : 1.5, - ...options - }); } - render(I, {x, y}, {x: X, y: Y, z: Z, title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO}) { + render(I, {x, y}, channels) { + const {x: X, y: Y, z: Z} = channels; return create("svg:g") .call(applyIndirectStyles, this) .call(applyTransform, x, y, 0.5, 0.5) @@ -62,16 +28,12 @@ export class Line extends Mark { .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", shapeLine() .curve(this.curve) .defined(i => defined(X[i]) && defined(Y[i])) .x(i => X[i]) - .y(i => Y[i])) - .call(titleGroup(L))) + .y(i => Y[i]))) .node(); } } diff --git a/src/style.js b/src/style.js index 7a5c366cef..c0aeae12b3 100644 --- a/src/style.js +++ b/src/style.js @@ -2,25 +2,47 @@ import {string, number, maybeColor, maybeNumber, titleGroup} from "./mark.js"; export const offset = typeof window !== "undefined" && window.devicePixelRatio > 1 ? 0 : 0.5; -// TODO This works for Area, but Line has different defaults (primary fill vs. primary stroke). -export function styles(mark, { - title, - fill, - fillOpacity, - stroke, - strokeWidth, - strokeOpacity, - strokeLinejoin, - strokeLinecap, - strokeMiterlimit, - strokeDasharray, - mixBlendMode, - shapeRendering -} = {}, channels) { - const [vstroke, cstroke] = maybeColor(stroke, "none"); - const [vstrokeOpacity, cstrokeOpacity] = maybeNumber(strokeOpacity); - const [vfill, cfill] = maybeColor(fill, cstroke === "none" ? "currentColor" : "none"); +// For marks whose primary color is fill-based, such as areas, the fill will +// default to currentColor and the stroke to none. +export const primaryFill = Symbol("fill"); + +// For marks whose primary color is stroke-based, such as lines, the fill will +// default to none and the stroke to currentColor; in addition, the strokeWidth +// will default to 1.5 instead of 1. +export const primaryStroke = Symbol("stroke"); + +export function styles( + mark, + { + title, + fill, + fillOpacity, + stroke, + strokeWidth, + strokeOpacity, + strokeLinejoin, + strokeLinecap, + strokeMiterlimit, + strokeDasharray, + mixBlendMode, + shapeRendering + } = {}, + channels, + primary = primaryFill +) { + let vstroke, cstroke, vfill, cfill; + if (primary === primaryFill) { + ([vstroke, cstroke] = maybeColor(stroke, "none")); + ([vfill, cfill] = maybeColor(fill, cstroke === "none" ? "currentColor" : "none")); + } else if (primary === primaryStroke) { + ([vfill, cfill] = maybeColor(fill, "none")); + ([vstroke, cstroke] = maybeColor(stroke, "currentColor")); + if (strokeWidth === undefined) strokeWidth = cstroke === "none" ? undefined : 1.5; + } else { + throw new Error("unknown primary"); + } const [vfillOpacity, cfillOpacity] = maybeNumber(fillOpacity); + const [vstrokeOpacity, cstrokeOpacity] = maybeNumber(strokeOpacity); if (strokeMiterlimit === undefined) strokeMiterlimit = cstroke === "none" ? undefined : 1; mark.fill = impliedString(cfill, "currentColor"); mark.fillOpacity = impliedNumber(cfillOpacity, 1); From 0d29ac367bd999028b19e55aaa043edafc5ec97f Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 8 Aug 2021 13:13:25 -0700 Subject: [PATCH 04/20] common styles for bars and cells --- src/marks/bar.js | 65 +++++++++--------------------------------------- src/style.js | 15 ++++++++--- 2 files changed, 23 insertions(+), 57 deletions(-) diff --git a/src/marks/bar.js b/src/marks/bar.js index 971c6412a3..a21e02bf21 100644 --- a/src/marks/bar.js +++ b/src/marks/bar.js @@ -1,52 +1,13 @@ 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, primaryFill, applyChannelStyles} from "../style.js"; import {maybeStackX, maybeStackY} from "../transforms/stack.js"; 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, primaryFill); + 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); @@ -56,8 +17,8 @@ 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 {fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO} = channels; + const index = filter(I, ...this._positions(channels), F, FO, S, SO); // TODO filter standard channels return create("svg:g") .call(applyIndirectStyles, this) .call(this._transform, scales) @@ -69,13 +30,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}) { @@ -99,7 +56,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, [ @@ -127,7 +85,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, [ diff --git a/src/style.js b/src/style.js index c0aeae12b3..94968f3dc9 100644 --- a/src/style.js +++ b/src/style.js @@ -1,4 +1,4 @@ -import {string, number, maybeColor, maybeNumber, titleGroup} from "./mark.js"; +import {string, number, maybeColor, maybeNumber, title, titleGroup} from "./mark.js"; export const offset = typeof window !== "undefined" && window.devicePixelRatio > 1 ? 0 : 0.5; @@ -56,12 +56,12 @@ export function styles( mark.mixBlendMode = impliedString(mixBlendMode, "normal"); mark.shapeRendering = impliedString(shapeRendering, "auto"); return [ + ...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}, - ...channels + {name: "strokeOpacity", value: vstrokeOpacity, scale: "opacity", optional: true} ]; } @@ -92,7 +92,14 @@ export function Style(mark, { mark.shapeRendering = impliedString(shapeRendering, "auto"); } -// TODO This works for Area and Line, but Dot needs to be applied to individual elements. +export function applyChannelStyles(selection, {title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO}) { + applyAttr(selection, "fill", F && (i => F[i])); + applyAttr(selection, "fill-opacity", FO && (i => FO[i])); + applyAttr(selection, "stroke", S && (i => S[i])); + applyAttr(selection, "stroke-opacity", SO && (i => SO[i])); + title(L)(selection); +} + export function applyGroupedChannelStyles(selection, {title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO}) { applyAttr(selection, "fill", F && (([i]) => F[i])); applyAttr(selection, "fill-opacity", FO && (([i]) => FO[i])); From 9705077beeb142931a4ef298515c957a86315160 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 8 Aug 2021 13:40:34 -0700 Subject: [PATCH 05/20] common styles for rects --- src/mark.js | 5 ++-- src/marks/area.js | 9 ++++++-- src/marks/bar.js | 6 +++-- src/marks/line.js | 11 +++++++-- src/marks/rect.js | 58 ++++++++++++----------------------------------- src/style.js | 35 +++++++++++----------------- 6 files changed, 51 insertions(+), 73 deletions(-) diff --git a/src/mark.js b/src/mark.js index 31f8f1ae87..228f3fcb75 100644 --- a/src/mark.js +++ b/src/mark.js @@ -8,13 +8,14 @@ const TypedArray = Object.getPrototypeOf(Uint8Array); const objectToString = Object.prototype.toString; export class Mark { - constructor(data, channels = [], {facet = "auto", ...options} = {}, primary) { + 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 (primary !== undefined) channels = styles(this, options, channels, primary); + if (defaults !== undefined) channels = styles(this, options, channels, defaults); this.channels = channels.filter(channel => { const {name, value, optional} = channel; if (value == null) { diff --git a/src/marks/area.js b/src/marks/area.js index 0b138a7a15..15b85f18e0 100644 --- a/src/marks/area.js +++ b/src/marks/area.js @@ -2,9 +2,14 @@ import {area as shapeArea, create, group} from "d3"; import {Curve} from "../curve.js"; import {defined} from "../defined.js"; import {Mark, indexOf, maybeZ} from "../mark.js"; -import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, primaryFill} from "../style.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, options = {}) { const {x1, y1, x2, y2, curve, tension} = options; @@ -18,7 +23,7 @@ export class Area extends Mark { {name: "z", value: maybeZ(options), optional: true} ], options, - primaryFill + defaults ); this.curve = Curve(curve, tension); } diff --git a/src/marks/bar.js b/src/marks/bar.js index a21e02bf21..2bcf6508d9 100644 --- a/src/marks/bar.js +++ b/src/marks/bar.js @@ -1,12 +1,14 @@ import {create} from "d3"; import {filter} from "../defined.js"; import {Mark, number} from "../mark.js"; -import {applyDirectStyles, applyIndirectStyles, applyTransform, impliedString, applyAttr, primaryFill, applyChannelStyles} from "../style.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, options = {}) { - super(data, channels, options, primaryFill); + 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); diff --git a/src/marks/line.js b/src/marks/line.js index 2b4b5e867d..20e93ac7f4 100644 --- a/src/marks/line.js +++ b/src/marks/line.js @@ -2,7 +2,14 @@ import {create, group, line as shapeLine} from "d3"; import {Curve} from "../curve.js"; import {defined} from "../defined.js"; import {Mark, indexOf, identity, maybeTuple, maybeZ} from "../mark.js"; -import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, primaryStroke} from "../style.js"; +import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} from "../style.js"; + +const defaults = { + fill: "none", + stroke: "currentColor", + strokeWidth: 1.5, + strokeMiterlimit: 1 +}; export class Line extends Mark { constructor(data, options = {}) { @@ -15,7 +22,7 @@ export class Line extends Mark { {name: "z", value: maybeZ(options), optional: true} ], options, - primaryStroke + defaults ); this.curve = Curve(curve, tension); } diff --git a/src/marks/rect.js b/src/marks/rect.js index cb8d55aaf4..e42eb13a67 100644 --- a/src/marks/rect.js +++ b/src/marks/rect.js @@ -1,58 +1,37 @@ 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 Rect extends Mark { - constructor( - data, - { + constructor(data, options = {}) { + const { x1, y1, x2, y2, - 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); + ry + } = options; super( data, [ {name: "x1", value: x1, scale: "x"}, {name: "y1", value: y1, scale: "y"}, {name: "x2", value: x2, scale: "x"}, - {name: "y2", value: y2, scale: "y"}, - {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: "y2", value: y2, scale: "y"} ], - options + options, + defaults ); - Style(this, { - fill: cfill, - fillOpacity: cfillOpacity, - stroke: cstroke, - strokeOpacity: cstrokeOpacity, - ...options - }); this.insetTop = number(insetTop); this.insetRight = number(insetRight); this.insetBottom = number(insetBottom); @@ -60,13 +39,10 @@ export class Rect extends Mark { this.rx = impliedString(rx, "auto"); // number or percentage this.ry = impliedString(ry, "auto"); } - render( - I, - {x, y}, - {x1: X1, y1: Y1, x2: X2, y2: Y2, title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO} - ) { + render(I, {x, y}, channels) { + const {x1: X1, y1: Y1, x2: X2, y2: Y2, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO} = channels; const {rx, ry} = this; - const index = filter(I, X1, Y2, X2, Y2, F, FO, S, SO); + const index = filter(I, X1, Y2, X2, Y2, F, FO, S, SO); // TODO filter standard channels return create("svg:g") .call(applyIndirectStyles, this) .call(applyTransform, x, y) @@ -78,13 +54,9 @@ export class Rect extends Mark { .attr("y", i => Math.min(Y1[i], Y2[i]) + this.insetTop) .attr("width", i => Math.max(0, Math.abs(X2[i] - X1[i]) - this.insetLeft - this.insetRight)) .attr("height", i => Math.max(0, Math.abs(Y1[i] - Y2[i]) - this.insetTop - this.insetBottom)) - .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(); } } diff --git a/src/style.js b/src/style.js index 94968f3dc9..fce33205ac 100644 --- a/src/style.js +++ b/src/style.js @@ -2,15 +2,6 @@ import {string, number, maybeColor, maybeNumber, title, titleGroup} from "./mark export const offset = typeof window !== "undefined" && window.devicePixelRatio > 1 ? 0 : 0.5; -// For marks whose primary color is fill-based, such as areas, the fill will -// default to currentColor and the stroke to none. -export const primaryFill = Symbol("fill"); - -// For marks whose primary color is stroke-based, such as lines, the fill will -// default to none and the stroke to currentColor; in addition, the strokeWidth -// will default to 1.5 instead of 1. -export const primaryStroke = Symbol("stroke"); - export function styles( mark, { @@ -26,24 +17,24 @@ export function styles( strokeDasharray, mixBlendMode, shapeRendering - } = {}, + }, channels, - primary = primaryFill + { + fill: defaultFill = "currentColor", + stroke: defaultStroke = "none", + strokeWidth: defaultStrokeWidth, + strokeMiterlimit: defaultStrokeMiterlimit + } ) { - let vstroke, cstroke, vfill, cfill; - if (primary === primaryFill) { - ([vstroke, cstroke] = maybeColor(stroke, "none")); - ([vfill, cfill] = maybeColor(fill, cstroke === "none" ? "currentColor" : "none")); - } else if (primary === primaryStroke) { - ([vfill, cfill] = maybeColor(fill, "none")); - ([vstroke, cstroke] = maybeColor(stroke, "currentColor")); - if (strokeWidth === undefined) strokeWidth = cstroke === "none" ? undefined : 1.5; - } else { - throw new Error("unknown primary"); + const [vstroke, cstroke] = maybeColor(stroke, defaultStroke); + if (cstroke !== "none") { + if (defaultFill === "currentColor") defaultFill = "none"; + if (strokeWidth === undefined) strokeWidth = defaultStrokeWidth; + if (strokeMiterlimit === undefined) strokeMiterlimit = defaultStrokeMiterlimit; } + const [vfill, cfill] = maybeColor(fill, defaultFill); const [vfillOpacity, cfillOpacity] = maybeNumber(fillOpacity); const [vstrokeOpacity, cstrokeOpacity] = maybeNumber(strokeOpacity); - if (strokeMiterlimit === undefined) strokeMiterlimit = cstroke === "none" ? undefined : 1; mark.fill = impliedString(cfill, "currentColor"); mark.fillOpacity = impliedNumber(cfillOpacity, 1); mark.stroke = impliedString(cstroke, "none"); From b514e711ca5a7d10db6daba6780b7893731758f3 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 8 Aug 2021 13:48:21 -0700 Subject: [PATCH 06/20] common styles for ticks --- src/marks/tick.js | 64 +++++++++++++++-------------------------------- src/style.js | 15 +++++++++-- 2 files changed, 33 insertions(+), 46 deletions(-) diff --git a/src/marks/tick.js b/src/marks/tick.js index 841493833f..d344d0c030 100644 --- a/src/marks/tick.js +++ b/src/marks/tick.js @@ -1,36 +1,20 @@ import {create} from "d3"; import {filter} from "../defined.js"; -import {Mark, identity, maybeColor, title, maybeNumber, number} from "../mark.js"; -import {Style, applyDirectStyles, applyIndirectStyles, applyTransform, applyAttr} from "../style.js"; +import {Mark, identity, number} from "../mark.js"; +import {applyDirectStyles, applyIndirectStyles, applyTransform, applyChannelStyles} from "../style.js"; + +const defaults = { + fill: null, + stroke: "currentColor" +}; class AbstractTick extends Mark { - constructor( - data, - channels, - { - title, - stroke, - strokeOpacity, - ...options - } = {} - ) { - const [vstroke, cstroke] = maybeColor(stroke, "currentColor"); - const [vstrokeOpacity, cstrokeOpacity] = maybeNumber(strokeOpacity); - super( - data, - [ - ...channels, - {name: "title", value: title, optional: true}, - {name: "stroke", value: vstroke, scale: "color", optional: true}, - {name: "strokeOpacity", value: vstrokeOpacity, scale: "opacity", optional: true} - ], - options - ); - Style(this, {stroke: cstroke, strokeOpacity: cstrokeOpacity, ...options}); + constructor(data, channels, options) { + super(data, channels, options, defaults); } render(I, scales, channels, dimensions) { - const {x: X, y: Y, title: L, stroke: S, strokeOpacity: SO} = channels; - const index = filter(I, X, Y, S, SO); + const {x: X, y: Y, stroke: S, strokeOpacity: SO} = channels; + const index = filter(I, X, Y, S, SO); // TODO filter standard channels return create("svg:g") .call(applyIndirectStyles, this) .call(this._transform, scales) @@ -42,25 +26,20 @@ class AbstractTick extends Mark { .attr("x2", this._x2(scales, channels, dimensions)) .attr("y1", this._y1(scales, channels, dimensions)) .attr("y2", this._y2(scales, channels, dimensions)) - .call(applyAttr, "stroke", S && (i => S[i])) - .call(applyAttr, "stroke-opacity", SO && (i => SO[i])) - .call(title(L))) + .call(applyChannelStyles, channels)) .node(); } } export class TickX extends AbstractTick { - constructor( - data, - { + constructor(data, options = {}) { + const { x, y, inset = 0, insetTop = inset, - insetBottom = inset, - ...options - } = {} - ) { + insetBottom = inset + } = options; super( data, [ @@ -90,17 +69,14 @@ export class TickX extends AbstractTick { } export class TickY extends AbstractTick { - constructor( - data, - { + constructor(data, options = {}) { + const { x, y, inset = 0, insetRight = inset, - insetLeft = inset, - ...options - } = {} - ) { + insetLeft = inset + } = options; super( data, [ diff --git a/src/style.js b/src/style.js index fce33205ac..16a4ab3d40 100644 --- a/src/style.js +++ b/src/style.js @@ -27,16 +27,27 @@ export function styles( } ) { const [vstroke, cstroke] = maybeColor(stroke, defaultStroke); + + // some styles only apply if there is a stroke (either constant non-none, or channel) if (cstroke !== "none") { if (defaultFill === "currentColor") defaultFill = "none"; if (strokeWidth === undefined) strokeWidth = defaultStrokeWidth; if (strokeMiterlimit === undefined) strokeMiterlimit = defaultStrokeMiterlimit; } + + // some marks don’t support fill (e.g., tick) + if (defaultFill === null) { + fill = null; + fillOpacity = null; + } + const [vfill, cfill] = maybeColor(fill, defaultFill); const [vfillOpacity, cfillOpacity] = maybeNumber(fillOpacity); const [vstrokeOpacity, cstrokeOpacity] = maybeNumber(strokeOpacity); - mark.fill = impliedString(cfill, "currentColor"); - mark.fillOpacity = impliedNumber(cfillOpacity, 1); + if (defaultFill !== null) { + mark.fill = impliedString(cfill, "currentColor"); + mark.fillOpacity = impliedNumber(cfillOpacity, 1); + } mark.stroke = impliedString(cstroke, "none"); mark.strokeWidth = impliedNumber(strokeWidth, 1); mark.strokeOpacity = impliedNumber(cstrokeOpacity, 1); From 451d8b60a30eb5a2e4143401ad7bdb458d60ba34 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sun, 8 Aug 2021 13:51:46 -0700 Subject: [PATCH 07/20] common styles for rules --- src/marks/rule.js | 77 ++++++++++++++++++----------------------------- 1 file changed, 29 insertions(+), 48 deletions(-) diff --git a/src/marks/rule.js b/src/marks/rule.js index cf732e3519..145b06921b 100644 --- a/src/marks/rule.js +++ b/src/marks/rule.js @@ -1,49 +1,44 @@ import {create} from "d3"; import {filter} from "../defined.js"; -import {Mark, identity, maybeColor, title, number, maybeNumber} from "../mark.js"; -import {Style, applyDirectStyles, applyIndirectStyles, applyTransform, applyAttr} from "../style.js"; +import {Mark, identity, number} from "../mark.js"; +import {applyDirectStyles, applyIndirectStyles, applyTransform, applyChannelStyles} from "../style.js"; + +const defaults = { + fill: null, + stroke: "currentColor" +}; export class RuleX extends Mark { - constructor( - data, - { + constructor(data, options = {}) { + const { x, y1, y2, - title, - stroke, - strokeOpacity, inset = 0, insetTop = inset, - insetBottom = inset, - ...options - } = {} - ) { - const [vstroke, cstroke] = maybeColor(stroke, "currentColor"); - const [vstrokeOpacity, cstrokeOpacity] = maybeNumber(strokeOpacity); + insetBottom = inset + } = options; super( data, [ {name: "x", value: x, scale: "x", optional: true}, {name: "y1", value: y1, scale: "y", optional: true}, - {name: "y2", value: y2, scale: "y", optional: true}, - {name: "title", value: title, optional: true}, - {name: "stroke", value: vstroke, scale: "color", optional: true}, - {name: "strokeOpacity", value: vstrokeOpacity, scale: "opacity", optional: true} + {name: "y2", value: y2, scale: "y", optional: true} ], - options + options, + defaults ); - Style(this, {stroke: cstroke, strokeOpacity: cstrokeOpacity, ...options}); this.insetTop = number(insetTop); this.insetBottom = number(insetBottom); } render( I, {x, y}, - {x: X, y1: Y1, y2: Y2, title: L, stroke: S, strokeOpacity: SO}, + channels, {width, height, marginTop, marginRight, marginLeft, marginBottom} ) { - const index = filter(I, X, Y1, Y2, S); + const {x: X, y1: Y1, y2: Y2, stroke: S, strokeOpacity: SO} = channels; + const index = filter(I, X, Y1, Y2, S, SO); // TODO filter standard channels return create("svg:g") .call(applyIndirectStyles, this) .call(applyTransform, X && x, null, 0.5, 0) @@ -55,54 +50,42 @@ export class RuleX extends Mark { .attr("x2", X ? i => X[i] : (marginLeft + width - marginRight) / 2) .attr("y1", Y1 ? i => Y1[i] + this.insetTop : marginTop + this.insetTop) .attr("y2", Y2 ? (y.bandwidth ? i => Y2[i] + y.bandwidth() - this.insetBottom : i => Y2[i] - this.insetBottom) : height - marginBottom - this.insetBottom) - .call(applyAttr, "stroke", S && (i => S[i])) - .call(applyAttr, "stroke-opacity", SO && (i => SO[i])) - .call(title(L))) + .call(applyChannelStyles, channels)) .node(); } } export class RuleY extends Mark { - constructor( - data, - { + constructor(data, options = {}) { + const { x1, x2, y, - title, - stroke, - strokeOpacity, inset = 0, insetRight = inset, - insetLeft = inset, - ...options - } = {} - ) { - const [vstroke, cstroke] = maybeColor(stroke, "currentColor"); - const [vstrokeOpacity, cstrokeOpacity] = maybeNumber(strokeOpacity); + insetLeft = inset + } = options; super( data, [ {name: "y", value: y, scale: "y", optional: true}, {name: "x1", value: x1, scale: "x", optional: true}, - {name: "x2", value: x2, scale: "x", optional: true}, - {name: "title", value: title, optional: true}, - {name: "stroke", value: vstroke, scale: "color", optional: true}, - {name: "strokeOpacity", value: vstrokeOpacity, scale: "opacity", optional: true} + {name: "x2", value: x2, scale: "x", optional: true} ], - options + options, + defaults ); - Style(this, {stroke: cstroke, strokeOpacity: cstrokeOpacity, ...options}); this.insetRight = number(insetRight); this.insetLeft = number(insetLeft); } render( I, {x, y}, - {y: Y, x1: X1, x2: X2, title: L, stroke: S, strokeOpacity: SO}, + channels, {width, height, marginTop, marginRight, marginLeft, marginBottom} ) { - const index = filter(I, Y, X1, X2); + const {y: Y, x1: X1, x2: X2, stroke: S, strokeOpacity: SO} = channels; + const index = filter(I, Y, X1, X2, S, SO); // TODO filter standard channels return create("svg:g") .call(applyIndirectStyles, this) .call(applyTransform, null, Y && y, 0, 0.5) @@ -114,9 +97,7 @@ export class RuleY extends Mark { .attr("x2", X2 ? (x.bandwidth ? i => X2[i] + x.bandwidth() - this.insetRight : i => X2[i] - this.insetRight) : width - marginRight - this.insetRight) .attr("y1", Y ? i => Y[i] : (marginTop + height - marginBottom) / 2) .attr("y2", Y ? i => Y[i] : (marginTop + height - marginBottom) / 2) - .call(applyAttr, "stroke", S && (i => S[i])) - .call(applyAttr, "stroke-opacity", SO && (i => SO[i])) - .call(title(L))) + .call(applyChannelStyles, channels)) .node(); } } From ce0938d3f3eb003348c6986714b687f324d76b86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 9 Aug 2021 10:17:05 +0200 Subject: [PATCH 08/20] common styles for links --- src/marks/link.js | 58 +++++++++++++---------------------------------- 1 file changed, 16 insertions(+), 42 deletions(-) diff --git a/src/marks/link.js b/src/marks/link.js index 955bd104e5..fc1a4c3a7f 100644 --- a/src/marks/link.js +++ b/src/marks/link.js @@ -1,61 +1,37 @@ import {create, path} from "d3"; import {filter} from "../defined.js"; -import {Mark, maybeColor, maybeNumber, title} from "../mark.js"; +import {Mark} from "../mark.js"; import {Curve} from "../curve.js"; -import {Style, applyDirectStyles, applyIndirectStyles, applyTransform, applyAttr} from "../style.js"; +import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js"; +const defaults = { + fill: "none", + stroke: "currentColor", + strokeMiterlimit: 1 +}; export class Link extends Mark { - constructor( - data, - { - x1, - y1, - x2, - y2, - title, - fill, - fillOpacity, - stroke, - strokeOpacity, - curve, - ...options - } = {} - ) { - const [vfill, cfill] = maybeColor(fill, "none"); - const [vfillOpacity, cfillOpacity] = maybeNumber(fillOpacity); - const [vstroke, cstroke] = maybeColor(stroke, "currentColor"); - const [vstrokeOpacity, cstrokeOpacity] = maybeNumber(strokeOpacity); + constructor(data, options = {}) { + const {x1, y1, x2, y2, curve} = 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: "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: "y2", value: y2, scale: "y", optional: true} ], - options + options, + defaults ); this.curve = Curve(curve); - 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, title: L, stroke: S, strokeOpacity: SO} + channels ) { - const index = filter(I, X1, Y1, X2, Y2, S, SO); + const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, stroke: S, strokeOpacity: SO} = channels; + const index = filter(I, X1, Y1, X2, Y2, S, SO); // TODO filter standard channels return create("svg:g") .call(applyIndirectStyles, this) .call(applyTransform, x, y, 0.5, 0.5) @@ -72,9 +48,7 @@ export class Link extends Mark { c.lineEnd(); return p + ""; }) - .call(applyAttr, "stroke", S && (i => S[i])) - .call(applyAttr, "stroke-opacity", SO && (i => SO[i])) - .call(title(L))) + .call(applyChannelStyles, channels)) .node(); } } From df0f62070cbe8976883e46b41e1356fac5df559e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 9 Aug 2021 10:33:04 +0200 Subject: [PATCH 09/20] test frame --- test/marks/frame-test.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 test/marks/frame-test.js diff --git a/test/marks/frame-test.js b/test/marks/frame-test.js new file mode 100644 index 0000000000..51ea371ac5 --- /dev/null +++ b/test/marks/frame-test.js @@ -0,0 +1,34 @@ +import * as Plot from "@observablehq/plot"; +import assert from "assert"; + +it("frame(options) has the expected defaults", () => { + const frame = Plot.frame(); + assert.strictEqual(frame.data, undefined); + assert.strictEqual(frame.transform, undefined); + assert.deepStrictEqual(frame.channels, []); + assert.strictEqual(frame.fill, "none"); + assert.strictEqual(frame.fillOpacity, undefined); + assert.strictEqual(frame.stroke, "currentColor"); + assert.strictEqual(frame.strokeWidth, undefined); + assert.strictEqual(frame.strokeOpacity, undefined); + assert.strictEqual(frame.strokeLinejoin, undefined); + assert.strictEqual(frame.strokeLinecap, undefined); + assert.strictEqual(frame.strokeMiterlimit, undefined); + assert.strictEqual(frame.strokeDasharray, undefined); + assert.strictEqual(frame.mixBlendMode, undefined); + assert.strictEqual(frame.shapeRendering, undefined); + assert.strictEqual(frame.insetTop, 0); + assert.strictEqual(frame.insetRight, 0); + assert.strictEqual(frame.insetBottom, 0); + assert.strictEqual(frame.insetLeft, 0); +}); + +it("frame({fill}) allows fill to be a constant color", () => { + const frame = Plot.frame({fill: "red"}); + assert.strictEqual(frame.fill, "red"); +}); + +it("frame({stroke}) allows stroke to be a constant color", () => { + const frame = Plot.frame({stroke: "red"}); + assert.strictEqual(frame.stroke, "red"); +}); From 672c2f6f441a5262d51622891535d26161585ff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 9 Aug 2021 10:39:31 +0200 Subject: [PATCH 10/20] common styles for frames --- src/marks/frame.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/marks/frame.js b/src/marks/frame.js index b714525a29..fec4b32769 100644 --- a/src/marks/frame.js +++ b/src/marks/frame.js @@ -1,20 +1,22 @@ import {create} from "d3"; import {Mark, number} from "../mark.js"; -import {Style, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js"; +import {applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js"; + +const defaults = { + fill: "none", + stroke: "currentColor" +}; export class Frame extends Mark { - constructor({ - fill = "none", - stroke = fill === null || fill === "none" ? "currentColor" : "none", - inset = 0, - insetTop = inset, - insetRight = inset, - insetBottom = inset, - insetLeft = inset, - ...style - } = {}) { - super(); - Style(this, {fill, stroke, ...style}); + constructor(options = {}) { + const { + inset = 0, + insetTop = inset, + insetRight = inset, + insetBottom = inset, + insetLeft = inset + } = options; + super(undefined, undefined, options, defaults); this.insetTop = number(insetTop); this.insetRight = number(insetRight); this.insetBottom = number(insetBottom); From dcdca0f4ba421843f73523cf8b9ff95b67967d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 9 Aug 2021 11:03:47 +0200 Subject: [PATCH 11/20] common styles for dots --- src/marks/dot.js | 59 ++++++++++++++---------------------------------- src/style.js | 5 ++++ 2 files changed, 22 insertions(+), 42 deletions(-) diff --git a/src/marks/dot.js b/src/marks/dot.js index b1bfa83553..b54baa6c5f 100644 --- a/src/marks/dot.js +++ b/src/marks/dot.js @@ -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, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO} = channels; + let index = filter(I, X, Y, F, FO, S, SO); // TODO filter standard channels if (R) index = index.filter(i => positive(R[i])); return create("svg:g") .call(applyIndirectStyles, this) @@ -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(); } } diff --git a/src/style.js b/src/style.js index 16a4ab3d40..7d39d2b33e 100644 --- a/src/style.js +++ b/src/style.js @@ -26,6 +26,11 @@ export function styles( strokeMiterlimit: defaultStrokeMiterlimit } ) { + // some marks default stroke to undefined if fill is not none (e.g., dot) + if (defaultFill === "none" && defaultStroke === "currentColor") { + if (fill != null && fill !== "none") defaultStroke = null; + } + const [vstroke, cstroke] = maybeColor(stroke, defaultStroke); // some styles only apply if there is a stroke (either constant non-none, or channel) From a0c547c6301314c9775cb51b4e843d7ba4f43c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 9 Aug 2021 11:12:08 +0200 Subject: [PATCH 12/20] common styles for texts --- src/marks/text.js | 56 ++++++++++++++--------------------------------- 1 file changed, 17 insertions(+), 39 deletions(-) diff --git a/src/marks/text.js b/src/marks/text.js index 17649bef30..e4e8ec5c6e 100644 --- a/src/marks/text.js +++ b/src/marks/text.js @@ -1,20 +1,18 @@ import {create} from "d3"; import {filter, nonempty} from "../defined.js"; -import {Mark, indexOf, identity, string, title, maybeColor, maybeNumber, maybeTuple, numberChannel} from "../mark.js"; -import {Style, applyDirectStyles, applyIndirectStyles, applyAttr, applyTransform} from "../style.js"; +import {Mark, indexOf, identity, string, maybeNumber, maybeTuple, numberChannel} from "../mark.js"; +import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyAttr, applyTransform} from "../style.js"; + +const defaults = { + fill: "currentColor" +}; export class Text extends Mark { - constructor( - data, - { + constructor(data, options = {}) { + const { x, y, text = indexOf, - title, - fill, - fillOpacity, - stroke, - strokeOpacity, textAnchor, fontFamily, fontSize, @@ -23,14 +21,8 @@ export class Text extends Mark { fontWeight, dx, dy = "0.32em", - rotate, - ...options - } = {} - ) { - const [vstroke, cstroke] = maybeColor(stroke, "none"); - const [vstrokeOpacity, cstrokeOpacity] = maybeNumber(strokeOpacity); - const [vfill, cfill] = maybeColor(fill, "currentColor"); - const [vfillOpacity, cfillOpacity] = maybeNumber(fillOpacity); + rotate + } = options; const [vrotate, crotate] = maybeNumber(rotate, 0); const [vfontSize, cfontSize] = maybeNumber(fontSize); super( @@ -40,22 +32,11 @@ export class Text extends Mark { {name: "y", value: y, scale: "y", optional: true}, {name: "fontSize", value: numberChannel(vfontSize), optional: true}, {name: "rotate", value: numberChannel(vrotate), optional: true}, - {name: "text", value: text}, - {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: "text", value: text} ], - options + options, + defaults ); - Style(this, { - fill: cfill, - fillOpacity: cfillOpacity, - stroke: cstroke, - strokeOpacity: cstrokeOpacity, - ...options - }); this.rotate = crotate; this.textAnchor = string(textAnchor); this.fontFamily = string(fontFamily); @@ -69,11 +50,12 @@ export class Text extends Mark { render( I, {x, y}, - {x: X, y: Y, rotate: R, text: T, title: L, fill: F, fillOpacity: FO, fontSize: FS, stroke: S, strokeOpacity: SO}, + channels, {width, height, marginTop, marginRight, marginBottom, marginLeft} ) { + const {x: X, y: Y, rotate: R, text: T, fill: F, fillOpacity: FO, fontSize: FS} = channels; const {rotate} = this; - const index = filter(I, X, Y, F, FO, R).filter(i => nonempty(T[i])); + const index = filter(I, X, Y, F, FO, R).filter(i => nonempty(T[i])); // TODO filter standard channels const cx = (marginLeft + width - marginRight) / 2; const cy = (marginTop + height - marginBottom) / 2; return create("svg:g") @@ -92,13 +74,9 @@ export class Text extends Mark { : Y ? i => `translate(${cx},${Y[i]}) rotate(${rotate})` : `translate(${cx},${cy}) rotate(${rotate})`) : text => text.attr("x", X ? i => X[i] : cx).attr("y", Y ? i => Y[i] : cy)) - .call(applyAttr, "fill", F && (i => F[i])) - .call(applyAttr, "fill-opacity", FO && (i => FO[i])) .call(applyAttr, "font-size", FS && (i => FS[i])) - .call(applyAttr, "stroke", S && (i => S[i])) - .call(applyAttr, "stroke-opacity", SO && (i => SO[i])) .text(i => T[i]) - .call(title(L))) + .call(applyChannelStyles, channels)) .node(); } } From d20d5a991ec0d5ee7565982ffa78a03a02120200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 9 Aug 2021 11:25:50 +0200 Subject: [PATCH 13/20] remove obsolete Style function --- src/style.js | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/src/style.js b/src/style.js index 7d39d2b33e..e4063d1c96 100644 --- a/src/style.js +++ b/src/style.js @@ -72,33 +72,6 @@ export function styles( ]; } -// TODO remove me -export function Style(mark, { - fill, - fillOpacity, - stroke, - strokeWidth, - strokeOpacity, - strokeLinejoin, - strokeLinecap, - strokeMiterlimit, - strokeDasharray, - mixBlendMode, - shapeRendering -} = {}) { - mark.fill = impliedString(fill, "currentColor"); - mark.fillOpacity = impliedNumber(fillOpacity, 1); - mark.stroke = impliedString(stroke, "none"); - mark.strokeWidth = impliedNumber(strokeWidth, 1); - mark.strokeOpacity = impliedNumber(strokeOpacity, 1); - mark.strokeLinejoin = impliedString(strokeLinejoin, "miter"); - mark.strokeLinecap = impliedString(strokeLinecap, "butt"); - mark.strokeMiterlimit = impliedNumber(strokeMiterlimit, 4); - mark.strokeDasharray = string(strokeDasharray); - mark.mixBlendMode = impliedString(mixBlendMode, "normal"); - mark.shapeRendering = impliedString(shapeRendering, "auto"); -} - export function applyChannelStyles(selection, {title: L, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO}) { applyAttr(selection, "fill", F && (i => F[i])); applyAttr(selection, "fill-opacity", FO && (i => FO[i])); From 3d0d16cfaf7d58c617bba7df0bef9d3b9f9061f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 9 Aug 2021 11:47:13 +0200 Subject: [PATCH 14/20] filter common styles --- src/marks/bar.js | 3 +-- src/marks/dot.js | 4 ++-- src/marks/link.js | 4 ++-- src/marks/rect.js | 4 ++-- src/marks/rule.js | 8 ++++---- src/marks/text.js | 4 ++-- src/marks/tick.js | 4 ++-- src/plot.js | 6 ++++-- 8 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/marks/bar.js b/src/marks/bar.js index 2bcf6508d9..7395929c9f 100644 --- a/src/marks/bar.js +++ b/src/marks/bar.js @@ -19,8 +19,7 @@ export class AbstractBar extends Mark { } render(I, scales, channels, dimensions) { const {rx, ry} = this; - const {fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO} = channels; - const index = filter(I, ...this._positions(channels), F, FO, S, SO); // TODO filter standard channels + const index = filter(I, ...this._positions(channels)); return create("svg:g") .call(applyIndirectStyles, this) .call(this._transform, scales) diff --git a/src/marks/dot.js b/src/marks/dot.js index b54baa6c5f..33158bfc10 100644 --- a/src/marks/dot.js +++ b/src/marks/dot.js @@ -31,8 +31,8 @@ export class Dot extends Mark { channels, {width, height, marginTop, marginRight, marginBottom, marginLeft} ) { - const {x: X, y: Y, r: R, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO} = channels; - let index = filter(I, X, Y, F, FO, S, SO); // TODO filter standard channels + 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) diff --git a/src/marks/link.js b/src/marks/link.js index fc1a4c3a7f..186a9f44d2 100644 --- a/src/marks/link.js +++ b/src/marks/link.js @@ -30,8 +30,8 @@ export class Link extends Mark { {x, y}, channels ) { - const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, stroke: S, strokeOpacity: SO} = channels; - const index = filter(I, X1, Y1, X2, Y2, S, SO); // TODO filter standard channels + const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels; + const index = filter(I, X1, Y1, X2, Y2); return create("svg:g") .call(applyIndirectStyles, this) .call(applyTransform, x, y, 0.5, 0.5) diff --git a/src/marks/rect.js b/src/marks/rect.js index e42eb13a67..9a3a4ebd75 100644 --- a/src/marks/rect.js +++ b/src/marks/rect.js @@ -40,9 +40,9 @@ export class Rect extends Mark { this.ry = impliedString(ry, "auto"); } render(I, {x, y}, channels) { - const {x1: X1, y1: Y1, x2: X2, y2: Y2, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO} = channels; + const {x1: X1, y1: Y1, x2: X2, y2: Y2} = channels; const {rx, ry} = this; - const index = filter(I, X1, Y2, X2, Y2, F, FO, S, SO); // TODO filter standard channels + const index = filter(I, X1, Y2, X2, Y2); return create("svg:g") .call(applyIndirectStyles, this) .call(applyTransform, x, y) diff --git a/src/marks/rule.js b/src/marks/rule.js index 145b06921b..a989f620c8 100644 --- a/src/marks/rule.js +++ b/src/marks/rule.js @@ -37,8 +37,8 @@ export class RuleX extends Mark { channels, {width, height, marginTop, marginRight, marginLeft, marginBottom} ) { - const {x: X, y1: Y1, y2: Y2, stroke: S, strokeOpacity: SO} = channels; - const index = filter(I, X, Y1, Y2, S, SO); // TODO filter standard channels + const {x: X, y1: Y1, y2: Y2} = channels; + const index = filter(I, X, Y1, Y2); return create("svg:g") .call(applyIndirectStyles, this) .call(applyTransform, X && x, null, 0.5, 0) @@ -84,8 +84,8 @@ export class RuleY extends Mark { channels, {width, height, marginTop, marginRight, marginLeft, marginBottom} ) { - const {y: Y, x1: X1, x2: X2, stroke: S, strokeOpacity: SO} = channels; - const index = filter(I, Y, X1, X2, S, SO); // TODO filter standard channels + const {y: Y, x1: X1, x2: X2} = channels; + const index = filter(I, Y, X1, X2); return create("svg:g") .call(applyIndirectStyles, this) .call(applyTransform, null, Y && y, 0, 0.5) diff --git a/src/marks/text.js b/src/marks/text.js index e4e8ec5c6e..f5d1790832 100644 --- a/src/marks/text.js +++ b/src/marks/text.js @@ -53,9 +53,9 @@ export class Text extends Mark { channels, {width, height, marginTop, marginRight, marginBottom, marginLeft} ) { - const {x: X, y: Y, rotate: R, text: T, fill: F, fillOpacity: FO, fontSize: FS} = channels; + const {x: X, y: Y, rotate: R, text: T, fontSize: FS} = channels; const {rotate} = this; - const index = filter(I, X, Y, F, FO, R).filter(i => nonempty(T[i])); // TODO filter standard channels + const index = filter(I, X, Y, R).filter(i => nonempty(T[i])); const cx = (marginLeft + width - marginRight) / 2; const cy = (marginTop + height - marginBottom) / 2; return create("svg:g") diff --git a/src/marks/tick.js b/src/marks/tick.js index d344d0c030..9d83fcaea0 100644 --- a/src/marks/tick.js +++ b/src/marks/tick.js @@ -13,8 +13,8 @@ class AbstractTick extends Mark { super(data, channels, options, defaults); } render(I, scales, channels, dimensions) { - const {x: X, y: Y, stroke: S, strokeOpacity: SO} = channels; - const index = filter(I, X, Y, S, SO); // TODO filter standard channels + const {x: X, y: Y} = channels; + const index = filter(I, X, Y); return create("svg:g") .call(applyIndirectStyles, this) .call(this._transform, scales) diff --git a/src/plot.js b/src/plot.js index 06bda82abf..cf08e1a92f 100644 --- a/src/plot.js +++ b/src/plot.js @@ -1,5 +1,6 @@ import {create} from "d3"; import {Axes, autoAxisTicks, autoAxisLabels} from "./axes.js"; +import {filter} from "./defined.js"; import {facets} from "./facet.js"; import {values} from "./mark.js"; import {Scales, autoScaleRange} from "./scales.js"; @@ -83,8 +84,9 @@ export function plot(options = {}) { for (const mark of marks) { const channels = markChannels.get(mark); - const index = markIndex.get(mark); - const node = mark.render(index, scales, values(channels, scales), dimensions, axes); + const scaled = values(channels, scales); + const index = filter(markIndex.get(mark), scaled.fill, scaled.fillOpacity, scaled.stroke, scaled.strokeOpacity); + const node = mark.render(index, scales, scaled, dimensions, axes); if (node != null) svg.appendChild(node); } From 479932648ffb43bdac99790668fed29187aeca29 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 9 Aug 2021 10:16:10 -0700 Subject: [PATCH 15/20] =?UTF-8?q?values=20=E2=86=A6=20applyScales?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/facet.js | 4 ++-- src/mark.js | 2 +- src/plot.js | 9 +++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/facet.js b/src/facet.js index 051aee2468..8184277435 100644 --- a/src/facet.js +++ b/src/facet.js @@ -1,6 +1,6 @@ import {cross, difference, groups, InternMap} from "d3"; import {create} from "d3"; -import {Mark, values, first, second} from "./mark.js"; +import {Mark, first, second, applyScales} from "./mark.js"; export function facets(data, {x, y, ...options}, marks) { return x === undefined && y === undefined @@ -76,7 +76,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) { diff --git a/src/mark.js b/src/mark.js index 228f3fcb75..f39a5c17f5 100644 --- a/src/mark.js +++ b/src/mark.js @@ -313,7 +313,7 @@ export function numberChannel(source) { } // TODO use Float64Array.from for position and radius scales? -export function values(channels = [], scales) { +export function applyScales(channels = [], scales) { const values = Object.create(null); for (let [name, {value, scale}] of channels) { if (name !== undefined) { diff --git a/src/plot.js b/src/plot.js index cf08e1a92f..220ef0a19e 100644 --- a/src/plot.js +++ b/src/plot.js @@ -2,7 +2,7 @@ import {create} from "d3"; import {Axes, autoAxisTicks, autoAxisLabels} from "./axes.js"; import {filter} from "./defined.js"; import {facets} from "./facet.js"; -import {values} from "./mark.js"; +import {applyScales} from "./mark.js"; import {Scales, autoScaleRange} from "./scales.js"; import {offset} from "./style.js"; @@ -84,9 +84,10 @@ export function plot(options = {}) { for (const mark of marks) { const channels = markChannels.get(mark); - const scaled = values(channels, scales); - const index = filter(markIndex.get(mark), scaled.fill, scaled.fillOpacity, scaled.stroke, scaled.strokeOpacity); - const node = mark.render(index, scales, scaled, dimensions, axes); + const values = applyScales(channels, scales); + const {fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO} = values; + const index = filter(markIndex.get(mark), F, FO, S, SO); + const node = mark.render(index, scales, values, dimensions, axes); if (node != null) svg.appendChild(node); } From 9ed2359dc504e841c8956063bb7e2fca2fe76c26 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 9 Aug 2021 10:18:02 -0700 Subject: [PATCH 16/20] prettier --- src/marks/link.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/marks/link.js b/src/marks/link.js index 186a9f44d2..96997adb45 100644 --- a/src/marks/link.js +++ b/src/marks/link.js @@ -9,6 +9,7 @@ const defaults = { stroke: "currentColor", strokeMiterlimit: 1 }; + export class Link extends Mark { constructor(data, options = {}) { const {x1, y1, x2, y2, curve} = options; @@ -25,11 +26,7 @@ export class Link extends Mark { ); this.curve = Curve(curve); } - render( - I, - {x, y}, - channels - ) { + render(I, {x, y}, channels) { const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels; const index = filter(I, X1, Y1, X2, Y2); return create("svg:g") From d30973647dfca41764c48094cc2b5d594058bfe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 9 Aug 2021 19:38:42 +0200 Subject: [PATCH 17/20] filter facets (and centralize the filtering of common styles in style.js) --- src/facet.js | 7 +++++-- src/plot.js | 6 ++---- src/style.js | 6 ++++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/facet.js b/src/facet.js index 8184277435..7aa3ed5fd0 100644 --- a/src/facet.js +++ b/src/facet.js @@ -1,6 +1,7 @@ import {cross, difference, groups, InternMap} from "d3"; import {create} from "d3"; import {Mark, first, second, applyScales} from "./mark.js"; +import {filterStyles} from "./style.js"; export function facets(data, {x, y, ...options}, marks) { return x === undefined && y === undefined @@ -110,10 +111,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); diff --git a/src/plot.js b/src/plot.js index 220ef0a19e..361c65165a 100644 --- a/src/plot.js +++ b/src/plot.js @@ -1,10 +1,9 @@ import {create} from "d3"; import {Axes, autoAxisTicks, autoAxisLabels} from "./axes.js"; -import {filter} from "./defined.js"; import {facets} from "./facet.js"; import {applyScales} from "./mark.js"; import {Scales, autoScaleRange} from "./scales.js"; -import {offset} from "./style.js"; +import {filterStyles, offset} from "./style.js"; export function plot(options = {}) { const {facet, style, caption} = options; @@ -85,8 +84,7 @@ export function plot(options = {}) { for (const mark of marks) { const channels = markChannels.get(mark); const values = applyScales(channels, scales); - const {fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO} = values; - const index = filter(markIndex.get(mark), F, FO, S, SO); + const index = filterStyles(markIndex.get(mark), values); const node = mark.render(index, scales, values, dimensions, axes); if (node != null) svg.appendChild(node); } diff --git a/src/style.js b/src/style.js index e4063d1c96..a664991780 100644 --- a/src/style.js +++ b/src/style.js @@ -1,4 +1,5 @@ import {string, number, maybeColor, maybeNumber, title, titleGroup} from "./mark.js"; +import {filter} from "./defined.js"; export const offset = typeof window !== "undefined" && window.devicePixelRatio > 1 ? 0 : 0.5; @@ -128,3 +129,8 @@ export function impliedString(value, impliedValue) { export function impliedNumber(value, impliedValue) { if ((value = number(value)) !== impliedValue) return value; } + +export function filterStyles(index, values) { + const {fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO} = values; + return filter(index, F, FO, S, SO); +} From 5f8f6feece462ed72328dbf1a7d0d82cf9b96a08 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 9 Aug 2021 11:49:26 -0700 Subject: [PATCH 18/20] normalize fill and stroke defaults --- src/marks/text.js | 4 +--- src/style.js | 46 ++++++++++++++++++++++++++++++---------------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/marks/text.js b/src/marks/text.js index f5d1790832..9daf2afab9 100644 --- a/src/marks/text.js +++ b/src/marks/text.js @@ -3,9 +3,7 @@ import {filter, nonempty} from "../defined.js"; import {Mark, indexOf, identity, string, maybeNumber, maybeTuple, numberChannel} from "../mark.js"; import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyAttr, applyTransform} from "../style.js"; -const defaults = { - fill: "currentColor" -}; +const defaults = {}; export class Text extends Mark { constructor(data, options = {}) { diff --git a/src/style.js b/src/style.js index a664991780..e71aba7f0b 100644 --- a/src/style.js +++ b/src/style.js @@ -27,33 +27,43 @@ export function styles( strokeMiterlimit: defaultStrokeMiterlimit } ) { - // some marks default stroke to undefined if fill is not none (e.g., dot) - if (defaultFill === "none" && defaultStroke === "currentColor") { - if (fill != null && fill !== "none") defaultStroke = null; - } - - const [vstroke, cstroke] = maybeColor(stroke, defaultStroke); - // some styles only apply if there is a stroke (either constant non-none, or channel) - if (cstroke !== "none") { - if (defaultFill === "currentColor") defaultFill = "none"; - if (strokeWidth === undefined) strokeWidth = defaultStrokeWidth; - if (strokeMiterlimit === undefined) strokeMiterlimit = defaultStrokeMiterlimit; - } - - // some marks don’t support fill (e.g., tick) + // Some marks don’t support fill (e.g., tick and rule). if (defaultFill === null) { fill = null; fillOpacity = null; } + // Some marks default to fill with no stroke, while others default to stroke + // with no fill. For example, bar and area default to fill, while dot and line + // default to stroke. For marks that fill by default, the default fill only + // applies if the stroke is (constant) none; if you set a stroke, then the + // default fill becomes none. Similarly for marks that stroke by stroke, the + // default stroke only applies if the fill is (constant) none. + if (none(defaultFill)) { + if (!none(defaultStroke) && !none(fill)) defaultStroke = "none"; + } else { + if (none(defaultStroke) && !none(stroke)) defaultFill = "none"; + } + const [vfill, cfill] = maybeColor(fill, defaultFill); const [vfillOpacity, cfillOpacity] = maybeNumber(fillOpacity); + const [vstroke, cstroke] = maybeColor(stroke, defaultStroke); const [vstrokeOpacity, cstrokeOpacity] = maybeNumber(strokeOpacity); + + // For styles that have no effect if there is no stroke, only apply the + // defaults if the stroke is not (constant) none. + if (cstroke !== "none") { + if (strokeWidth === undefined) strokeWidth = defaultStrokeWidth; + if (strokeMiterlimit === undefined) strokeMiterlimit = defaultStrokeMiterlimit; + } + + // Some marks don’t support fill (e.g., tick and rule). if (defaultFill !== null) { mark.fill = impliedString(cfill, "currentColor"); mark.fillOpacity = impliedNumber(cfillOpacity, 1); } + mark.stroke = impliedString(cstroke, "none"); mark.strokeWidth = impliedNumber(strokeWidth, 1); mark.strokeOpacity = impliedNumber(cstrokeOpacity, 1); @@ -63,6 +73,7 @@ export function styles( mark.strokeDasharray = string(strokeDasharray); mark.mixBlendMode = impliedString(mixBlendMode, "normal"); mark.shapeRendering = impliedString(shapeRendering, "auto"); + return [ ...channels, {name: "title", value: title, optional: true}, @@ -130,7 +141,10 @@ export function impliedNumber(value, impliedValue) { if ((value = number(value)) !== impliedValue) return value; } -export function filterStyles(index, values) { - const {fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO} = values; +export function filterStyles(index, {fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO}) { return filter(index, F, FO, S, SO); } + +function none(color) { + return color == null || color === "none"; +} From ab880ccc0db0bff9a1738a990d9a74845efd03c8 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 9 Aug 2021 12:29:50 -0700 Subject: [PATCH 19/20] move applyScales --- src/facet.js | 3 ++- src/mark.js | 17 ----------------- src/plot.js | 3 +-- src/scales.js | 17 +++++++++++++++++ 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/facet.js b/src/facet.js index 7aa3ed5fd0..936794f121 100644 --- a/src/facet.js +++ b/src/facet.js @@ -1,6 +1,7 @@ import {cross, difference, groups, InternMap} from "d3"; import {create} from "d3"; -import {Mark, first, second, applyScales} 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) { diff --git a/src/mark.js b/src/mark.js index f39a5c17f5..662749574a 100644 --- a/src/mark.js +++ b/src/mark.js @@ -312,23 +312,6 @@ export function numberChannel(source) { }; } -// TODO use Float64Array.from for position and radius scales? -export function applyScales(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; diff --git a/src/plot.js b/src/plot.js index 361c65165a..777cd76a81 100644 --- a/src/plot.js +++ b/src/plot.js @@ -1,8 +1,7 @@ import {create} from "d3"; import {Axes, autoAxisTicks, autoAxisLabels} from "./axes.js"; import {facets} from "./facet.js"; -import {applyScales} from "./mark.js"; -import {Scales, autoScaleRange} from "./scales.js"; +import {Scales, autoScaleRange, applyScales} from "./scales.js"; import {filterStyles, offset} from "./style.js"; export function plot(options = {}) { diff --git a/src/scales.js b/src/scales.js index 6a2b328260..810cd0649a 100644 --- a/src/scales.js +++ b/src/scales.js @@ -112,3 +112,20 @@ function inferScaleType(key, channels, {type, domain, range}) { function asOrdinalType(key) { return registry.get(key) === position ? "point" : "ordinal"; } + +// TODO use Float64Array.from for position and radius scales? +export function applyScales(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; +} From d8809cccc482df867879e331002019cb5dc57fee Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Mon, 9 Aug 2021 12:31:30 -0700 Subject: [PATCH 20/20] test frame stroke and fill --- test/marks/frame-test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/marks/frame-test.js b/test/marks/frame-test.js index 51ea371ac5..7fbac5bca2 100644 --- a/test/marks/frame-test.js +++ b/test/marks/frame-test.js @@ -26,9 +26,11 @@ it("frame(options) has the expected defaults", () => { it("frame({fill}) allows fill to be a constant color", () => { const frame = Plot.frame({fill: "red"}); assert.strictEqual(frame.fill, "red"); + assert.strictEqual(frame.stroke, undefined); }); it("frame({stroke}) allows stroke to be a constant color", () => { const frame = Plot.frame({stroke: "red"}); assert.strictEqual(frame.stroke, "red"); + assert.strictEqual(frame.fill, "none"); });