From 19780531e566ff32827496dba11b702d0b6e8046 Mon Sep 17 00:00:00 2001 From: gka Date: Tue, 27 May 2025 19:56:43 +0200 Subject: [PATCH 1/4] fix: determine point/band scale domain from interval --- src/lib/helpers/autoTicks.ts | 16 ++++++++-------- src/lib/helpers/scales.ts | 19 ++++++++++++++++++- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/lib/helpers/autoTicks.ts b/src/lib/helpers/autoTicks.ts index eff9de52..6f747204 100644 --- a/src/lib/helpers/autoTicks.ts +++ b/src/lib/helpers/autoTicks.ts @@ -1,6 +1,6 @@ import type { RawValue, ScaleType } from '$lib/types.js'; import { maybeTimeInterval } from './time.js'; -import { range as rangei } from 'd3-array'; +import { extent, range as rangei } from 'd3-array'; export function maybeInterval(interval: null | number | string | ((d: T) => T)) { if (interval == null) return; @@ -38,11 +38,11 @@ export function autoTicks( scaleFn, count: number ) { - return ticks - ? ticks - : interval - ? maybeInterval(interval, type).range(domain[0], domain[1]) - : typeof scaleFn.ticks === 'function' - ? scaleFn.ticks(count) - : []; + if (ticks) return ticks; + if (interval) { + const [lo, hi] = extent(domain); + const I = maybeInterval(interval, type); + return I.range(lo, I.offset(hi)); + } + return typeof scaleFn.ticks === 'function' ? scaleFn.ticks(count) : []; } diff --git a/src/lib/helpers/scales.ts b/src/lib/helpers/scales.ts index 2d5be986..f8cca117 100644 --- a/src/lib/helpers/scales.ts +++ b/src/lib/helpers/scales.ts @@ -29,6 +29,7 @@ import type { import isDataRecord from './isDataRecord.js'; import { createProjection } from './projection.js'; +import { maybeInterval } from './autoTicks.js'; /** * compute the plot scales @@ -302,7 +303,7 @@ export function createScale( const valueArray = type === 'quantile' || type === 'quantile-cont' ? allDataValues.toSorted() : valueArr; - const domain = scaleOptions.domain + let domain = scaleOptions.domain ? isOrdinal ? scaleOptions.domain : extent(scaleOptions.zero ? [0, ...scaleOptions.domain] : scaleOptions.domain) @@ -317,6 +318,16 @@ export function createScale( : valueArray : extent(scaleOptions.zero ? [0, ...valueArray] : valueArray); + if (scaleOptions.interval) { + if (isOrdinal) { + domain = domainFromInterval(domain, scaleOptions.interval); + } else { + throw new Error( + 'Setting interval via axis options is only supported for ordinal scales' + ); + } + } + if (!scaleOptions.scale) { throw new Error(`No scale function defined for ${name}`); } @@ -350,6 +361,12 @@ export function createScale( }; } +function domainFromInterval(domain: RawValue[], interval: string | number) { + const interval_ = maybeInterval(interval); + const [lo, hi] = extent(domain); + return interval_.range(lo, interval_.offset(hi)); +} + /** * Infer a scale type based on the scale name, the data values mapped to it and * the mark types that are bound to the scale From 9867cf0cfb6ef72768b167a89c613336aa766a22 Mon Sep 17 00:00:00 2001 From: gka Date: Tue, 27 May 2025 19:57:05 +0200 Subject: [PATCH 2/4] catch and log Plot-level errors --- src/lib/Plot.svelte | 150 ++++++++++++++++++++++++-------------------- 1 file changed, 81 insertions(+), 69 deletions(-) diff --git a/src/lib/Plot.svelte b/src/lib/Plot.svelte index 665c5412..53750c28 100644 --- a/src/lib/Plot.svelte +++ b/src/lib/Plot.svelte @@ -91,89 +91,101 @@ - - {#snippet children({ - hasProjection, - hasExplicitAxisX, - hasExplicitAxisY, - hasExplicitGridX, - hasExplicitGridY, - options, - scales, - ...restProps - })} - console.warn(err)}> - - {#if !hasProjection && !hasExplicitAxisX} - {#if options.axes && (options.x.axis === 'top' || options.x.axis === 'both')} - + + + {#snippet children({ + hasProjection, + hasExplicitAxisX, + hasExplicitAxisY, + hasExplicitGridX, + hasExplicitGridY, + options, + scales, + ...restProps + })} + console.warn(err)}> + + {#if !hasProjection && !hasExplicitAxisX} + {#if options.axes && (options.x.axis === 'top' || options.x.axis === 'both')} + + {/if} + {#if options.axes && (options.x.axis === 'bottom' || options.x.axis === 'both')} + + {/if} {/if} - {#if options.axes && (options.x.axis === 'bottom' || options.x.axis === 'both')} - + {#if !hasProjection && !hasExplicitAxisY} + {#if options.axes && (options.y.axis === 'left' || options.y.axis === 'both')} + + {/if} + {#if options.axes && (options.y.axis === 'right' || options.y.axis === 'both')} + + {/if} {/if} - {/if} - {#if !hasProjection && !hasExplicitAxisY} - {#if options.axes && (options.y.axis === 'left' || options.y.axis === 'both')} - + + {#if !hasExplicitGridX && (options.grid || options.x.grid)} + {/if} - {#if options.axes && (options.y.axis === 'right' || options.y.axis === 'both')} - + {#if !hasExplicitGridY && (options.grid || options.y.grid)} + {/if} - {/if} - - {#if !hasExplicitGridX && (options.grid || options.x.grid)} - - {/if} - {#if !hasExplicitGridY && (options.grid || options.y.grid)} - - {/if} - - {#if options.frame} - - {/if} - {@render parentChildren?.({ - options, - scales, - ...restProps - })} - {#snippet failed(error, reset)} - - {#each error.message.split('\n') as line, i (i)} - {line} - {/each} - {/snippet} - - {/snippet} - {#snippet facetAxes()} - + + {#if options.frame} + + {/if} + {@render parentChildren?.({ + options, + scales, + ...restProps + })} + {#snippet failed(error, reset)} + + {#each error.message.split('\n') as line, i (i)} + {line} + {/each} + {/snippet} + + {/snippet} + {#snippet facetAxes()} + + {/snippet} + + {#snippet failed(error)} +
Error: {error.message}
{/snippet} - + From e1112f8b20f3760cad068a739f2ea30ac96bfc54 Mon Sep 17 00:00:00 2001 From: gka Date: Tue, 27 May 2025 21:30:00 +0200 Subject: [PATCH 3/4] add bar examples --- src/lib/helpers/scales.ts | 15 +++-- src/routes/marks/bar/+page.md | 119 +++++++++++++--------------------- 2 files changed, 53 insertions(+), 81 deletions(-) diff --git a/src/lib/helpers/scales.ts b/src/lib/helpers/scales.ts index f8cca117..87d3a411 100644 --- a/src/lib/helpers/scales.ts +++ b/src/lib/helpers/scales.ts @@ -320,11 +320,13 @@ export function createScale( if (scaleOptions.interval) { if (isOrdinal) { - domain = domainFromInterval(domain, scaleOptions.interval); + domain = domainFromInterval(domain, scaleOptions.interval, name); } else { - throw new Error( - 'Setting interval via axis options is only supported for ordinal scales' - ); + if (markTypes.size > 0) { + console.warn( + 'Setting interval via axis options is only supported for ordinal scales' + ); + } } } @@ -361,10 +363,11 @@ export function createScale( }; } -function domainFromInterval(domain: RawValue[], interval: string | number) { +function domainFromInterval(domain: RawValue[], interval: string | number, name: ScaleName) { const interval_ = maybeInterval(interval); const [lo, hi] = extent(domain); - return interval_.range(lo, interval_.offset(hi)); + const out = interval_.range(lo, interval_.offset(hi)); + return name === 'y' ? out.toReversed() : out; } /** diff --git a/src/routes/marks/bar/+page.md b/src/routes/marks/bar/+page.md index 5b9f2333..b23c53db 100644 --- a/src/routes/marks/bar/+page.md +++ b/src/routes/marks/bar/+page.md @@ -7,33 +7,63 @@ title: Bar mark import StackedBarPlot from './StackedBarPlot.svelte'; -Bars are cool. They come in two flavors: [BarY](#BarY) for vertical bars (columns) and [BarX](#BarX) for horizontal bars. - -Here's a very simple bar chart: +Bars are useful to show quantitative data for different categories. They come in two flavors: [BarX](#BarX) for horizontal bars (y axis requires band scale) and [BarY](#BarY) for vertical bars aka. columns (x axis requires band scale). ```svelte live - - + + ``` ```svelte - - + + + + +``` + +[fork](https://svelte.dev/playground/7a0d38cf74be4a9985feb7bef0456008?version=5) + +SveltePlot automatically infers a band scale for the y axis in the above example. but since our data is missing a value for 2023, the value `"2023"` is entirely missing from the band scale domain. We could fix this by passing the domain value manually, or by using the `interval` option of the y axis: + +```svelte live + + + + + + +``` + +``` + + ``` -You can create stacked bar charts by defining a fill channel which will be used for grouping the series by the implicit [stack transform](/transforms/stack): +You can create stacked bar charts by defining a fill channel which will be used for grouping the series by the implicit [stack transform](/transforms/stack). In the following example we're first grouping the penguins dataset by island to then stack them by species: ```svelte live - - - v ** 2)} - fill="steelblue" /> - - -``` - -```svelte - - v ** 2)} - fill="steelblue" /> - - -``` - [fork](https://svelte.dev/playground/8b9fb6c1946d4579a3dc9da32f6c983c?version=5) -For stacked bar charts, provide a `fill` channel that will be used for grouping the series: - -```svelte - - - -``` - ## Insets You can create bullet bars using the `inset` option and two `BarX` layers: From c6d785cf6900f4e0e7a585e99cc6b499aaecfa65 Mon Sep 17 00:00:00 2001 From: gka Date: Tue, 27 May 2025 21:51:31 +0200 Subject: [PATCH 4/4] add tests --- src/routes/marks/bar/+page.md | 25 ++++++++++++++ src/tests/barX.test.ts | 61 +++++++++++++++++++++++++++++++++ src/tests/barY.test.ts | 63 +++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+) diff --git a/src/routes/marks/bar/+page.md b/src/routes/marks/bar/+page.md index b23c53db..ab85e244 100644 --- a/src/routes/marks/bar/+page.md +++ b/src/routes/marks/bar/+page.md @@ -151,6 +151,31 @@ The `BarY` component renders vertical bars (columns), typically used with a band Additionally, `BarY` supports all common styling properties like `fill`, `stroke`, `opacity`, etc. +```svelte live + + + + + + +``` + +```svelte + + + + +``` + [fork](https://svelte.dev/playground/8b9fb6c1946d4579a3dc9da32f6c983c?version=5) ## Insets diff --git a/src/tests/barX.test.ts b/src/tests/barX.test.ts index 5b186d83..19e74f70 100644 --- a/src/tests/barX.test.ts +++ b/src/tests/barX.test.ts @@ -84,6 +84,67 @@ describe('BarX mark', () => { // // check that bar length match data expect(barDims.map((d) => d.w)).toStrictEqual([1, 2, 3, 4, 5].map((m) => barDims[0].w * m)); }); + + const timeseries = [ + { year: 2019, value: 1 }, + { year: 2020, value: 2 }, + { year: 2021, value: 3 }, + { year: 2022, value: 4 }, + { year: 2024, value: 5 } + ]; + + it('skips missing years in band scale domain', () => { + const { container } = render(BarXTest, { + props: { + plotArgs: { + height: 200, + axes: true + }, + barArgs: { + data: timeseries, + x: 'value', + y: 'year' + } + } + }); + + const bars = container.querySelectorAll('g.bar-x > rect') as NodeListOf; + expect(bars.length).toBe(5); + + const yAxisLabels = container.querySelectorAll( + 'g.axis-y .tick text' + ) as NodeListOf; + expect(yAxisLabels.length).toBe(5); + const labels = Array.from(yAxisLabels).map((d) => d.textContent); + expect(labels.sort()).toStrictEqual(['2019', '2020', '2021', '2022', '2024']); + }); + + it('includes missing years in band scale domain if interval is set', () => { + const { container } = render(BarXTest, { + props: { + plotArgs: { + height: 200, + axes: true, + y: { interval: 1 } + }, + barArgs: { + data: timeseries, + x: 'value', + y: 'year' + } + } + }); + + const bars = container.querySelectorAll('g.bar-x > rect') as NodeListOf; + expect(bars.length).toBe(5); + + const yAxisLabels = container.querySelectorAll( + 'g.axis-y .tick text' + ) as NodeListOf; + expect(yAxisLabels.length).toBe(6); + const labels = Array.from(yAxisLabels).map((d) => d.textContent); + expect(labels.sort()).toEqual(['2019', '2020', '2021', '2022', '2023', '2024']); + }); }); function getRectDims(rect: SVGRectElement) { diff --git a/src/tests/barY.test.ts b/src/tests/barY.test.ts index 118b2725..9a10b9fc 100644 --- a/src/tests/barY.test.ts +++ b/src/tests/barY.test.ts @@ -110,6 +110,69 @@ describe('BarY mark', () => { expect(barDims[3].h).toBe(barDims[0].h * 4); expect(barDims[4].h).toBe(barDims[0].h * 5); }); + + const timeseries = [ + { year: 2019, value: 1 }, + { year: 2020, value: 2 }, + { year: 2021, value: 3 }, + { year: 2022, value: 4 }, + { year: 2024, value: 5 } + ]; + + it('skips missing years in band scale domain', () => { + const { container } = render(BarYTest, { + props: { + plotArgs: { + width: 400, + height: 400, + axes: true + }, + barArgs: { + data: timeseries, + y: 'value', + x: 'year' + } + } + }); + + const bars = container.querySelectorAll('g.bar-y > rect') as NodeListOf; + expect(bars.length).toBe(5); + + const xAxisLabels = container.querySelectorAll( + 'g.axis-x .tick text' + ) as NodeListOf; + expect(xAxisLabels.length).toBe(5); + const labels = Array.from(xAxisLabels).map((d) => d.textContent); + expect(labels.sort()).toStrictEqual(['2019', '2020', '2021', '2022', '2024']); + }); + + it('includes missing years in band scale domain if interval is set', () => { + const { container } = render(BarYTest, { + props: { + plotArgs: { + width: 500, + height: 400, + axes: true, + x: { interval: 1 } + }, + barArgs: { + data: timeseries, + y: 'value', + x: 'year' + } + } + }); + + const bars = container.querySelectorAll('g.bar-y > rect') as NodeListOf; + expect(bars.length).toBe(5); + + const xAxisLabels = container.querySelectorAll( + 'g.axis-x .tick text' + ) as NodeListOf; + expect(xAxisLabels.length).toBe(6); + const labels = Array.from(xAxisLabels).map((d) => d.textContent); + expect(labels.sort()).toEqual(['2019', '2020', '2021', '2022', '2023', '2024']); + }); }); function getRectDims(rect: SVGRectElement) { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy