From 9904cd55d62854bb189756b3e59dd33db5b87d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Tue, 31 Aug 2021 15:21:37 +0200 Subject: [PATCH 1/2] rebased --- src/axis.js | 16 +++++++++++---- src/facet.js | 6 ++++-- src/plot.js | 4 ++-- src/style.js | 3 +++ test/output/penguinSexMassCulmenSpecies.svg | 20 +++++++++---------- test/plots/penguin-sex-mass-culmen-species.js | 16 +++++++++++---- 6 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/axis.js b/src/axis.js index 18f296cab8..41f6946233 100644 --- a/src/axis.js +++ b/src/axis.js @@ -15,7 +15,8 @@ export class AxisX { labelAnchor, labelOffset, line, - tickRotate + tickRotate, + className } = {}) { this.name = name; this.axis = keyword(axis, "axis", ["top", "bottom"]); @@ -29,6 +30,7 @@ export class AxisX { this.labelOffset = number(labelOffset); this.line = boolean(line); this.tickRotate = number(tickRotate); + this.className = className; } render( index, @@ -54,12 +56,14 @@ export class AxisX { labelAnchor, labelOffset, line, - tickRotate + tickRotate, + className } = this; const offset = this.name === "x" ? 0 : axis === "top" ? marginTop - facetMarginTop : marginBottom - facetMarginBottom; const offsetSign = axis === "top" ? -1 : 1; const ty = offsetSign * offset + (axis === "top" ? marginTop : height - marginBottom); return create("svg:g") + .attr("class", className) .attr("transform", `translate(0,${ty})`) .call(createAxis(axis === "top" ? axisTop : axisBottom, x, this)) .call(maybeTickRotate, tickRotate) @@ -98,7 +102,8 @@ export class AxisY { labelAnchor, labelOffset, line, - tickRotate + tickRotate, + className } = {}) { this.name = name; this.axis = keyword(axis, "axis", ["left", "right"]); @@ -112,6 +117,7 @@ export class AxisY { this.labelOffset = number(labelOffset); this.line = boolean(line); this.tickRotate = number(tickRotate); + this.className = className; } render( index, @@ -135,12 +141,14 @@ export class AxisY { labelAnchor, labelOffset, line, - tickRotate + tickRotate, + className } = this; const offset = this.name === "y" ? 0 : axis === "left" ? marginLeft - facetMarginLeft : marginRight - facetMarginRight; const offsetSign = axis === "left" ? -1 : 1; const tx = offsetSign * offset + (axis === "right" ? width - marginRight : marginLeft); return create("svg:g") + .attr("class", className) .attr("transform", `translate(${tx},0)`) .call(createAxis(axis === "right" ? axisRight : axisLeft, y, this)) .call(maybeTickRotate, tickRotate) diff --git a/src/facet.js b/src/facet.js index 62b198f2b4..cc26032090 100644 --- a/src/facet.js +++ b/src/facet.js @@ -11,7 +11,7 @@ export function facets(data, {x, y, ...options}, marks) { } class Facet extends Mark { - constructor(data, {x, y, ...options} = {}, marks = []) { + constructor(data, {x, y, className, ...options} = {}, marks = []) { if (data == null) throw new Error("missing facet data"); super( data, @@ -25,6 +25,7 @@ class Facet extends Mark { // The following fields are set by initialize: this.marksChannels = undefined; // array of mark channels this.marksIndexByFacet = undefined; // map from facet key to array of mark indexes + this.className = className; } initialize() { const {index, channels} = super.initialize(); @@ -69,7 +70,7 @@ class Facet extends Mark { return {index, channels: [...channels, ...subchannels]}; } render(I, scales, channels, dimensions, axes) { - const {marks, marksChannels, marksIndexByFacet} = this; + const {marks, marksChannels, marksIndexByFacet, className} = this; const {fx, fy} = scales; const fyDomain = fy && fy.domain(); const fxDomain = fx && fx.domain(); @@ -78,6 +79,7 @@ class Facet extends Mark { const subdimensions = {...dimensions, ...fxMargins, ...fyMargins}; const marksValues = marksChannels.map(channels => applyScales(channels, scales)); return create("svg:g") + .attr("class", className) .call(g => { if (fy && axes.y) { const axis1 = axes.y, axis2 = nolabel(axis1); diff --git a/src/plot.js b/src/plot.js index e2d526853f..681c8953aa 100644 --- a/src/plot.js +++ b/src/plot.js @@ -6,7 +6,7 @@ import {Scales, autoScaleRange, applyScales} from "./scales.js"; import {filterStyles, offset} from "./style.js"; export function plot(options = {}) { - const {facet, style, caption} = options; + const {facet, style, caption, className = "plot"} = options; // When faceting, wrap all marks in a faceting mark. if (facet !== undefined) { @@ -69,7 +69,7 @@ export function plot(options = {}) { const {width, height} = dimensions; const svg = create("svg") - .attr("class", "plot") + .attr("class", className) .attr("fill", "currentColor") .attr("text-anchor", "middle") .attr("width", width) diff --git a/src/style.js b/src/style.js index 57181e74b2..d795b89470 100644 --- a/src/style.js +++ b/src/style.js @@ -6,6 +6,7 @@ export const offset = typeof window !== "undefined" && window.devicePixelRatio > export function styles( mark, { + className, title, fill, fillOpacity, @@ -66,6 +67,7 @@ export function styles( mark.fillOpacity = impliedNumber(cfillOpacity, 1); } + mark.className = string(className); mark.stroke = impliedString(cstroke, "none"); mark.strokeWidth = impliedNumber(cstrokeWidth, 1); mark.strokeOpacity = impliedNumber(cstrokeOpacity, 1); @@ -106,6 +108,7 @@ export function applyGroupedChannelStyles(selection, {title: L, fill: F, fillOpa } export function applyIndirectStyles(selection, mark) { + applyAttr(selection, "class", mark.className); applyAttr(selection, "fill", mark.fill); applyAttr(selection, "fill-opacity", mark.fillOpacity); applyAttr(selection, "stroke", mark.stroke); diff --git a/test/output/penguinSexMassCulmenSpecies.svg b/test/output/penguinSexMassCulmenSpecies.svg index 6775ee14c6..809f9e5807 100644 --- a/test/output/penguinSexMassCulmenSpecies.svg +++ b/test/output/penguinSexMassCulmenSpecies.svg @@ -1,5 +1,5 @@ - - + + 34 @@ -53,7 +53,7 @@ ↑ culmen_length_mm - + FEMALE @@ -64,9 +64,9 @@ sex - + - + 3k @@ -98,7 +98,7 @@ - + 3k @@ -130,7 +130,7 @@ - + 3k @@ -163,7 +163,7 @@ - + @@ -213,7 +213,7 @@ - + @@ -260,7 +260,7 @@ - + diff --git a/test/plots/penguin-sex-mass-culmen-species.js b/test/plots/penguin-sex-mass-culmen-species.js index 3d47f1be68..f7515c4c18 100644 --- a/test/plots/penguin-sex-mass-culmen-species.js +++ b/test/plots/penguin-sex-mass-culmen-species.js @@ -7,16 +7,23 @@ export default async function() { inset: 10, height: 320, grid: true, + className: "plot classtest", x: { ticks: 10, - tickFormat: "~s" + tickFormat: "~s", + className: "axis-x" }, y: { - ticks: 10 + ticks: 10, + className: "axis-y" }, facet: { data, - x: "sex" + x: "sex", + className: "facet" + }, + fx: { + className: "axis-fx" }, marks: [ Plot.frame(), @@ -29,7 +36,8 @@ export default async function() { y: "culmen_length_mm", stroke: "species", fill: "species", - fillOpacity: 0.2 + fillOpacity: 0.2, + className: "bin" })) ] }); From 4fd65f46ca63f0e2a1e155504438594b64cc3368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 25 Oct 2021 13:09:35 +0200 Subject: [PATCH 2/2] adopt validateClassName --- src/axis.js | 6 +++--- src/facet.js | 5 +++-- src/plot.js | 5 +++-- src/style.js | 11 +++++++++++ test/output/penguinSexMassCulmenSpecies.svg | 2 +- test/plots/penguin-sex-mass-culmen-species.js | 2 +- 6 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/axis.js b/src/axis.js index 41f6946233..4ec3ec1840 100644 --- a/src/axis.js +++ b/src/axis.js @@ -1,7 +1,7 @@ import {axisTop, axisBottom, axisRight, axisLeft, create, format, utcFormat} from "d3"; import {formatIsoDate} from "./format.js"; import {boolean, take, number, string, keyword, maybeKeyword, constant, isTemporal} from "./mark.js"; - +import {validateClassName} from "./style.js"; export class AxisX { constructor({ name = "x", @@ -30,7 +30,7 @@ export class AxisX { this.labelOffset = number(labelOffset); this.line = boolean(line); this.tickRotate = number(tickRotate); - this.className = className; + this.className = validateClassName(className); } render( index, @@ -117,7 +117,7 @@ export class AxisY { this.labelOffset = number(labelOffset); this.line = boolean(line); this.tickRotate = number(tickRotate); - this.className = className; + this.className = validateClassName(className); } render( index, diff --git a/src/facet.js b/src/facet.js index cc26032090..0238c0ec24 100644 --- a/src/facet.js +++ b/src/facet.js @@ -2,7 +2,7 @@ import {cross, difference, groups, InternMap} from "d3"; import {create} from "d3"; import {Mark, first, second, markify, where} from "./mark.js"; import {applyScales} from "./scales.js"; -import {filterStyles} from "./style.js"; +import {filterStyles, validateClassName} from "./style.js"; export function facets(data, {x, y, ...options}, marks) { return x === undefined && y === undefined @@ -70,7 +70,8 @@ class Facet extends Mark { return {index, channels: [...channels, ...subchannels]}; } render(I, scales, channels, dimensions, axes) { - const {marks, marksChannels, marksIndexByFacet, className} = this; + const {marks, marksChannels, marksIndexByFacet} = this; + const className = validateClassName(this.className); const {fx, fy} = scales; const fyDomain = fy && fy.domain(); const fxDomain = fx && fx.domain(); diff --git a/src/plot.js b/src/plot.js index 681c8953aa..51507ba89a 100644 --- a/src/plot.js +++ b/src/plot.js @@ -3,10 +3,11 @@ import {Axes, autoAxisTicks, autoAxisLabels} from "./axes.js"; import {facets} from "./facet.js"; import {markify} from "./mark.js"; import {Scales, autoScaleRange, applyScales} from "./scales.js"; -import {filterStyles, offset} from "./style.js"; +import {filterStyles, offset, validateClassName} from "./style.js"; export function plot(options = {}) { - const {facet, style, caption, className = "plot"} = options; + const {facet, style, caption} = options; + const className = validateClassName(options.className === undefined ? "plot" : options.className); // When faceting, wrap all marks in a faceting mark. if (facet !== undefined) { diff --git a/src/style.js b/src/style.js index d795b89470..f8883f15ac 100644 --- a/src/style.js +++ b/src/style.js @@ -156,3 +156,14 @@ export function filterStyles(index, {fill: F, fillOpacity: FO, stroke: S, stroke function none(color) { return color == null || color === "none"; } + +function validClassName(str) { + return typeof str === "string" && + !!str.match(/^-?([_a-z]|[\240-\377]|\\[0-9a-f]{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-f])([_a-z0-9-]|[\240-\377]|\\[0-9a-f]{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-f])*$/); +} + +export function validateClassName(str) { + if (str == null) return null; + if (validClassName(str)) return str; + throw new Error(`Invalid class name ${str}`); +} diff --git a/test/output/penguinSexMassCulmenSpecies.svg b/test/output/penguinSexMassCulmenSpecies.svg index 809f9e5807..e41d59937a 100644 --- a/test/output/penguinSexMassCulmenSpecies.svg +++ b/test/output/penguinSexMassCulmenSpecies.svg @@ -1,4 +1,4 @@ - + 34 diff --git a/test/plots/penguin-sex-mass-culmen-species.js b/test/plots/penguin-sex-mass-culmen-species.js index f7515c4c18..7b901bd89e 100644 --- a/test/plots/penguin-sex-mass-culmen-species.js +++ b/test/plots/penguin-sex-mass-culmen-species.js @@ -7,7 +7,7 @@ export default async function() { inset: 10, height: 320, grid: true, - className: "plot classtest", + className: "classtest", x: { ticks: 10, tickFormat: "~s",