From 0f07e6ba44259df4b1f5dc369999ad6a8309a5f7 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 28 Jun 2023 15:39:54 -0500 Subject: [PATCH 1/3] paint --- src/index.d.ts | 1 + src/index.js | 1 + src/mark.d.ts | 5 ++- src/options.js | 7 ++++ src/paint.d.ts | 10 +++++ src/paint.js | 13 ++++++ src/style.js | 24 +++++------ test/output/penguinSpeciesPaint.svg | 65 +++++++++++++++++++++++++++++ test/plots/penguin-species.ts | 10 +++++ 9 files changed, 121 insertions(+), 15 deletions(-) create mode 100644 src/paint.d.ts create mode 100644 src/paint.js create mode 100644 test/output/penguinSpeciesPaint.svg diff --git a/src/index.d.ts b/src/index.d.ts index 19e9442a31..5fda3d4aa6 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -37,6 +37,7 @@ export * from "./marks/tip.js"; export * from "./marks/tree.js"; export * from "./marks/vector.js"; export * from "./options.js"; +export * from "./paint.js"; export * from "./plot.js"; export * from "./projection.js"; export * from "./reducer.js"; diff --git a/src/index.js b/src/index.js index fec7e243ec..b6bb57cc63 100644 --- a/src/index.js +++ b/src/index.js @@ -45,3 +45,4 @@ export {pointer, pointerX, pointerY} from "./interactions/pointer.js"; export {formatIsoDate, formatWeekday, formatMonth} from "./format.js"; export {scale} from "./scales.js"; export {legend} from "./legends.js"; +export {linearGradient} from "./paint.js"; diff --git a/src/mark.d.ts b/src/mark.d.ts index 21921fdd79..cbb5fa781b 100644 --- a/src/mark.d.ts +++ b/src/mark.d.ts @@ -1,6 +1,7 @@ import type {Channel, ChannelDomainSort, ChannelValue, ChannelValues, ChannelValueSpec} from "./channel.js"; import type {Context} from "./context.js"; import type {Dimensions} from "./dimensions.js"; +import type {Paint} from "./paint.js"; import type {plot} from "./plot.js"; import type {ScaleFunctions} from "./scales.js"; import type {InitializerFunction, SortOrder, TransformFunction} from "./transforms/basic.js"; @@ -309,7 +310,7 @@ export interface MarkOptions { * * [1]: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill */ - fill?: ChannelValueSpec; + fill?: ChannelValueSpec | Paint; /** * The [fill-opacity][1]; a constant number between 0 and 1, or a channel @@ -329,7 +330,7 @@ export interface MarkOptions { * * [1]: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke */ - stroke?: ChannelValueSpec; + stroke?: ChannelValueSpec | Paint; /** * The [stroke-dasharray][1]; a constant number indicating the length in diff --git a/src/options.js b/src/options.js index f70a118ef8..67c27b1ce1 100644 --- a/src/options.js +++ b/src/options.js @@ -442,12 +442,19 @@ export function isEvery(values, is) { return every; } +// TODO Make isPaint the generic one (either a color or a custom paint +// implementation), rather than making isColor the generic one? +export function isPaint(value) { + return typeof value?.paint === "function"; +} + // Mostly relies on d3-color, with a few extra color keywords. Currently this // strictly requires that the value be a string; we might want to apply string // coercion here, though note that d3-color instances would need to support // valueOf to work correctly with InternMap. // https://www.w3.org/TR/SVG11/painting.html#SpecifyingPaint export function isColor(value) { + if (isPaint(value)) return true; if (typeof value !== "string") return false; value = value.toLowerCase().trim(); return ( diff --git a/src/paint.d.ts b/src/paint.d.ts new file mode 100644 index 0000000000..e612fdbc5a --- /dev/null +++ b/src/paint.d.ts @@ -0,0 +1,10 @@ +import type {Context} from "./context.js"; + +/** TODO */ +export interface Paint { + /** TODO */ + paint(context: Context): void; +} + +/** TODO */ +export function linearGradient(): Paint; diff --git a/src/paint.js b/src/paint.js new file mode 100644 index 0000000000..b23faf9abe --- /dev/null +++ b/src/paint.js @@ -0,0 +1,13 @@ +import {create} from "./context.js"; + +export function linearGradient() { + return { + paint(context) { + const gradient = create("svg:linearGradient", context).attr("gradientTransform", "rotate(90)"); + gradient.append("stop").attr("offset", "5%").attr("stop-color", "purple"); + gradient.append("stop").attr("offset", "75%").attr("stop-color", "red"); + gradient.append("stop").attr("offset", "100%").attr("stop-color", "gold"); + return gradient.node(); + } + }; +} diff --git a/src/style.js b/src/style.js index b3e046cc91..cda28d2742 100644 --- a/src/style.js +++ b/src/style.js @@ -2,17 +2,8 @@ import {geoPath, group, namespaces} from "d3"; import {create} from "./context.js"; import {defined, nonempty} from "./defined.js"; import {formatDefault} from "./format.js"; -import { - string, - number, - maybeColorChannel, - maybeNumberChannel, - maybeKeyword, - isNoneish, - isNone, - isRound, - keyof -} from "./options.js"; +import {isNone, isNoneish, isPaint, isRound} from "./options.js"; +import {keyof, maybeColorChannel, maybeKeyword, maybeNumberChannel, number, string} from "./options.js"; import {warn} from "./warnings.js"; export const offset = (typeof window !== "undefined" ? window.devicePixelRatio > 1 : typeof it === "undefined") ? 0 : 0.5; // prettier-ignore @@ -115,7 +106,7 @@ export function styles( // Some marks don’t support fill (e.g., tick and rule). if (defaultFill !== null) { - mark.fill = impliedString(cfill, "currentColor"); + mark.fill = isPaint(cfill) ? cfill : impliedString(cfill, "currentColor"); mark.fillOpacity = impliedNumber(cfillOpacity, 1); } @@ -364,7 +355,14 @@ function applyClip(selection, mark, dimensions, context) { // Note: may mutate selection.node! export function applyIndirectStyles(selection, mark, dimensions, context) { applyClip(selection, mark, dimensions, context); - applyAttr(selection, "fill", mark.fill); + if (isPaint(mark.fill)) { + const paint = mark.fill.paint(context); + paint.setAttribute("id", "test-paint"); + context.ownerSVGElement.append(paint); + selection.attr("fill", "url(#test-paint)"); + } else { + applyAttr(selection, "fill", mark.fill); + } applyAttr(selection, "fill-opacity", mark.fillOpacity); applyAttr(selection, "stroke", mark.stroke); applyAttr(selection, "stroke-width", mark.strokeWidth); diff --git a/test/output/penguinSpeciesPaint.svg b/test/output/penguinSpeciesPaint.svg new file mode 100644 index 0000000000..076c9944e6 --- /dev/null +++ b/test/output/penguinSpeciesPaint.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + 0 + 20 + 40 + 60 + 80 + 100 + 120 + 140 + + + ↑ Frequency + + + + + + + + Adelie + Chinstrap + Gentoo + + + species + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/penguin-species.ts b/test/plots/penguin-species.ts index c7eacdde31..993a5ece59 100644 --- a/test/plots/penguin-species.ts +++ b/test/plots/penguin-species.ts @@ -29,6 +29,16 @@ export async function penguinSpeciesCheysson() { }); } +export async function penguinSpeciesPaint() { + const penguins = await d3.csv("data/penguins.csv", d3.autoType); + return Plot.plot({ + marks: [ + Plot.barY(penguins, Plot.groupX({y: "count"}, {x: "species", fill: Plot.linearGradient()})), + Plot.ruleY([0]) + ] + }); +} + export async function penguinSpeciesGradient() { const penguins = await d3.csv("data/penguins.csv", d3.autoType); return Plot.plot({ From 5251f24d2223808c650c2462eab6d91981b32c3a Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 13 Jul 2024 12:25:24 -0400 Subject: [PATCH 2/3] placeholder documentation --- docs/.vitepress/config.ts | 1 + docs/data/api.data.ts | 1 + docs/features/paints.md | 3 +++ 3 files changed, 5 insertions(+) create mode 100644 docs/features/paints.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 351b8e372d..e72a077a46 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -70,6 +70,7 @@ export default defineConfig({ {text: "Formats", link: "/features/formats"}, {text: "Intervals", link: "/features/intervals"}, {text: "Markers", link: "/features/markers"}, + {text: "Paints", link: "/features/paints"}, {text: "Shorthand", link: "/features/shorthand"}, {text: "Accessibility", link: "/features/accessibility"} ] diff --git a/docs/data/api.data.ts b/docs/data/api.data.ts index bbabdd8cce..7de8fbdde8 100644 --- a/docs/data/api.data.ts +++ b/docs/data/api.data.ts @@ -46,6 +46,7 @@ function getHref(name: string, path: string): string { case "features/interval": case "features/mark": case "features/marker": + case "features/paint": case "features/plot": case "features/projection": return `${path}s`; diff --git a/docs/features/paints.md b/docs/features/paints.md new file mode 100644 index 0000000000..654a24ec73 --- /dev/null +++ b/docs/features/paints.md @@ -0,0 +1,3 @@ +# Paints + +A **paint** defines a fill or stroke implementation such as a pattern or gradient; it is an alternative to a constant color. From 899d25d183b6affac89cedc071ed153e934a6358 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Sat, 13 Jul 2024 12:29:26 -0400 Subject: [PATCH 3/3] more placeholder documentation --- docs/features/paints.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/features/paints.md b/docs/features/paints.md index 654a24ec73..312837afd8 100644 --- a/docs/features/paints.md +++ b/docs/features/paints.md @@ -1,3 +1,7 @@ # Paints A **paint** defines a fill or stroke implementation such as a pattern or gradient; it is an alternative to a constant color. + +## linearGradient() {#linearGradient} + +Returns a linear gradient.