diff --git a/README.md b/README.md index 999a9ab832..fddeb855d8 100644 --- a/README.md +++ b/README.md @@ -3273,6 +3273,15 @@ For each pixel in the raster grid, initiates a random walk, stopping when either + +## Halo + +The line mark support a halo filter, allowing to better separate multiple lines by adding a thick white background underneath each line, [a technique described by Sara Soueidan](https://tympanus.net/codrops/2019/01/22/svg-filter-effects-outline-text-with-femorphology/). The halo options can be specified as: + +* *haloColor* - the halo’s color, defaults to white +* *haloRadius* - the halo’s radius, which defaults to 2px +* *halo* - if true, activates the halo filter; if specified as a color, defines the halo’s color; if specified as a number, defines the halo’s radius + ## Markers A [marker](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/marker) defines a graphic drawn on vertices of a [line](#line) or a [link](#link) mark. The supported marker options are: diff --git a/src/marks/halo.js b/src/marks/halo.js new file mode 100644 index 0000000000..e12ecfe934 --- /dev/null +++ b/src/marks/halo.js @@ -0,0 +1,33 @@ +import {isColor} from "../options.js"; + +let nextHaloId = 0; + +export function applyHalo(g, {color, radius}) { + const id = `plot-linehalo-${nextHaloId++}`; + g.selectChildren().style("filter", `url(#${id})`); + g.append("filter").attr("id", id).html(` + + + + + + + `); +} + +export function maybeHalo(halo, color, radius) { + if (halo === undefined) halo = color !== undefined || radius !== undefined; + if (!halo) return false; + const defaults = {color: "white", radius: 2}; + if (color === undefined) { + color = isColor(halo) ? halo : defaults.color; + } else if (!isColor(color)) { + throw new Error(`Unsupported halo color: ${color}`); + } + if (radius === undefined) { + radius = !isNaN(+halo) ? +halo : defaults.radius; + } else if (isNaN(+radius)) { + throw new Error(`Unsupported halo radius: ${radius}`); + } + return {color, radius}; +} diff --git a/src/marks/line.js b/src/marks/line.js index 77a7d080ad..61cc75e42f 100644 --- a/src/marks/line.js +++ b/src/marks/line.js @@ -1,4 +1,4 @@ -import {curveLinear, geoPath, line as shapeLine} from "d3"; +import {curveLinear, geoPath, group, line as shapeLine} from "d3"; import {create} from "../context.js"; import {Curve} from "../curve.js"; import {Mark} from "../mark.js"; @@ -13,6 +13,7 @@ import { } from "../style.js"; import {maybeDenseIntervalX, maybeDenseIntervalY} from "../transforms/bin.js"; import {applyGroupedMarkers, markers} from "./marker.js"; +import {applyHalo, maybeHalo} from "./halo.js"; const defaults = { ariaLabel: "line", @@ -39,7 +40,7 @@ function LineCurve({curve = curveAuto, tension}) { export class Line extends Mark { constructor(data, options = {}) { - const {x, y, z} = options; + const {x, y, z, halo, haloColor, haloRadius} = options; super( data, { @@ -52,6 +53,7 @@ export class Line extends Mark { ); this.z = z; this.curve = LineCurve(options); + this.halo = maybeHalo(halo, haloColor, haloRadius); markers(this, options); } filter(index) { @@ -66,8 +68,8 @@ export class Line extends Mark { render(index, scales, channels, dimensions, context) { const {x: X, y: Y} = channels; const {curve} = this; - return create("svg:g", context) - .call(applyIndirectStyles, this, dimensions, context) + const g = create("svg:g", context) + .call(applyIndirectStyles, this, scales, dimensions) .call(applyTransform, this, scales) .call((g) => g @@ -88,8 +90,25 @@ export class Line extends Mark { .x((i) => X[i]) .y((i) => Y[i]) ) - ) - .node(); + ); + + if (this.halo) { + // With variable aesthetics, we need to regroup segments by line + let line = -1; + let segmented = false; + const groups = group(g.selectAll("path"), (d) => + d.__data__.segment === undefined ? ++line : ((segmented = true), line) + ); + if (segmented) { + for (const [, paths] of groups) { + const l = g.append("g").node(); + for (const p of paths) l.appendChild(p); + } + } + applyHalo(g, this.halo); + } + + return g.node(); } } diff --git a/src/style.js b/src/style.js index 083c5754ab..a3bc167f65 100644 --- a/src/style.js +++ b/src/style.js @@ -263,6 +263,7 @@ export function* groupIndex(I, position, {z}, channels) { for (const G of Z ? groupZ(I, Z, z) : [I]) { let Ag; // the A-values (aesthetics) of the current group, if any let Gg; // the current group index (a subset of G, and I), if any + let segment = 0; // counter of sub-groups for the current z out: for (const i of G) { // If any channel has an undefined value for this index, skip it. for (const c of C) { @@ -275,8 +276,9 @@ export function* groupIndex(I, position, {z}, channels) { // Otherwise, if this is a new group, record the aesthetics for this // group. Yield the current group and start a new one. if (Ag === undefined) { - if (Gg) yield Gg; - (Ag = A.map((c) => keyof(c[i]))), (Gg = [i]); + if (Gg) segment++ ? Object.assign(Gg, {segment}) : Gg; + Ag = A.map((c) => keyof(c[i])); + Gg = [i]; continue; } @@ -287,15 +289,16 @@ export function* groupIndex(I, position, {z}, channels) { for (let j = 0; j < A.length; ++j) { const k = keyof(A[j][i]); if (k !== Ag[j]) { - yield Gg; - (Ag = A.map((c) => keyof(c[i]))), (Gg = [i]); + yield segment++ ? Object.assign(Gg, {segment}) : Gg; + Ag = A.map((c) => keyof(c[i])); + Gg = [i]; continue out; } } } // Yield the current group, if any. - if (Gg) yield Gg; + if (Gg) yield segment++ ? Object.assign(Gg, {segment}) : Gg; } } diff --git a/test/data/README.md b/test/data/README.md index e4e77a68a3..ae0fade967 100644 --- a/test/data/README.md +++ b/test/data/README.md @@ -154,6 +154,11 @@ https://github.com/topojson/us-atlas U.S. Bureau of Labor Statistics, 2016 https://www.bls.gov/lau/tables.htm +## us-gdp.csv +U.S. Gross Domestic Product, chained dollars, Jan 1947-Jan 2022 +Federal Reserve Economic Data +https://fred.stlouisfed.org/ + ## us-state-capitals.csv Unknown origin diff --git a/test/data/us-gdp.csv b/test/data/us-gdp.csv new file mode 100644 index 0000000000..d46c25da6a --- /dev/null +++ b/test/data/us-gdp.csv @@ -0,0 +1,302 @@ +date,gdpc1 +1947-01-01,2034.45 +1947-04-01,2029.024 +1947-07-01,2024.834 +1947-10-01,2056.508 +1948-01-01,2087.442 +1948-04-01,2121.899 +1948-07-01,2134.056 +1948-10-01,2136.44 +1949-01-01,2107.001 +1949-04-01,2099.814 +1949-07-01,2121.493 +1949-10-01,2103.688 +1950-01-01,2186.365 +1950-04-01,2253.045 +1950-07-01,2340.112 +1950-10-01,2384.92 +1951-01-01,2417.311 +1951-04-01,2459.196 +1951-07-01,2509.88 +1951-10-01,2515.408 +1952-01-01,2542.286 +1952-04-01,2547.762 +1952-07-01,2566.153 +1952-10-01,2650.431 +1953-01-01,2699.699 +1953-04-01,2720.566 +1953-07-01,2705.258 +1953-10-01,2664.302 +1954-01-01,2651.566 +1954-04-01,2654.456 +1954-07-01,2684.434 +1954-10-01,2736.96 +1955-01-01,2815.134 +1955-04-01,2860.942 +1955-07-01,2899.578 +1955-10-01,2916.985 +1956-01-01,2905.656 +1956-04-01,2929.666 +1956-07-01,2927.034 +1956-10-01,2975.209 +1957-01-01,2994.259 +1957-04-01,2987.699 +1957-07-01,3016.979 +1957-10-01,2985.775 +1958-01-01,2908.281 +1958-04-01,2927.395 +1958-07-01,2995.112 +1958-10-01,3065.141 +1959-01-01,3123.978 +1959-04-01,3194.429 +1959-07-01,3196.683 +1959-10-01,3205.79 +1960-01-01,3277.847 +1960-04-01,3260.177 +1960-07-01,3276.133 +1960-10-01,3234.087 +1961-01-01,3255.914 +1961-04-01,3311.181 +1961-07-01,3374.742 +1961-10-01,3440.924 +1962-01-01,3502.298 +1962-04-01,3533.947 +1962-07-01,3577.362 +1962-10-01,3589.128 +1963-01-01,3628.306 +1963-04-01,3669.02 +1963-07-01,3749.681 +1963-10-01,3774.264 +1964-01-01,3853.835 +1964-04-01,3895.793 +1964-07-01,3956.657 +1964-10-01,3968.878 +1965-01-01,4064.915 +1965-04-01,4116.267 +1965-07-01,4207.782 +1965-10-01,4304.731 +1966-01-01,4409.518 +1966-04-01,4424.581 +1966-07-01,4462.053 +1966-10-01,4498.66 +1967-01-01,4538.498 +1967-04-01,4541.28 +1967-07-01,4584.246 +1967-10-01,4618.812 +1968-01-01,4713.013 +1968-04-01,4791.758 +1968-07-01,4828.892 +1968-10-01,4847.885 +1969-01-01,4923.76 +1969-04-01,4938.728 +1969-07-01,4971.349 +1969-10-01,4947.104 +1970-01-01,4939.759 +1970-04-01,4946.77 +1970-07-01,4992.357 +1970-10-01,4938.857 +1971-01-01,5072.996 +1971-04-01,5100.447 +1971-07-01,5142.422 +1971-10-01,5154.547 +1972-01-01,5249.337 +1972-04-01,5368.485 +1972-07-01,5419.184 +1972-10-01,5509.926 +1973-01-01,5646.286 +1973-04-01,5707.755 +1973-07-01,5677.738 +1973-10-01,5731.632 +1974-01-01,5682.353 +1974-04-01,5695.859 +1974-07-01,5642.025 +1974-10-01,5620.126 +1975-01-01,5551.713 +1975-04-01,5591.382 +1975-07-01,5687.087 +1975-10-01,5763.665 +1976-01-01,5893.276 +1976-04-01,5936.515 +1976-07-01,5969.089 +1976-10-01,6012.356 +1977-01-01,6083.391 +1977-04-01,6201.659 +1977-07-01,6313.559 +1977-10-01,6313.697 +1978-01-01,6333.848 +1978-04-01,6578.605 +1978-07-01,6644.754 +1978-10-01,6734.069 +1979-01-01,6746.176 +1979-04-01,6753.389 +1979-07-01,6803.558 +1979-10-01,6820.572 +1980-01-01,6842.024 +1980-04-01,6701.046 +1980-07-01,6693.082 +1980-10-01,6817.903 +1981-01-01,6951.495 +1981-04-01,6899.98 +1981-07-01,6982.609 +1981-10-01,6906.529 +1982-01-01,6799.233 +1982-04-01,6830.251 +1982-07-01,6804.139 +1982-10-01,6806.857 +1983-01-01,6896.561 +1983-04-01,7053.5 +1983-07-01,7194.504 +1983-10-01,7344.597 +1984-01-01,7488.167 +1984-04-01,7617.547 +1984-07-01,7690.985 +1984-10-01,7754.117 +1985-01-01,7829.26 +1985-04-01,7898.194 +1985-07-01,8018.809 +1985-10-01,8078.415 +1986-01-01,8153.829 +1986-04-01,8190.552 +1986-07-01,8268.935 +1986-10-01,8313.338 +1987-01-01,8375.274 +1987-04-01,8465.63 +1987-07-01,8539.075 +1987-10-01,8685.694 +1988-01-01,8730.569 +1988-04-01,8845.28 +1988-07-01,8897.107 +1988-10-01,9015.661 +1989-01-01,9107.314 +1989-04-01,9176.827 +1989-07-01,9244.816 +1989-10-01,9263.033 +1990-01-01,9364.259 +1990-04-01,9398.243 +1990-07-01,9404.494 +1990-10-01,9318.876 +1991-01-01,9275.276 +1991-04-01,9347.597 +1991-07-01,9394.834 +1991-10-01,9427.581 +1992-01-01,9540.444 +1992-04-01,9643.893 +1992-07-01,9739.185 +1992-10-01,9840.753 +1993-01-01,9857.185 +1993-04-01,9914.565 +1993-07-01,9961.873 +1993-10-01,10097.362 +1994-01-01,10195.338 +1994-04-01,10333.495 +1994-07-01,10393.898 +1994-10-01,10512.962 +1995-01-01,10550.251 +1995-04-01,10581.723 +1995-07-01,10671.738 +1995-10-01,10744.203 +1996-01-01,10824.674 +1996-04-01,11005.217 +1996-07-01,11103.935 +1996-10-01,11219.238 +1997-01-01,11291.665 +1997-04-01,11479.33 +1997-07-01,11622.911 +1997-10-01,11722.722 +1998-01-01,11839.876 +1998-04-01,11949.492 +1998-07-01,12099.191 +1998-10-01,12294.737 +1999-01-01,12410.778 +1999-04-01,12514.408 +1999-07-01,12679.977 +1999-10-01,12888.281 +2000-01-01,12935.252 +2000-04-01,13170.749 +2000-07-01,13183.89 +2000-10-01,13262.25 +2001-01-01,13219.251 +2001-04-01,13301.394 +2001-07-01,13248.142 +2001-10-01,13284.881 +2002-01-01,13394.91 +2002-04-01,13477.356 +2002-07-01,13531.741 +2002-10-01,13549.421 +2003-01-01,13619.434 +2003-04-01,13741.107 +2003-07-01,13970.157 +2003-10-01,14131.379 +2004-01-01,14212.34 +2004-04-01,14323.017 +2004-07-01,14457.832 +2004-10-01,14605.595 +2005-01-01,14767.846 +2005-04-01,14839.707 +2005-07-01,14956.291 +2005-10-01,15041.232 +2006-01-01,15244.088 +2006-04-01,15281.525 +2006-07-01,15304.517 +2006-10-01,15433.643 +2007-01-01,15478.956 +2007-04-01,15577.779 +2007-07-01,15671.605 +2007-10-01,15767.146 +2008-01-01,15702.906 +2008-04-01,15792.773 +2008-07-01,15709.562 +2008-10-01,15366.607 +2009-01-01,15187.475 +2009-04-01,15161.772 +2009-07-01,15216.647 +2009-10-01,15379.155 +2010-01-01,15456.059 +2010-04-01,15605.628 +2010-07-01,15726.282 +2010-10-01,15807.995 +2011-01-01,15769.911 +2011-04-01,15876.839 +2011-07-01,15870.684 +2011-10-01,16048.702 +2012-01-01,16179.968 +2012-04-01,16253.726 +2012-07-01,16282.151 +2012-10-01,16300.035 +2013-01-01,16441.485 +2013-04-01,16464.402 +2013-07-01,16594.743 +2013-10-01,16712.76 +2014-01-01,16654.247 +2014-04-01,16868.109 +2014-07-01,17064.616 +2014-10-01,17141.235 +2015-01-01,17280.647 +2015-04-01,17380.875 +2015-07-01,17437.08 +2015-10-01,17462.579 +2016-01-01,17565.465 +2016-04-01,17618.581 +2016-07-01,17724.489 +2016-10-01,17812.56 +2017-01-01,17896.623 +2017-04-01,17996.802 +2017-07-01,18126.226 +2017-10-01,18296.685 +2018-01-01,18436.262 +2018-04-01,18590.004 +2018-07-01,18679.599 +2018-10-01,18721.281 +2019-01-01,18833.195 +2019-04-01,18982.528 +2019-07-01,19112.653 +2019-10-01,19202.31 +2020-01-01,18951.992 +2020-04-01,17258.205 +2020-07-01,18560.774 +2020-10-01,18767.778 +2021-01-01,19055.655 +2021-04-01,19368.31 +2021-07-01,19478.893 +2021-10-01,19806.29 +2022-01-01,19735.895 \ No newline at end of file diff --git a/test/output/recession.svg b/test/output/recession.svg new file mode 100644 index 0000000000..cfa3649b42 --- /dev/null +++ b/test/output/recession.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + −10 + −8 + −6 + −4 + −2 + 0 + +2 + +4 + +6 + +8 + +10 + + + ↑ cumulative change in GDP from the start of the last 5 recessions (%) + + + + + + + + + + + + + + + + + + + + + + + 4 + 8 + 12 + 16 + + + + + + + + + + ← Final quarter before recession + ← 9 quarters into recession + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1980 + 1990 + 2001 + 2008 + 2020 + + \ No newline at end of file diff --git a/test/output/recessionHalos.svg b/test/output/recessionHalos.svg new file mode 100644 index 0000000000..03d155b888 --- /dev/null +++ b/test/output/recessionHalos.svg @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + −10 + −8 + −6 + −4 + −2 + 0 + +2 + +4 + +6 + +8 + +10 + + + ↑ cumulative change in GDP from the start of the last 5 recessions (%) + + + + + + + + + + + + + + + + + + + + + + + 4 + 8 + 12 + 16 + + + + + + + + + + ← Final quarter before recession + ← 9 quarters into recession + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/index.js b/test/plots/index.js index dafea94656..720b7b15b9 100644 --- a/test/plots/index.js +++ b/test/plots/index.js @@ -296,4 +296,5 @@ export * from "./raster-ca55.js"; export * from "./raster-penguins.js"; export * from "./raster-vapor.js"; export * from "./raster-walmart.js"; +export * from "./recession.js"; export * from "./volcano.js"; diff --git a/test/plots/recession.js b/test/plots/recession.js new file mode 100644 index 0000000000..8544f19f0a --- /dev/null +++ b/test/plots/recession.js @@ -0,0 +1,174 @@ +import * as Plot from "@observablehq/plot"; +import * as d3 from "d3"; + +export async function recession() { + const gdp = await d3.csv("data/us-gdp.csv", d3.autoType); + const recession = ["1980-04-01", "1990-10-01", "2001-04-01", "2008-01-01", "2020-01-01"].map(d3.isoParse); + const quarters = 16; + const fredSeries = recession.flatMap((start) => { + const min = d3.utcMonth.offset(start, -3); + const max = d3.utcMonth.offset(start, quarters * 3); + return gdp + .filter((d) => d.date >= min && d.date < max) + .map((d) => ({...d, start, quarters: d3.utcMonth.every(3).range(d3.utcMonth.offset(start, -3), d.date).length})); + }); + return Plot.plot({ + width: 600, + height: 350, + marginRight: 30, + x: { + insetLeft: 20, + insetRight: 0, + ticks: quarters, + tickFormat: (d) => (d === 0 ? "" : d % 4 ? "" : d), + label: null, + line: true + }, + y: { + type: "log", + grid: true, + tickSize: 0, + tickFormat: (d1) => (d1 === 1 ? "0" : d3.format("+")(Math.round(100 * (d1 - 1)))), + label: "↑ cumulative change in GDP from the start of the last 5 recessions (%)", + insetTop: -10, + insetBottom: 15 + }, + color: {range: d3.schemeBlues[6].slice(-4).concat("red")}, + marks: [ + Plot.ruleY([1], {strokeWidth: 0.5}), + Plot.ruleX([0, 9], {strokeDasharray: [2, 4]}), + Plot.text([0, 9], { + x: [0, 9], + y: [1 + 7 / 100, 1 - 7 / 100], + textAnchor: "start", + text: ["← Final quarter before recession", "← 9 quarters into recession"], + dx: 4 + }), + Plot.line( + fredSeries, + Plot.normalizeY({ + x: "quarters", + y: "gdpc1", + stroke: "start", + marker: true, + halo: true + }) + ), + Plot.text( + fredSeries, + Plot.selectMaxX( + Plot.normalizeY({ + x: "quarters", + y: "gdpc1", + z: "start", + textAnchor: "start", + dx: 5, + text: (d) => String(d.start.getUTCFullYear()), + fill: (d) => String(d.start.getUTCFullYear()) + }) + ) + ) + ] + }); +} + +export async function recessionHalos() { + const gdp = await d3.csv("data/us-gdp.csv", d3.autoType); + const recession = ["1980-04-01", "1990-10-01", "2001-04-01", "2008-01-01", "2020-01-01"].map(d3.isoParse); + const quarters = 16; + const fredSeries = recession.flatMap((start) => { + const min = d3.utcMonth.offset(start, -3); + const max = d3.utcMonth.offset(start, quarters * 3); + return gdp + .filter((d) => d.date >= min && d.date < max) + .map((d) => ({...d, start, quarters: d3.utcMonth.every(3).range(d3.utcMonth.offset(start, -3), d.date).length})); + }); + const random = d3.randomLcg(42); + return Plot.plot({ + height: 350, + width: 600, + marginRight: 30, + marks: [ + Plot.ruleY([1], {strokeWidth: 0.5}), + Plot.ruleX([0, 9], {strokeDasharray: [2, 4]}), + Plot.text([0, 9], { + x: [0, 9], + y: [1 + 7 / 100, 1 - 7 / 100], + textAnchor: "start", + text: ["← Final quarter before recession", "← 9 quarters into recession"], + dx: 4 + }), + Plot.line( + fredSeries, + Plot.normalizeY({ + filter: (d) => d.start === recession[0], + x: "quarters", + y: "gdpc1", + stroke: "start", + halo: "lightblue" + }) + ), + Plot.line( + fredSeries, + Plot.normalizeY({ + filter: (d) => d.start === recession[1], + x: "quarters", + y: "gdpc1", + stroke: "start", + halo: true + }) + ), + Plot.line( + fredSeries, + Plot.normalizeY({ + filter: (d) => d.start === recession[2], + x: "quarters", + y: "gdpc1", + stroke: "start", + strokeWidth: () => 3 + 2 * random(), + halo: true + }) + ), + Plot.line( + fredSeries, + Plot.normalizeY({ + filter: (d) => d.start === recession[3], + x: "quarters", + y: "gdpc1", + stroke: "start", + halo: 7 + }) + ), + Plot.line( + fredSeries, + Plot.normalizeY({ + filter: (d) => d.start === recession[4], + x: "quarters", + y: "gdpc1", + stroke: "start", + strokeDasharray: "2% 1.5%", + haloColor: "pink", + haloRadius: 1 + }) + ) + ], + x: { + insetLeft: 20, + insetRight: 0, + ticks: quarters, + tickFormat: (d) => (d === 0 ? "" : d % 4 ? "" : d), + label: null, + line: true + }, + y: { + type: "log", + grid: true, + tickSize: 0, + tickFormat: (d1) => (d1 === 1 ? "0" : d3.format("+")(Math.round(100 * (d1 - 1)))), + label: "↑ cumulative change in GDP from the start of the last 5 recessions (%)", + insetTop: -10, + insetBottom: 15 + }, + color: {range: d3.schemeBlues[6].slice(-4).concat("red")} + }); +}