From 7a01a34539b06751f3fcc0de74780d119281dfb4 Mon Sep 17 00:00:00 2001 From: Robert Lesser Date: Sat, 22 Oct 2022 19:07:25 -0400 Subject: [PATCH 1/5] add className to style options --- src/plot.js | 3 ++- src/style.js | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/plot.js b/src/plot.js index 0e53efdce0..0014d25e6c 100644 --- a/src/plot.js +++ b/src/plot.js @@ -19,7 +19,7 @@ import { } from "./options.js"; import {Scales, ScaleFunctions, autoScaleRange, exposeScales} from "./scales.js"; import {position, registry as scaleRegistry} from "./scales/index.js"; -import {applyInlineStyles, maybeClassName, maybeClip, styles} from "./style.js"; +import {applyInlineStyles, maybeClassName, maybeClassNameOptional, maybeClip, styles} from "./style.js"; import {basic, initializer} from "./transforms/basic.js"; import {maybeInterval} from "./transforms/interval.js"; import {consumeWarnings, warn} from "./warnings.js"; @@ -683,6 +683,7 @@ export class Mark { this.dx = +dx || 0; this.dy = +dy || 0; this.clip = maybeClip(clip); + this.className = maybeClassNameOptional(options.className); } initialize(facets, facetChannels) { let data = arrayify(this.data); diff --git a/src/style.js b/src/style.js index 3547c50f1e..bb5e40a246 100644 --- a/src/style.js +++ b/src/style.js @@ -297,6 +297,7 @@ export function applyIndirectStyles(selection, mark, scales, dimensions) { applyAttr(selection, "aria-label", mark.ariaLabel); applyAttr(selection, "aria-description", mark.ariaDescription); applyAttr(selection, "aria-hidden", mark.ariaHidden); + applyAttr(selection, "class", mark.className); applyAttr(selection, "fill", mark.fill); applyAttr(selection, "fill-opacity", mark.fillOpacity); applyAttr(selection, "stroke", mark.stroke); @@ -378,6 +379,10 @@ export function maybeClassName(name) { return name; } +export function maybeClassNameOptional(name) { + return name === undefined ? undefined : maybeClassName(name); +} + export function applyInlineStyles(selection, style) { if (typeof style === "string") { selection.property("style", style); From c5a5e128ed92620aaf3fa5c8b46b3a2d0596e582 Mon Sep 17 00:00:00 2001 From: Robert Lesser Date: Sat, 22 Oct 2022 19:07:43 -0400 Subject: [PATCH 2/5] new test for className option --- test/output/classNameOnMarks.svg | 74 ++++++++++++++++++++++++++++++++ test/plots/className-on-marks.js | 40 +++++++++++++++++ test/plots/index.js | 1 + 3 files changed, 115 insertions(+) create mode 100644 test/output/classNameOnMarks.svg create mode 100644 test/plots/className-on-marks.js diff --git a/test/output/classNameOnMarks.svg b/test/output/classNameOnMarks.svg new file mode 100644 index 0000000000..221b4ef278 --- /dev/null +++ b/test/output/classNameOnMarks.svg @@ -0,0 +1,74 @@ + + + + + bananas + + + oranges + + + grapes + + + apples + + + + + 0 + + + 2 + + + 4 + + + 6 + + + 8 + + + 10 + + + 12 + + + 14 + + + 16 + + + 18 + + + 20 + units → + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/className-on-marks.js b/test/plots/className-on-marks.js new file mode 100644 index 0000000000..450ab109dc --- /dev/null +++ b/test/plots/className-on-marks.js @@ -0,0 +1,40 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export default async function () { + const sales = await d3.csv("data/fruit-sales.csv", d3.autoType); + const plotSelection = d3.select( + Plot.plot({ + marginLeft: 50, + y: { + label: null, + reverse: true + }, + marks: [ + Plot.barX( + sales, + Plot.groupY({x: "sum"}, {x: "units", y: "fruit", sort: {y: "x", reverse: true}, className: "fruitbars"}) + ), + Plot.ruleX([0]) + ] + }) + ); + + const div = d3.create("div"); + div.node().appendChild(plotSelection.node()); + + plotSelection.select(".fruitbars").attr("stroke", "gold"); + plotSelection + .select(".fruitbars") + .on("mouseover", function (d) { + d3.select(d.target).attr("fill", "red"); + }) + .on("mouseout", function (d) { + d3.select(d.target).attr("fill", "black"); + }); + plotSelection.select(".fruitbars").attr("fill", "black"); + + const numRects = plotSelection.selectAll(".fruitbars rect").size(); + div.append("p").text(`There are ${numRects} instances of 'fruitbars' class rects.`); + return div.node(); +} diff --git a/test/plots/index.js b/test/plots/index.js index a7b8169421..ece539a81a 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -36,6 +36,7 @@ export {default as carsJitter} from "./cars-jitter.js"; export {default as carsMpg} from "./cars-mpg.js"; export {default as carsParcoords} from "./cars-parcoords.js"; export {default as clamp} from "./clamp.js"; +export {default as classNameOnMarks} from "./className-on-marks.js"; export {default as collapsedHistogram} from "./collapsed-histogram.js"; export {default as covidIhmeProjectedDeaths} from "./covid-ihme-projected-deaths.js"; export {default as crimeanWarArrow} from "./crimean-war-arrow.js"; From 3d84a7f10084a44886b6446acd69587fc56c37ae Mon Sep 17 00:00:00 2001 From: Robert Lesser Date: Wed, 28 Dec 2022 16:22:37 -0500 Subject: [PATCH 3/5] Removed redundant function, added test output Removed the maybeClassNameOptional function, and fixed the existing maybeClassName function calls to take in a "provide" boolean. Added the test output file as well. --- src/legends/ramp.js | 2 +- src/legends/swatches.js | 2 +- src/plot.js | 6 +-- src/style.js | 8 +--- test/output/classNameOnMarks.html | 76 +++++++++++++++++++++++++++++++ 5 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 test/output/classNameOnMarks.html diff --git a/src/legends/ramp.js b/src/legends/ramp.js index a5494b1cb8..398cfd7605 100644 --- a/src/legends/ramp.js +++ b/src/legends/ramp.js @@ -23,7 +23,7 @@ export function legendRamp(color, options) { className } = options; const context = Context(options); - className = maybeClassName(className); + className = maybeClassName(className, true); if (tickFormat === null) tickFormat = () => null; const svg = create("svg", context) diff --git a/src/legends/swatches.js b/src/legends/swatches.js index 8b36fe3199..ed2e16cd0a 100644 --- a/src/legends/swatches.js +++ b/src/legends/swatches.js @@ -94,7 +94,7 @@ function legendItems(scale, options = {}, swatch, swatchStyle) { width } = options; const context = Context(options); - className = maybeClassName(className); + className = maybeClassName(className, true); tickFormat = maybeAutoTickFormat(tickFormat, scale.domain); const swatches = create("div", context) diff --git a/src/plot.js b/src/plot.js index db7e6c0d6e..2f29d33712 100644 --- a/src/plot.js +++ b/src/plot.js @@ -9,7 +9,7 @@ import {arrayify, isDomainSort, isScaleOptions, keyword, map, maybeNamed, range, import {maybeProject} from "./projection.js"; import {Scales, ScaleFunctions, autoScaleRange, exposeScales} from "./scales.js"; import {position, registry as scaleRegistry} from "./scales/index.js"; -import {applyInlineStyles, maybeClassName, maybeClassNameOptional, maybeClip, styles} from "./style.js"; +import {applyInlineStyles, maybeClassName, maybeClip, styles} from "./style.js"; import {basic, initializer} from "./transforms/basic.js"; import {maybeInterval} from "./transforms/interval.js"; import {consumeWarnings, warn} from "./warnings.js"; @@ -19,7 +19,7 @@ export function plot(options = {}) { const {facet, style, caption, ariaLabel, ariaDescription} = options; // className for inline styles - const className = maybeClassName(options.className); + const className = maybeClassName(options.className, true); // Flatten any nested marks. const marks = options.marks === undefined ? [] : options.marks.flat(Infinity).map(markify); @@ -389,7 +389,7 @@ export class Mark { this.dx = +dx || 0; this.dy = +dy || 0; this.clip = maybeClip(clip); - this.className = maybeClassNameOptional(options.className); + this.className = maybeClassName(options.className, false); } initialize(facets, facetChannels) { let data = arrayify(this.data); diff --git a/src/style.js b/src/style.js index e9796bf077..9aa60b7337 100644 --- a/src/style.js +++ b/src/style.js @@ -400,17 +400,13 @@ export function impliedNumber(value, impliedValue) { const validClassName = /^-?([_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 maybeClassName(name) { - if (name === undefined) return `plot-${Math.random().toString(16).slice(2)}`; +export function maybeClassName(name, provide) { + if (name === undefined) return provide ? `plot-${Math.random().toString(16).slice(2)}` : undefined; name = `${name}`; if (!validClassName.test(name)) throw new Error(`invalid class name: ${name}`); return name; } -export function maybeClassNameOptional(name) { - return name === undefined ? undefined : maybeClassName(name); -} - export function applyInlineStyles(selection, style) { if (typeof style === "string") { selection.property("style", style); diff --git a/test/output/classNameOnMarks.html b/test/output/classNameOnMarks.html new file mode 100644 index 0000000000..9b01b9640b --- /dev/null +++ b/test/output/classNameOnMarks.html @@ -0,0 +1,76 @@ +
+ + + + bananas + + + oranges + + + grapes + + + apples + + + + + 0 + + + 2 + + + 4 + + + 6 + + + 8 + + + 10 + + + 12 + + + 14 + + + 16 + + + 18 + + + 20 + units → + + + + + + + + + + + +

There are 4 instances of 'fruitbars' class rects.

+
\ No newline at end of file From dc50f3cefe6321d5a599f7378567fd5c51c1ecb2 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 3 Jul 2024 19:29:30 -0400 Subject: [PATCH 4/5] push ifs up --- src/legends/ramp.js | 2 +- src/legends/swatches.js | 2 +- src/mark.js | 3 ++- src/plot.js | 2 +- src/style.js | 4 ++-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/legends/ramp.js b/src/legends/ramp.js index dd7706484c..3dcaf411c6 100644 --- a/src/legends/ramp.js +++ b/src/legends/ramp.js @@ -24,7 +24,7 @@ export function legendRamp(color, options) { className } = options; const context = createContext(options); - className = maybeClassName(className, true); + className = maybeClassName(className); opacity = maybeNumberChannel(opacity)[1]; if (tickFormat === null) tickFormat = () => null; diff --git a/src/legends/swatches.js b/src/legends/swatches.js index 561a20b051..b628a0a5af 100644 --- a/src/legends/swatches.js +++ b/src/legends/swatches.js @@ -85,7 +85,7 @@ function legendItems(scale, options = {}, swatch) { width } = options; const context = createContext(options); - className = maybeClassName(className, true); + className = maybeClassName(className); tickFormat = inferTickFormat(scale.scale, scale.domain, undefined, tickFormat); const swatches = create("div", context).attr( diff --git a/src/mark.js b/src/mark.js index d39b97eeac..c8c3bca719 100644 --- a/src/mark.js +++ b/src/mark.js @@ -22,6 +22,7 @@ export class Mark { marginRight = margin, marginBottom = margin, marginLeft = margin, + className, clip = defaults?.clip, channels: extraChannels, tip, @@ -71,7 +72,7 @@ export class Mark { this.marginLeft = +marginLeft; this.clip = maybeClip(clip); this.tip = maybeTip(tip); - this.className = maybeClassName(options.className, false); + this.className = className ? maybeClassName(className) : null; // Super-faceting currently disallow position channels; in the future, we // could allow position to be specified in fx and fy in addition to (or // instead of) x and y. diff --git a/src/plot.js b/src/plot.js index c0bd9cd3b6..a091d8d8a8 100644 --- a/src/plot.js +++ b/src/plot.js @@ -23,7 +23,7 @@ export function plot(options = {}) { const {facet, style, title, subtitle, caption, ariaLabel, ariaDescription} = options; // className for inline styles - const className = maybeClassName(options.className, true); + const className = maybeClassName(options.className); // Flatten any nested marks. const marks = options.marks === undefined ? [] : flatMarks(options.marks); diff --git a/src/style.js b/src/style.js index dbb3ba7790..53601f3f62 100644 --- a/src/style.js +++ b/src/style.js @@ -412,10 +412,10 @@ export function impliedNumber(value, impliedValue) { const validClassName = /^-?([_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])*$/i; -export function maybeClassName(name, provide) { +export function maybeClassName(name) { // The default should be changed whenever the default styles are changed, so // as to avoid conflict when multiple versions of Plot are on the page. - if (name === undefined) return provide ? "plot-d6a7b5" : undefined; + if (name === undefined) return "plot-d6a7b5"; name = `${name}`; if (!validClassName.test(name)) throw new Error(`invalid class name: ${name}`); return name; From 4f438601d824fe747ab55f38bf1623d4150bfb0a Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 3 Jul 2024 19:33:46 -0400 Subject: [PATCH 5/5] documentation --- docs/features/marks.md | 1 + src/mark.d.ts | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/docs/features/marks.md b/docs/features/marks.md index 99f805f0a6..da319afdb0 100644 --- a/docs/features/marks.md +++ b/docs/features/marks.md @@ -482,6 +482,7 @@ All marks support the following style options: * **dx** - horizontal offset (in pixels; defaults to 0) * **dy** - vertical offset (in pixels; defaults to 0) * **target** - link target (e.g., “_blank” for a new window); for use with the **href** channel +* **className** - the [class attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class), if any (defaults to null) * **ariaDescription** - a textual description of the mark’s contents * **ariaHidden** - if true, hide this content from the accessibility tree * **pointerEvents** - the [pointer events](https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events) (*e.g.*, *none*) diff --git a/src/mark.d.ts b/src/mark.d.ts index 54f14276ca..d244fdba76 100644 --- a/src/mark.d.ts +++ b/src/mark.d.ts @@ -239,6 +239,13 @@ export interface MarkOptions { */ marginLeft?: number; + /** + * The [class attribute][1]; a constant string. + * + * [1]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/class + */ + className?: string; + /** * The [aria-description][1]; a constant textual description. *