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}
-
+
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..87d3a411 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,18 @@ export function createScale(
: valueArray
: extent(scaleOptions.zero ? [0, ...valueArray] : valueArray);
+ if (scaleOptions.interval) {
+ if (isOrdinal) {
+ domain = domainFromInterval(domain, scaleOptions.interval, name);
+ } else {
+ if (markTypes.size > 0) {
+ console.warn(
+ '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 +363,13 @@ export function createScale(
};
}
+function domainFromInterval(domain: RawValue[], interval: string | number, name: ScaleName) {
+ const interval_ = maybeInterval(interval);
+ const [lo, hi] = extent(domain);
+ const out = interval_.range(lo, interval_.offset(hi));
+ return name === 'y' ? out.toReversed() : out;
+}
+
/**
* 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
diff --git a/src/routes/marks/bar/+page.md b/src/routes/marks/bar/+page.md
index 5b9f2333..ab85e244 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:
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) {