From 978c54eca3785ed3df6161a697d84174c023f1c4 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 17 Feb 2022 10:30:59 -0800 Subject: [PATCH] group aesthetics (#761) * group aesthetics * default filter * single paths for undefined data * relax none detection * default round caps and joins * warn on high-cardinality implicit z --- src/legends/swatches.js | 6 +- src/marks/area.js | 25 ++-- src/marks/line.js | 21 ++-- src/options.js | 17 +++ src/plot.js | 12 +- src/scales/ordinal.js | 5 +- src/style.js | 86 ++++++++++--- test/marks/line-test.js | 6 +- test/output/aaplBollinger.svg | 4 +- test/output/aaplClose.svg | 2 +- test/output/aaplCloseUntyped.svg | 2 +- test/output/availability.svg | 2 +- test/output/carsMpg.svg | 2 +- test/output/carsParcoords.svg | 2 +- test/output/covidIhmeProjectedDeaths.svg | 4 +- test/output/crimeanWarArrow.svg | 2 +- test/output/crimeanWarLine.svg | 2 +- test/output/gistempAnomalyMoving.svg | 2 +- test/output/googleTrendsRidgeline.svg | 128 ++++++++++---------- test/output/metroUnemployment.svg | 2 +- test/output/metroUnemploymentHighlight.svg | 2 +- test/output/metroUnemploymentIndex.svg | 2 +- test/output/metroUnemploymentMoving.svg | 2 +- test/output/metroUnemploymentNormalize.svg | 2 +- test/output/metroUnemploymentRidgeline.svg | 90 +++++++------- test/output/metroUnemploymentStroke.svg | 2 +- test/output/musicRevenue.svg | 2 +- test/output/randomWalk.svg | 4 +- test/output/sfTemperatureBand.svg | 4 +- test/output/sfTemperatureBandArea.svg | 2 +- test/output/simpsonsRatingsDots.svg | 2 +- test/output/stargazers.svg | 2 +- test/output/stocksIndex.svg | 2 +- test/output/travelersYearOverYear.svg | 4 +- test/output/usRetailSales.svg | 4 +- test/output/wealthBritainProportionPlot.svg | 2 +- 36 files changed, 272 insertions(+), 188 deletions(-) diff --git a/src/legends/swatches.js b/src/legends/swatches.js index 9266df201e..41cbcf3b85 100644 --- a/src/legends/swatches.js +++ b/src/legends/swatches.js @@ -1,8 +1,8 @@ import {create, path} from "d3"; import {inferFontVariant} from "../axes.js"; import {maybeTickFormat} from "../axis.js"; -import {maybeColorChannel, maybeNumberChannel} from "../options.js"; -import {applyInlineStyles, impliedString, maybeClassName, none} from "../style.js"; +import {isNoneish, maybeColorChannel, maybeNumberChannel} from "../options.js"; +import {applyInlineStyles, impliedString, maybeClassName} from "../style.js"; function maybeScale(scale, key) { if (key == null) return key; @@ -29,7 +29,7 @@ export function legendSwatches(color, options) { export function legendSymbols(symbol, { fill = symbol.hint?.fill !== undefined ? symbol.hint.fill : "none", fillOpacity = 1, - stroke = symbol.hint?.stroke !== undefined ? symbol.hint.stroke : none(fill) ? "currentColor" : "none", + stroke = symbol.hint?.stroke !== undefined ? symbol.hint.stroke : isNoneish(fill) ? "currentColor" : "none", strokeOpacity = 1, strokeWidth = 1.5, r = 4.5, diff --git a/src/marks/area.js b/src/marks/area.js index 625b142464..809dbfdd9c 100644 --- a/src/marks/area.js +++ b/src/marks/area.js @@ -1,49 +1,52 @@ -import {area as shapeArea, create, group} from "d3"; +import {area as shapeArea, create} from "d3"; import {Curve} from "../curve.js"; -import {defined} from "../defined.js"; import {Mark} from "../plot.js"; import {indexOf, maybeZ} from "../options.js"; -import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} from "../style.js"; +import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, groupIndex} from "../style.js"; import {maybeIdentityX, maybeIdentityY} from "../transforms/identity.js"; import {maybeStackX, maybeStackY} from "../transforms/stack.js"; const defaults = { + filter: null, ariaLabel: "area", strokeWidth: 1, + strokeLinecap: "round", + strokeLinejoin: "round", strokeMiterlimit: 1 }; export class Area extends Mark { constructor(data, options = {}) { - const {x1, y1, x2, y2, curve, tension} = options; + const {x1, y1, x2, y2, z, curve, tension} = options; super( data, [ - {name: "x1", value: x1, filter: null, scale: "x"}, - {name: "y1", value: y1, filter: null, scale: "y"}, - {name: "x2", value: x2, filter: null, scale: "x", optional: true}, - {name: "y2", value: y2, filter: null, scale: "y", optional: true}, + {name: "x1", value: x1, scale: "x"}, + {name: "y1", value: y1, scale: "y"}, + {name: "x2", value: x2, scale: "x", optional: true}, + {name: "y2", value: y2, scale: "y", optional: true}, {name: "z", value: maybeZ(options), optional: true} ], options, defaults ); + this.z = z; this.curve = Curve(curve, tension); } render(I, {x, y}, channels, dimensions) { - const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1, z: Z} = channels; + const {x1: X1, y1: Y1, x2: X2 = X1, y2: Y2 = Y1} = channels; const {dx, dy} = this; return create("svg:g") .call(applyIndirectStyles, this, dimensions) .call(applyTransform, x, y, dx, dy) .call(g => g.selectAll() - .data(Z ? group(I, i => Z[i]).values() : [I]) + .data(groupIndex(I, [X1, Y1, X2, Y2], this, channels)) .join("path") .call(applyDirectStyles, this) .call(applyGroupedChannelStyles, this, channels) .attr("d", shapeArea() .curve(this.curve) - .defined(i => defined(X1[i]) && defined(Y1[i]) && defined(X2[i]) && defined(Y2[i])) + .defined(i => i >= 0) .x0(i => X1[i]) .y0(i => Y1[i]) .x1(i => X2[i]) diff --git a/src/marks/line.js b/src/marks/line.js index 209d40147f..6cbde103bf 100644 --- a/src/marks/line.js +++ b/src/marks/line.js @@ -1,50 +1,53 @@ -import {create, group, line as shapeLine} from "d3"; +import {create, line as shapeLine} from "d3"; import {Curve} from "../curve.js"; -import {defined} from "../defined.js"; import {Mark} from "../plot.js"; import {indexOf, identity, maybeTuple, maybeZ} from "../options.js"; -import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, offset} from "../style.js"; +import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, offset, groupIndex} from "../style.js"; import {applyGroupedMarkers, markers} from "./marker.js"; const defaults = { + filter: null, ariaLabel: "line", fill: "none", stroke: "currentColor", strokeWidth: 1.5, + strokeLinecap: "round", + strokeLinejoin: "round", strokeMiterlimit: 1 }; export class Line extends Mark { constructor(data, options = {}) { - const {x, y, curve, tension} = options; + const {x, y, z, curve, tension} = options; super( data, [ - {name: "x", value: x, filter: null, scale: "x"}, - {name: "y", value: y, filter: null, scale: "y"}, + {name: "x", value: x, scale: "x"}, + {name: "y", value: y, scale: "y"}, {name: "z", value: maybeZ(options), optional: true} ], options, defaults ); + this.z = z; this.curve = Curve(curve, tension); markers(this, options); } render(I, {x, y}, channels, dimensions) { - const {x: X, y: Y, z: Z} = channels; + const {x: X, y: Y} = channels; const {dx, dy} = this; return create("svg:g") .call(applyIndirectStyles, this, dimensions) .call(applyTransform, x, y, offset + dx, offset + dy) .call(g => g.selectAll() - .data(Z ? group(I, i => Z[i]).values() : [I]) + .data(groupIndex(I, [X, Y], this, channels)) .join("path") .call(applyDirectStyles, this) .call(applyGroupedChannelStyles, this, channels) .call(applyGroupedMarkers, this, channels) .attr("d", shapeLine() .curve(this.curve) - .defined(i => defined(X[i]) && defined(Y[i])) + .defined(i => i >= 0) .x(i => X[i]) .y(i => Y[i]))) .node(); diff --git a/src/options.js b/src/options.js index 5eeb0aefe2..1456537ce2 100644 --- a/src/options.js +++ b/src/options.js @@ -123,6 +123,11 @@ export function take(values, index) { return Array.from(index, i => values[i]); } +// Based on InternMap (d3.group). +export function keyof(value) { + return value !== null && typeof value === "object" ? value.valueOf() : value; +} + export function maybeInput(key, options) { if (options[key] !== undefined) return options[key]; switch (key) { @@ -271,6 +276,18 @@ export function isColor(value) { || color(value) !== null; } +export function isNoneish(value) { + return value == null || isNone(value); +} + +export function isNone(value) { + return /^\s*none\s*$/i.test(value); +} + +export function isRound(value) { + return /^\s*round\s*$/i.test(value); +} + const symbols = new Map([ ["asterisk", symbolAsterisk], ["circle", symbolCircle], diff --git a/src/plot.js b/src/plot.js index 5299d73fb2..d619b6bd9d 100644 --- a/src/plot.js +++ b/src/plot.js @@ -99,7 +99,8 @@ export function plot(options = {}) { for (const mark of marks) { const channels = markChannels.get(mark) ?? []; const values = applyScales(channels, scales); - const index = filter(markIndex.get(mark), channels, values); + let index = markIndex.get(mark); + if (mark.filter != null) index = mark.filter(index, channels, values); const node = mark.render(index, scales, values, dimensions, axes); if (node != null) svg.appendChild(node); } @@ -136,7 +137,7 @@ export function plot(options = {}) { return figure; } -function filter(index, channels, values) { +function defaultFilter(index, channels, values) { for (const [name, {filter = defined}] of channels) { if (name !== undefined && filter !== null) { const value = values[name]; @@ -154,6 +155,7 @@ export class Mark { this.sort = isOptions(sort) ? sort : null; this.facet = facet == null || facet === false ? null : keyword(facet === true ? "include" : facet, "facet", ["auto", "include", "exclude"]); const {transform} = basic(options); + this.filter = defaults?.filter === undefined ? defaultFilter : defaults.filter; this.transform = transform; if (defaults !== undefined) channels = styles(this, options, channels, defaults); this.channels = channels.filter(channel => { @@ -328,9 +330,11 @@ class Facet extends Mark { .each(function(key) { const marksFacetIndex = marksIndexByFacet.get(key); for (let i = 0; i < marks.length; ++i) { + const mark = marks[i]; const values = marksValues[i]; - const index = filter(marksFacetIndex[i], marksChannels[i], values); - const node = marks[i].render(index, scales, values, subdimensions); + let index = marksFacetIndex[i]; + if (mark.filter != null) index = mark.filter(index, marksChannels[i], values); + const node = mark.render(index, scales, values, subdimensions); if (node != null) this.appendChild(node); } })) diff --git a/src/scales/ordinal.js b/src/scales/ordinal.js index d07a53d638..90ee306dec 100644 --- a/src/scales/ordinal.js +++ b/src/scales/ordinal.js @@ -1,8 +1,7 @@ import {InternSet, quantize, reverse as reverseof, sort, symbolsFill, symbolsStroke} from "d3"; import {scaleBand, scaleOrdinal, scalePoint, scaleImplicit} from "d3"; import {ascendingDefined} from "../defined.js"; -import {maybeSymbol} from "../options.js"; -import {none} from "../style.js"; +import {maybeSymbol, isNoneish} from "../options.js"; import {registry, color, symbol} from "./index.js"; import {maybeBooleanRange, ordinalScheme, quantitativeScheme} from "./schemes.js"; @@ -127,5 +126,5 @@ function inferSymbolHint(channels) { } function inferSymbolRange(hint) { - return none(hint.fill) ? symbolsStroke : symbolsFill; + return isNoneish(hint.fill) ? symbolsStroke : symbolsFill; } diff --git a/src/style.js b/src/style.js index c85e17934d..76416d47a6 100644 --- a/src/style.js +++ b/src/style.js @@ -1,7 +1,8 @@ -import {isoFormat, namespaces} from "d3"; -import {nonempty} from "./defined.js"; +import {group, isoFormat, namespaces} from "d3"; +import {defined, nonempty} from "./defined.js"; import {formatNumber} from "./format.js"; -import {string, number, maybeColorChannel, maybeNumberChannel, isTemporal, isNumeric} from "./options.js"; +import {string, number, maybeColorChannel, maybeNumberChannel, isTemporal, isNumeric, isNoneish, isNone, isRound, keyof} from "./options.js"; +import {warn} from "./warnings.js"; export const offset = typeof window !== "undefined" && window.devicePixelRatio > 1 ? 0 : 0.5; @@ -64,10 +65,10 @@ export function styles( // applies if the stroke is (constant) none; if you set a stroke, then the // default fill becomes none. Similarly for marks that stroke by stroke, the // default stroke only applies if the fill is (constant) none. - if (none(defaultFill)) { - if (!none(defaultStroke) && !none(fill)) defaultStroke = "none"; + if (isNoneish(defaultFill)) { + if (!isNoneish(defaultStroke) && !isNoneish(fill)) defaultStroke = "none"; } else { - if (none(defaultStroke) && !none(stroke)) defaultFill = "none"; + if (isNoneish(defaultStroke) && !isNoneish(stroke)) defaultFill = "none"; } const [vfill, cfill] = maybeColorChannel(fill, defaultFill); @@ -79,16 +80,19 @@ export function styles( // For styles that have no effect if there is no stroke, only apply the // defaults if the stroke is not the constant none. (If stroke is a channel, // then cstroke will be undefined, but there’s still a stroke; hence we don’t - // use the none helper here.) - if (cstroke !== "none") { + // use isNoneish here.) + if (!isNone(cstroke)) { if (strokeWidth === undefined) strokeWidth = defaultStrokeWidth; if (strokeLinecap === undefined) strokeLinecap = defaultStrokeLinecap; if (strokeLinejoin === undefined) strokeLinejoin = defaultStrokeLinejoin; - if (strokeMiterlimit === undefined) strokeMiterlimit = defaultStrokeMiterlimit; + + // The default stroke miterlimit need not be applied if the current stroke + // is the constant round; this only has effect on miter joins. + if (strokeMiterlimit === undefined && !isRound(strokeLinejoin)) strokeMiterlimit = defaultStrokeMiterlimit; // The paint order only takes effect if there is both a fill and a stroke // (at least if we ignore markers, which no built-in marks currently use). - if (cfill !== "none" && paintOrder === undefined) paintOrder = defaultPaintOrder; + if (!isNone(cfill) && paintOrder === undefined) paintOrder = defaultPaintOrder; } const [vstrokeWidth, cstrokeWidth] = maybeNumberChannel(strokeWidth); @@ -176,6 +180,64 @@ export function applyGroupedChannelStyles(selection, {target}, {ariaLabel: AL, t applyTitleGroup(selection, T); } +function groupAesthetics({ariaLabel: AL, title: T, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO, strokeWidth: SW, opacity: O, href: H}) { + return [AL, T, F, FO, S, SO, SW, O, H].filter(c => c !== undefined); +} + +function groupZ(I, Z, z) { + const G = group(I, i => Z[i]); + if (z === undefined && G.size > I.length >> 2) { + warn(`Warning: the implicit z channel has high cardinality. This may occur when the fill or stroke channel is associated with quantitative data rather than ordinal or categorical data. You can suppress this warning by setting the z option explicitly; if this data represents a single series, set z to null.`); + } + return G.values(); +} + +export function* groupIndex(I, position, {z}, channels) { + const {z: Z} = channels; // group channel + const A = groupAesthetics(channels); // aesthetic channels + const C = [...position, ...A]; // all channels + + // Group the current index by Z (if any). + 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 + out: for (const i of G) { + + // If any channel has an undefined value for this index, skip it. + for (const c of C) { + if (!defined(c[i])) { + if (Gg) Gg.push(-1); + continue out; + } + } + + // 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]; + continue; + } + + // Otherwise, add the current index to the current group. Then, if any of + // the aesthetics don’t match the current group, yield the current group + // and start a new group of the current index. + Gg.push(i); + 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]; + continue out; + } + } + } + + // Yield the current group, if any. + if (Gg) yield Gg; + } +} + // clip: true clips to the frame // TODO: accept other types of clips (paths, urls, x, y, other marks?…) // https://github.com/observablehq/plot/issues/181 @@ -254,10 +316,6 @@ export function impliedNumber(value, impliedValue) { if ((value = number(value)) !== impliedValue) return value; } -export function none(color) { - return color == null || /^\s*none\s*$/i.test(color); -} - 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) { diff --git a/test/marks/line-test.js b/test/marks/line-test.js index 45ac5a7d7d..855ec2f44f 100644 --- a/test/marks/line-test.js +++ b/test/marks/line-test.js @@ -15,9 +15,9 @@ it("line() has the expected defaults", () => { assert.strictEqual(line.stroke, "currentColor"); assert.strictEqual(line.strokeWidth, 1.5); assert.strictEqual(line.strokeOpacity, undefined); - assert.strictEqual(line.strokeLinejoin, undefined); - assert.strictEqual(line.strokeLinecap, undefined); - assert.strictEqual(line.strokeMiterlimit, 1); + assert.strictEqual(line.strokeLinejoin, "round"); + assert.strictEqual(line.strokeLinecap, "round"); + assert.strictEqual(line.strokeMiterlimit, undefined); assert.strictEqual(line.strokeDasharray, undefined); assert.strictEqual(line.strokeDashoffset, undefined); assert.strictEqual(line.mixBlendMode, undefined); diff --git a/test/output/aaplBollinger.svg b/test/output/aaplBollinger.svg index 9832b70148..8c0b0bb19b 100644 --- a/test/output/aaplBollinger.svg +++ b/test/output/aaplBollinger.svg @@ -91,10 +91,10 @@ - + - + \ No newline at end of file diff --git a/test/output/aaplClose.svg b/test/output/aaplClose.svg index 2fcfc1ca27..0a6824c72b 100644 --- a/test/output/aaplClose.svg +++ b/test/output/aaplClose.svg @@ -75,7 +75,7 @@ - + diff --git a/test/output/aaplCloseUntyped.svg b/test/output/aaplCloseUntyped.svg index 9a2a058825..ad4cba533a 100644 --- a/test/output/aaplCloseUntyped.svg +++ b/test/output/aaplCloseUntyped.svg @@ -72,7 +72,7 @@ 2018 - + diff --git a/test/output/availability.svg b/test/output/availability.svg index b0b2069689..c00322a192 100644 --- a/test/output/availability.svg +++ b/test/output/availability.svg @@ -56,7 +56,7 @@ - + diff --git a/test/output/carsMpg.svg b/test/output/carsMpg.svg index 78d6db060e..4ed068ea38 100644 --- a/test/output/carsMpg.svg +++ b/test/output/carsMpg.svg @@ -96,7 +96,7 @@ 82 year - + diff --git a/test/output/carsParcoords.svg b/test/output/carsParcoords.svg index 4966ee8586..b5926d6849 100644 --- a/test/output/carsParcoords.svg +++ b/test/output/carsParcoords.svg @@ -45,7 +45,7 @@ - + diff --git a/test/output/covidIhmeProjectedDeaths.svg b/test/output/covidIhmeProjectedDeaths.svg index 65c70f8358..5a5704c756 100644 --- a/test/output/covidIhmeProjectedDeaths.svg +++ b/test/output/covidIhmeProjectedDeaths.svg @@ -226,12 +226,12 @@ cone of uncertainty - + actual data - + projected values diff --git a/test/output/crimeanWarArrow.svg b/test/output/crimeanWarArrow.svg index ceaebb2d75..7b1469ff05 100644 --- a/test/output/crimeanWarArrow.svg +++ b/test/output/crimeanWarArrow.svg @@ -86,7 +86,7 @@ - + diff --git a/test/output/crimeanWarLine.svg b/test/output/crimeanWarLine.svg index 5e33f8b204..4def76a495 100644 --- a/test/output/crimeanWarLine.svg +++ b/test/output/crimeanWarLine.svg @@ -86,7 +86,7 @@ - + diff --git a/test/output/gistempAnomalyMoving.svg b/test/output/gistempAnomalyMoving.svg index 3e0722ee5d..30876377b4 100644 --- a/test/output/gistempAnomalyMoving.svg +++ b/test/output/gistempAnomalyMoving.svg @@ -1727,7 +1727,7 @@ - + \ No newline at end of file diff --git a/test/output/googleTrendsRidgeline.svg b/test/output/googleTrendsRidgeline.svg index 64663d667f..60b0ece5e0 100644 --- a/test/output/googleTrendsRidgeline.svg +++ b/test/output/googleTrendsRidgeline.svg @@ -250,7 +250,7 @@ - + @@ -261,7 +261,7 @@ - + @@ -272,7 +272,7 @@ - + @@ -283,7 +283,7 @@ - + @@ -294,7 +294,7 @@ - + @@ -305,7 +305,7 @@ - + @@ -316,7 +316,7 @@ - + @@ -327,7 +327,7 @@ - + @@ -338,7 +338,7 @@ - + @@ -349,7 +349,7 @@ - + @@ -360,7 +360,7 @@ - + @@ -371,7 +371,7 @@ - + @@ -382,7 +382,7 @@ - + @@ -393,7 +393,7 @@ - + @@ -404,7 +404,7 @@ - + @@ -415,7 +415,7 @@ - + @@ -426,7 +426,7 @@ - + @@ -437,7 +437,7 @@ - + @@ -448,7 +448,7 @@ - + @@ -459,7 +459,7 @@ - + @@ -470,7 +470,7 @@ - + @@ -481,7 +481,7 @@ - + @@ -492,7 +492,7 @@ - + @@ -503,7 +503,7 @@ - + @@ -514,7 +514,7 @@ - + @@ -525,7 +525,7 @@ - + @@ -536,7 +536,7 @@ - + @@ -547,7 +547,7 @@ - + @@ -558,7 +558,7 @@ - + @@ -569,7 +569,7 @@ - + @@ -580,7 +580,7 @@ - + @@ -591,7 +591,7 @@ - + @@ -602,7 +602,7 @@ - + @@ -613,7 +613,7 @@ - + @@ -624,7 +624,7 @@ - + @@ -635,7 +635,7 @@ - + @@ -646,7 +646,7 @@ - + @@ -657,7 +657,7 @@ - + @@ -668,7 +668,7 @@ - + @@ -679,7 +679,7 @@ - + @@ -690,7 +690,7 @@ - + @@ -701,7 +701,7 @@ - + @@ -712,7 +712,7 @@ - + @@ -723,7 +723,7 @@ - + @@ -734,7 +734,7 @@ - + @@ -745,7 +745,7 @@ - + @@ -756,7 +756,7 @@ - + @@ -767,7 +767,7 @@ - + @@ -778,7 +778,7 @@ - + @@ -789,7 +789,7 @@ - + @@ -800,7 +800,7 @@ - + @@ -811,7 +811,7 @@ - + @@ -822,7 +822,7 @@ - + @@ -833,7 +833,7 @@ - + @@ -844,7 +844,7 @@ - + @@ -855,7 +855,7 @@ - + @@ -866,7 +866,7 @@ - + @@ -877,7 +877,7 @@ - + @@ -888,7 +888,7 @@ - + @@ -899,7 +899,7 @@ - + @@ -910,7 +910,7 @@ - + @@ -921,7 +921,7 @@ - + @@ -932,7 +932,7 @@ - + @@ -943,7 +943,7 @@ - + diff --git a/test/output/metroUnemployment.svg b/test/output/metroUnemployment.svg index 4d48c16d71..902e59b870 100644 --- a/test/output/metroUnemployment.svg +++ b/test/output/metroUnemployment.svg @@ -65,7 +65,7 @@ 2012 - + diff --git a/test/output/metroUnemploymentHighlight.svg b/test/output/metroUnemploymentHighlight.svg index 10db30463e..56afed79e8 100644 --- a/test/output/metroUnemploymentHighlight.svg +++ b/test/output/metroUnemploymentHighlight.svg @@ -77,7 +77,7 @@ - + diff --git a/test/output/metroUnemploymentIndex.svg b/test/output/metroUnemploymentIndex.svg index 6da9a70bef..400e691d6f 100644 --- a/test/output/metroUnemploymentIndex.svg +++ b/test/output/metroUnemploymentIndex.svg @@ -62,7 +62,7 @@ 7,000 - + \ No newline at end of file diff --git a/test/output/metroUnemploymentMoving.svg b/test/output/metroUnemploymentMoving.svg index a770904d1e..aec01032ef 100644 --- a/test/output/metroUnemploymentMoving.svg +++ b/test/output/metroUnemploymentMoving.svg @@ -65,7 +65,7 @@ 2012 - + diff --git a/test/output/metroUnemploymentNormalize.svg b/test/output/metroUnemploymentNormalize.svg index 8ab5c876de..6f16e3b11c 100644 --- a/test/output/metroUnemploymentNormalize.svg +++ b/test/output/metroUnemploymentNormalize.svg @@ -66,7 +66,7 @@ 2012 - + diff --git a/test/output/metroUnemploymentRidgeline.svg b/test/output/metroUnemploymentRidgeline.svg index d8c688033d..5476bbf722 100644 --- a/test/output/metroUnemploymentRidgeline.svg +++ b/test/output/metroUnemploymentRidgeline.svg @@ -178,7 +178,7 @@ - + @@ -189,7 +189,7 @@ - + @@ -200,7 +200,7 @@ - + @@ -211,7 +211,7 @@ - + @@ -222,7 +222,7 @@ - + @@ -233,7 +233,7 @@ - + @@ -244,7 +244,7 @@ - + @@ -255,7 +255,7 @@ - + @@ -266,7 +266,7 @@ - + @@ -277,7 +277,7 @@ - + @@ -288,7 +288,7 @@ - + @@ -299,7 +299,7 @@ - + @@ -310,7 +310,7 @@ - + @@ -321,7 +321,7 @@ - + @@ -332,7 +332,7 @@ - + @@ -343,7 +343,7 @@ - + @@ -354,7 +354,7 @@ - + @@ -365,7 +365,7 @@ - + @@ -376,7 +376,7 @@ - + @@ -387,7 +387,7 @@ - + @@ -398,7 +398,7 @@ - + @@ -409,7 +409,7 @@ - + @@ -420,7 +420,7 @@ - + @@ -431,7 +431,7 @@ - + @@ -442,7 +442,7 @@ - + @@ -453,7 +453,7 @@ - + @@ -464,7 +464,7 @@ - + @@ -475,7 +475,7 @@ - + @@ -486,7 +486,7 @@ - + @@ -497,7 +497,7 @@ - + @@ -508,7 +508,7 @@ - + @@ -519,7 +519,7 @@ - + @@ -530,7 +530,7 @@ - + @@ -541,7 +541,7 @@ - + @@ -552,7 +552,7 @@ - + @@ -563,7 +563,7 @@ - + @@ -574,7 +574,7 @@ - + @@ -585,7 +585,7 @@ - + @@ -596,7 +596,7 @@ - + @@ -607,7 +607,7 @@ - + @@ -618,7 +618,7 @@ - + @@ -629,7 +629,7 @@ - + @@ -640,7 +640,7 @@ - + @@ -651,7 +651,7 @@ - + @@ -662,7 +662,7 @@ - + diff --git a/test/output/metroUnemploymentStroke.svg b/test/output/metroUnemploymentStroke.svg index 291c6592a0..34e3a17649 100644 --- a/test/output/metroUnemploymentStroke.svg +++ b/test/output/metroUnemploymentStroke.svg @@ -65,7 +65,7 @@ 2012 - + diff --git a/test/output/musicRevenue.svg b/test/output/musicRevenue.svg index 955206c841..94d4ca8c72 100644 --- a/test/output/musicRevenue.svg +++ b/test/output/musicRevenue.svg @@ -186,7 +186,7 @@ Vinyl - + diff --git a/test/output/randomWalk.svg b/test/output/randomWalk.svg index 7126003308..ccd0564c17 100644 --- a/test/output/randomWalk.svg +++ b/test/output/randomWalk.svg @@ -83,10 +83,10 @@ 450 - + - + \ No newline at end of file diff --git a/test/output/sfTemperatureBand.svg b/test/output/sfTemperatureBand.svg index 67b46c3315..778ec63097 100644 --- a/test/output/sfTemperatureBand.svg +++ b/test/output/sfTemperatureBand.svg @@ -83,10 +83,10 @@ - + - + \ No newline at end of file diff --git a/test/output/sfTemperatureBandArea.svg b/test/output/sfTemperatureBandArea.svg index 93e919cc65..f84b79828b 100644 --- a/test/output/sfTemperatureBandArea.svg +++ b/test/output/sfTemperatureBandArea.svg @@ -107,7 +107,7 @@ - + \ No newline at end of file diff --git a/test/output/simpsonsRatingsDots.svg b/test/output/simpsonsRatingsDots.svg index 0593275f7c..af34bdab9b 100644 --- a/test/output/simpsonsRatingsDots.svg +++ b/test/output/simpsonsRatingsDots.svg @@ -161,7 +161,7 @@ - + diff --git a/test/output/stargazers.svg b/test/output/stargazers.svg index af4d11dea5..313af3ca65 100644 --- a/test/output/stargazers.svg +++ b/test/output/stargazers.svg @@ -85,7 +85,7 @@ - + 1,096 diff --git a/test/output/stocksIndex.svg b/test/output/stocksIndex.svg index 719f98a74a..f059683662 100644 --- a/test/output/stocksIndex.svg +++ b/test/output/stocksIndex.svg @@ -75,7 +75,7 @@ - + diff --git a/test/output/travelersYearOverYear.svg b/test/output/travelersYearOverYear.svg index e5fbf953e1..1919287cdd 100644 --- a/test/output/travelersYearOverYear.svg +++ b/test/output/travelersYearOverYear.svg @@ -114,10 +114,10 @@ - + - + 2019 diff --git a/test/output/usRetailSales.svg b/test/output/usRetailSales.svg index 921047f81f..50003f955f 100644 --- a/test/output/usRetailSales.svg +++ b/test/output/usRetailSales.svg @@ -83,10 +83,10 @@ 2020 - + - + diff --git a/test/output/wealthBritainProportionPlot.svg b/test/output/wealthBritainProportionPlot.svg index 6b1ac7cefa..d99b9a3e18 100644 --- a/test/output/wealthBritainProportionPlot.svg +++ b/test/output/wealthBritainProportionPlot.svg @@ -21,7 +21,7 @@ Share of wealth - +