diff --git a/src/marks/difference.d.ts b/src/marks/difference.d.ts index f292be2a30..ecd32d268a 100644 --- a/src/marks/difference.d.ts +++ b/src/marks/difference.d.ts @@ -1,4 +1,4 @@ -import type {ChannelValueSpec} from "../channel.js"; +import type {ChannelValue, ChannelValueSpec} from "../channel.js"; import type {CurveOptions} from "../curve.js"; import type {Data, MarkOptions, RenderableMark} from "../mark.js"; @@ -61,6 +61,14 @@ export interface DifferenceOptions extends MarkOptions, CurveOptions { * defaults to **fillOpacity**. */ negativeFillOpacity?: number; + + /** + * An optional ordinal channel for grouping data into series to be drawn as + * separate areas; defaults to **fillPositive** if a channel for the positive + * area, **fillNegative** if a channel for the negative area, or **stroke** if + * a channel. + */ + z?: ChannelValue; } /** TODO */ diff --git a/src/marks/difference.js b/src/marks/difference.js index 3c661ad17f..08e0cbe51d 100644 --- a/src/marks/difference.js +++ b/src/marks/difference.js @@ -24,7 +24,6 @@ export function differenceY( stroke, strokeOpacity, z = maybeColorChannel(stroke)[0], - clip = true, tip, render, ...options @@ -45,7 +44,7 @@ export function differenceY( z, fill: positiveFill, fillOpacity: positiveFillOpacity, - render: composeRender(render, clipDifference(true)), + render: composeRender(render, clipDifferenceY(true)), ...options }), {ariaLabel: "positive difference"} @@ -61,7 +60,7 @@ export function differenceY( z, fill: negativeFill, fillOpacity: negativeFillOpacity, - render: composeRender(render, clipDifference(false)), + render: composeRender(render, clipDifferenceY(false)), ...options }), {ariaLabel: "negative difference"} @@ -74,7 +73,7 @@ export function differenceY( stroke, strokeOpacity, tip, - clip, + clip: true, ...options }) ); @@ -108,20 +107,23 @@ function memo(v) { return {transform: (data) => V || (V = valueof(data, value)), label}; } -function clipDifference(positive) { +function clipDifferenceY(positive) { return (index, scales, channels, dimensions, context, next) => { - const clip = getClipId(); - const clipPath = create("svg:clipPath", context).attr("id", clip).node(); const {x1, x2} = channels; const {height} = dimensions; const y1 = new Float32Array(x1.length); const y2 = new Float32Array(x2.length); (positive === inferScaleOrder(scales.y) < 0 ? y1 : y2).fill(height); - const c = next(index, scales, {...channels, x2: x1, y2}, dimensions, context); - clipPath.append(...c.childNodes); + const c = next(index, scales, {...channels, x2: x1, y2}, dimensions, context).querySelectorAll("path"); const g = next(index, scales, {...channels, x1: x2, y1}, dimensions, context); - g.insertBefore(clipPath, g.firstChild); - g.setAttribute("clip-path", `url(#${clip})`); + let i = -1; + for (const node of g.querySelectorAll("path")) { + const clip = getClipId(); + const clipPath = create("svg:clipPath", context).attr("id", clip).node(); + clipPath.append(c[++i]); + node.parentElement.insertBefore(clipPath, node); + node.setAttribute("clip-path", `url(#${clip})`); + } return g; }; } diff --git a/test/output/differenceFilterX.svg b/test/output/differenceFilterX.svg index 546c02045a..ca08fc5bc4 100644 --- a/test/output/differenceFilterX.svg +++ b/test/output/differenceFilterX.svg @@ -51,17 +51,17 @@ 2017 2018 - + - + - + - + diff --git a/test/output/differenceFilterY1.svg b/test/output/differenceFilterY1.svg index ba1686167c..e23c4dc6fa 100644 --- a/test/output/differenceFilterY1.svg +++ b/test/output/differenceFilterY1.svg @@ -51,17 +51,17 @@ 2017 2018 - + - + - + - + diff --git a/test/output/differenceFilterY2.svg b/test/output/differenceFilterY2.svg index 9459a48d9d..36336cfc14 100644 --- a/test/output/differenceFilterY2.svg +++ b/test/output/differenceFilterY2.svg @@ -51,17 +51,17 @@ 2017 2018 - + - + - + - + diff --git a/test/output/differenceY.svg b/test/output/differenceY.svg index 88c61aa691..5ad8b7f9d6 100644 --- a/test/output/differenceY.svg +++ b/test/output/differenceY.svg @@ -54,17 +54,17 @@ 2017 2018 - + - + - + - + diff --git a/test/output/differenceY1.svg b/test/output/differenceY1.svg index 58f0cec02f..2a1dfa2c1e 100644 --- a/test/output/differenceY1.svg +++ b/test/output/differenceY1.svg @@ -60,17 +60,17 @@ 2017 2018 - + - + - + - + diff --git a/test/output/differenceYClip.svg b/test/output/differenceYClip.svg new file mode 100644 index 0000000000..5a7e9966dc --- /dev/null +++ b/test/output/differenceYClip.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + −0.4 + −0.2 + 0.0 + 0.2 + 0.4 + 0.6 + 0.8 + 1.0 + + + ↑ Anomaly + + + + + + + + + + + + 1880 + 1900 + 1920 + 1940 + 1960 + 1980 + 2000 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/differenceYClipVariable.svg b/test/output/differenceYClipVariable.svg new file mode 100644 index 0000000000..6d8fa2ecd2 --- /dev/null +++ b/test/output/differenceYClipVariable.svg @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + 1.0 + 1.2 + 1.4 + 1.6 + 1.8 + 2.0 + 2.2 + 2.4 + 2.6 + 2.8 + + + ↑ Close + + + + + + + + + + 2014 + 2015 + 2016 + 2017 + 2018 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/differenceYConstant.svg b/test/output/differenceYConstant.svg index b433a3211c..8bad268552 100644 --- a/test/output/differenceYConstant.svg +++ b/test/output/differenceYConstant.svg @@ -62,17 +62,17 @@ 2017 2018 - + - + - + - + diff --git a/test/output/differenceYCurve.svg b/test/output/differenceYCurve.svg index 6832e54495..91ba74ba6d 100644 --- a/test/output/differenceYCurve.svg +++ b/test/output/differenceYCurve.svg @@ -68,17 +68,17 @@ 22 29 - + - + - + - + diff --git a/test/output/differenceYNegative.svg b/test/output/differenceYNegative.svg index 66b4b5b126..92c3bf2f07 100644 --- a/test/output/differenceYNegative.svg +++ b/test/output/differenceYNegative.svg @@ -54,11 +54,11 @@ 1980 2000 - + - + diff --git a/test/output/differenceYOrdinal.svg b/test/output/differenceYOrdinal.svg new file mode 100644 index 0000000000..4f51ef6aa6 --- /dev/null +++ b/test/output/differenceYOrdinal.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + A + B + C + D + E + + + + + + + + + + + 0 + 5 + 10 + 15 + 20 + 25 + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/differenceYOrdinalFlip.svg b/test/output/differenceYOrdinalFlip.svg new file mode 100644 index 0000000000..9cf9ab175e --- /dev/null +++ b/test/output/differenceYOrdinalFlip.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + E + D + C + B + A + + + + + + + + + + + 0 + 5 + 10 + 15 + 20 + 25 + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/output/differenceYRandom.svg b/test/output/differenceYRandom.svg index a6a78d1c74..dc482d56bb 100644 --- a/test/output/differenceYRandom.svg +++ b/test/output/differenceYRandom.svg @@ -47,17 +47,17 @@ 40 50 - + - + - + - + diff --git a/test/output/differenceYReverse.svg b/test/output/differenceYReverse.svg index 50871a6d1e..cf8490eaa5 100644 --- a/test/output/differenceYReverse.svg +++ b/test/output/differenceYReverse.svg @@ -54,17 +54,17 @@ 1980 2000 - + - + - + - + diff --git a/test/output/differenceYVariable.svg b/test/output/differenceYVariable.svg index 91fc4063f9..a9c7002452 100644 --- a/test/output/differenceYVariable.svg +++ b/test/output/differenceYVariable.svg @@ -54,30 +54,40 @@ 2017 2018 - + + + + + + + + + + + + + + + + - - - - - - + - - + + - + - - + + diff --git a/test/output/differenceYZero.svg b/test/output/differenceYZero.svg index c51b060f8c..b479797be5 100644 --- a/test/output/differenceYZero.svg +++ b/test/output/differenceYZero.svg @@ -54,17 +54,17 @@ 1980 2000 - + - + - + - + diff --git a/test/output/youngAdults.html b/test/output/youngAdults.html new file mode 100644 index 0000000000..336d07e429 --- /dev/null +++ b/test/output/youngAdults.html @@ -0,0 +1,449 @@ +
+

Share of young adults living with their parents (%)

+

…by age and sex. Data: Eurostat

+
+ + + Y16-19 + + Y20-24 + + Y25-29 +
+ + + + SE + + + FR + + + DE + + + TR + + + IT + + + + geo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 10 + 20 + 30 + 40 + 50 + 60 + 70 + 80 + 90 + 100 + + + + ↑ OBS_VALUE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2005 + 2010 + 2015 + 2020 + + + 2005 + 2010 + 2015 + 2020 + + + 2005 + 2010 + 2015 + 2020 + + + 2005 + 2010 + 2015 + 2020 + + + 2005 + 2010 + 2015 + 2020 + + + + TIME_PERIOD → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/test/plots/difference.ts b/test/plots/difference.ts index c0c659c288..5ee4ea3690 100644 --- a/test/plots/difference.ts +++ b/test/plots/difference.ts @@ -73,11 +73,66 @@ export async function differenceYVariable() { }); } +export async function differenceYClip() { + const gistemp = await d3.csv("data/gistemp.csv", d3.autoType); + return Plot.differenceY(gistemp, Plot.windowY(28, {x: "Date", y: "Anomaly", clip: "frame"})).plot({ + x: {insetLeft: -50} + }); +} + +export async function differenceYClipVariable() { + const stocks = await readStocks(); + return Plot.plot({ + marks: [ + Plot.differenceY( + stocks, + Plot.normalizeY( + Plot.groupX( + {y1: Plot.find((d) => d.Symbol === "GOOG"), y2: Plot.find((d) => d.Symbol === "AAPL")}, + {x: "Date", y: "Close", negativeFill: "#eee", positiveFill: ([d]) => d.Date.getUTCFullYear(), clip: true} + ) + ) + ) + ] + }); +} + export async function differenceYConstant() { const aapl = await d3.csv("data/aapl.csv", d3.autoType); return Plot.differenceY(aapl, {x: "Date", y1: 115, y2: "Close"}).plot(); } +export async function differenceYOrdinal() { + const random = d3.randomLcg(42); + return Plot.plot({ + marks: [ + Plot.differenceY( + {length: 30}, + { + y1: () => "ABCDE"[(random() * 5) | 0], + y2: () => "ABCDE"[(random() * 5) | 0] + } + ) + ] + }); +} + +export async function differenceYOrdinalFlip() { + const random = d3.randomLcg(42); + return Plot.plot({ + y: {reverse: true}, + marks: [ + Plot.differenceY( + {length: 30}, + { + y1: () => "ABCDE"[(random() * 5) | 0], + y2: () => "ABCDE"[(random() * 5) | 0] + } + ) + ] + }); +} + export async function differenceYReverse() { const gistemp = await d3.csv("data/gistemp.csv", d3.autoType); return Plot.differenceY(gistemp, Plot.windowY(28, {x: "Date", y: "Anomaly"})).plot({y: {reverse: true}}); diff --git a/test/plots/index.ts b/test/plots/index.ts index 2b202b065f..3eb5b5de2a 100644 --- a/test/plots/index.ts +++ b/test/plots/index.ts @@ -338,3 +338,4 @@ export * from "./word-length-moby-dick.js"; export * from "./yearly-requests-dot.js"; export * from "./yearly-requests-line.js"; export * from "./yearly-requests.js"; +export * from "./young-adults.js"; diff --git a/test/plots/young-adults.ts b/test/plots/young-adults.ts new file mode 100644 index 0000000000..9d6f50dd18 --- /dev/null +++ b/test/plots/young-adults.ts @@ -0,0 +1,44 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export async function youngAdults() { + const ages = ["Y16-19", "Y20-24", "Y25-29"]; + const geos = ["SE", "FR", "DE", "TR", "IT"]; + const ilc_lvps08 = await d3.csv("data/ilc_lvps08.csv", (d) => + ages.includes(d.age) && geos.includes(d.geo) ? d3.autoType(d) : null + ); + return Plot.plot({ + title: "Share of young adults living with their parents (%)", + subtitle: "…by age and sex. Data: Eurostat", + width: 928, + color: {legend: true}, + style: `max-width:${4000}px; overflow-y: visible;`, + x: {ticks: 4, tickFormat: "d"}, + y: {grid: true, nice: true}, + marks: [ + Plot.frame(), + Plot.differenceY( + ilc_lvps08, + Plot.groupX( + { + y1: Plot.find((d) => d.sex === "F"), + y2: Plot.find((d) => d.sex === "M"), + positiveFill: "first" + }, + { + x: "TIME_PERIOD", + y: "OBS_VALUE", + negativeFill: "grey", + positiveFill: "age", + z: "age", + sort: {fx: {value: "y", reduce: "mean"}}, + fillOpacity: 0.5, + fx: "geo", + curve: "basis", + tip: true + } + ) + ) + ] + }); +}