@fundar/data-chart-telling 1.7.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/layout/base/BasePlotLayout.svelte +20 -6
- package/dist/layout/scales/MultilineYTicks.svelte +101 -0
- package/dist/layout/scales/MultilineYTicks.svelte.d.ts +11 -0
- package/dist/layout/scales/ScalesLayout.svelte +24 -0
- package/dist/layout/scales/TickLines.d.ts +19 -0
- package/dist/layout/scales/TickLines.js +58 -0
- package/dist/layout/styles/ValueLabels.js +1 -1
- package/dist/plots/correlation/scatter/markers.js +1 -1
- package/dist/plots/distribution/histogram/markers.js +1 -1
- package/dist/plots/distribution/pyramid/markers.js +1 -1
- package/dist/plots/ranking/bar/markers.js +1 -1
- package/dist/types/layout/scales.d.ts +1 -0
- package/docs/scales.md +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [1.8.1] - 2026-11-02
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `styles.values.format` gets the labelled row as its third argument on `BarPlot`, `PyramidPlot`, `HistogramPlot`, `ScatterPlot` and the end-of-line labels of `LinePlot` and `AreaPlot`, as its documentation says; only `ClevelandPlot` did. A format that returns no text for some rows - the only way to label some pieces of a stacked bar and not others - had no row to decide by.
|
|
10
|
+
|
|
11
|
+
## [1.8.0] - 2026-11-02
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `scales.x.lines` and `scales.y.lines` write each label of a category axis on up to that many lines, split at its spaces so the lines are as even as they can be, and keep the `x` labels upright instead of turning them. A `tickFormat` is applied first.
|
|
16
|
+
- A band `y` axis whose `scales.y.tickFormat` gives a label on several lines (a list of strings, or a text with line breaks) writes it on those lines, and its left margin grows to the widest one. svelteplot's own axis joined the lines with spaces, so only the `x` axis could wrap its labels.
|
|
17
|
+
|
|
5
18
|
## [1.7.0] - 2026-11-02
|
|
6
19
|
|
|
7
20
|
### Added
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { getConfiguration } from '../../configuration/config.svelte';
|
|
15
15
|
import { SveltePlotScale } from '../scales/SveltePlotScale';
|
|
16
16
|
import { TickLabels } from '../scales/TickLabels';
|
|
17
|
+
import { TickLines } from '../scales/TickLines';
|
|
17
18
|
import ScalesLayout from '../scales/ScalesLayout.svelte';
|
|
18
19
|
import GeometryLayout from '../geometry/GeometryLayout.svelte';
|
|
19
20
|
import CoordinatesLayout from '../coordinates/CoordinatesLayout.svelte';
|
|
@@ -160,7 +161,12 @@
|
|
|
160
161
|
x?: ReturnType<typeof TickLabels.layout>;
|
|
161
162
|
y?: { every: number; values: string[] };
|
|
162
163
|
} = {};
|
|
163
|
-
|
|
164
|
+
// Labels split in lines are meant to stay upright, so they are not judged by how long one line would be.
|
|
165
|
+
if (
|
|
166
|
+
bands.includes('x') &&
|
|
167
|
+
scales?.x?.tickRotate === undefined &&
|
|
168
|
+
!((scales?.x?.lines ?? 1) > 1)
|
|
169
|
+
) {
|
|
164
170
|
out.x = TickLabels.layout(
|
|
165
171
|
categories('x'),
|
|
166
172
|
width - estimate.left - estimate.right,
|
|
@@ -180,11 +186,19 @@
|
|
|
180
186
|
}
|
|
181
187
|
return out;
|
|
182
188
|
});
|
|
183
|
-
/**
|
|
189
|
+
/** An axis' scale with its labels split in the `lines` it asks for, as a `tickFormat` that gives them. */
|
|
190
|
+
function withLines<TScale extends AxisScale<TData> | undefined>(scale: TScale): TScale {
|
|
191
|
+
if (!scale?.lines || scale.lines < 2) return scale;
|
|
192
|
+
const { lines, tickFormat, ...rest } = scale;
|
|
193
|
+
const format = typeof tickFormat === 'function' ? tickFormat : undefined;
|
|
194
|
+
return { ...rest, tickFormat: TickLines.wrapped(format, lines) } as TScale;
|
|
195
|
+
}
|
|
196
|
+
/** `scales` with the rotation the tick layout chose for the x labels, and the labels split in lines. */
|
|
184
197
|
const layoutScales = $derived.by(() => {
|
|
198
|
+
if (scales === undefined) return scales;
|
|
185
199
|
const rotate = tickLayout?.x?.rotate;
|
|
186
|
-
|
|
187
|
-
return { ...scales, x: { ...scales.
|
|
200
|
+
const x = withLines(rotate ? { ...scales.x, tickRotate: rotate } : scales.x);
|
|
201
|
+
return { ...scales, ...(x ? { x } : {}), ...(scales.y ? { y: withLines(scales.y) } : {}) };
|
|
188
202
|
});
|
|
189
203
|
/** The category labels a crowded axis still writes: one in every few. */
|
|
190
204
|
const tickValues = $derived.by(() => {
|
|
@@ -252,9 +266,9 @@
|
|
|
252
266
|
return (label ? { ...out, label } : out) as SveltePlotProps['x'];
|
|
253
267
|
});
|
|
254
268
|
const resolvedY = $derived.by(() => {
|
|
255
|
-
const built = SveltePlotScale.build(
|
|
269
|
+
const built = SveltePlotScale.build(layoutScales?.y, data, getY);
|
|
256
270
|
const domain = edgeDomains.get('y');
|
|
257
|
-
const label = titleOfEmpty('y',
|
|
271
|
+
const label = titleOfEmpty('y', layoutScales?.y);
|
|
258
272
|
const out = domain ? { ...built, domain, nice: false } : built;
|
|
259
273
|
return (label ? { ...out, label } : out) as SveltePlotProps['y'];
|
|
260
274
|
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext, untrack } from 'svelte';
|
|
3
|
+
import type { Writable } from 'svelte/store';
|
|
4
|
+
import { usePlot } from '../plot/usePlot';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The labels of a band y axis, each on the lines it was given. svelteplot's own axis joins the lines of a
|
|
8
|
+
* label with spaces, so an axis whose `tickFormat` gives several lines hides its labels and has them
|
|
9
|
+
* drawn here. The left margin grows to the widest line, as the axis does for its own labels.
|
|
10
|
+
*/
|
|
11
|
+
let {
|
|
12
|
+
ticks,
|
|
13
|
+
lines,
|
|
14
|
+
tickSize,
|
|
15
|
+
tickPadding,
|
|
16
|
+
fontSize
|
|
17
|
+
}: {
|
|
18
|
+
ticks: unknown[];
|
|
19
|
+
/** The lines of each tick, in the order of `ticks`. */
|
|
20
|
+
lines: string[][];
|
|
21
|
+
tickSize: number;
|
|
22
|
+
tickPadding: number;
|
|
23
|
+
fontSize: number;
|
|
24
|
+
} = $props();
|
|
25
|
+
|
|
26
|
+
const LINE_HEIGHT = 1.15;
|
|
27
|
+
|
|
28
|
+
const plot = usePlot().svelteplot;
|
|
29
|
+
const marginLeft = $derived(Number(plot.options.marginLeft) || 0);
|
|
30
|
+
|
|
31
|
+
const placed = $derived(
|
|
32
|
+
ticks.map((tick, i) => {
|
|
33
|
+
const scale = plot.scales.y.fn;
|
|
34
|
+
return {
|
|
35
|
+
lines: lines[i] ?? [],
|
|
36
|
+
y: Number(scale(tick as never)) + (scale.bandwidth?.() ?? 0) / 2
|
|
37
|
+
};
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
let labels: (SVGTextElement | undefined)[] = $state([]);
|
|
42
|
+
|
|
43
|
+
const { autoMarginLeft } = getContext<{ autoMarginLeft: Writable<Map<string, number>> }>(
|
|
44
|
+
'svelteplot/autoMargins'
|
|
45
|
+
);
|
|
46
|
+
const marginId = `dct-multiline-y-ticks-${Math.random().toString(36).slice(2)}`;
|
|
47
|
+
|
|
48
|
+
$effect(() => {
|
|
49
|
+
// Read so new labels or a new size measure again.
|
|
50
|
+
void [lines, fontSize, tickSize, tickPadding];
|
|
51
|
+
let frame: number | undefined;
|
|
52
|
+
let cancelled = false;
|
|
53
|
+
const measure = () => {
|
|
54
|
+
if (cancelled) return;
|
|
55
|
+
const widest = Math.max(
|
|
56
|
+
0,
|
|
57
|
+
...labels.map((label) => label?.getBoundingClientRect().width ?? 0)
|
|
58
|
+
);
|
|
59
|
+
const wanted = Math.ceil(widest) + Math.max(0, tickSize + tickPadding);
|
|
60
|
+
untrack(() => {
|
|
61
|
+
if ($autoMarginLeft.get(marginId) !== wanted) $autoMarginLeft.set(marginId, wanted);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
// Text measures right only once its fonts load.
|
|
65
|
+
(document.fonts?.ready ?? Promise.resolve()).then(() => {
|
|
66
|
+
if (!cancelled) frame = requestAnimationFrame(measure);
|
|
67
|
+
});
|
|
68
|
+
return () => {
|
|
69
|
+
cancelled = true;
|
|
70
|
+
if (frame !== undefined) cancelAnimationFrame(frame);
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
$effect(() => () => {
|
|
74
|
+
$autoMarginLeft.delete(marginId);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const x = $derived(marginLeft - tickSize - tickPadding);
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<g class="dct-multiline-y-ticks">
|
|
81
|
+
{#each placed as tick, i (i)}
|
|
82
|
+
<text
|
|
83
|
+
bind:this={labels[i]}
|
|
84
|
+
{x}
|
|
85
|
+
y={tick.y}
|
|
86
|
+
text-anchor="end"
|
|
87
|
+
font-size={fontSize}
|
|
88
|
+
fill="currentColor"
|
|
89
|
+
>
|
|
90
|
+
{#each tick.lines as line, l (l)}
|
|
91
|
+
<!-- The block of lines is centred on the tick: the first sits half the others above it. -->
|
|
92
|
+
<tspan
|
|
93
|
+
{x}
|
|
94
|
+
dy={l === 0
|
|
95
|
+
? `${0.32 - ((tick.lines.length - 1) / 2) * LINE_HEIGHT}em`
|
|
96
|
+
: `${LINE_HEIGHT}em`}>{line}</tspan
|
|
97
|
+
>
|
|
98
|
+
{/each}
|
|
99
|
+
</text>
|
|
100
|
+
{/each}
|
|
101
|
+
</g>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
ticks: unknown[];
|
|
3
|
+
/** The lines of each tick, in the order of `ticks`. */
|
|
4
|
+
lines: string[][];
|
|
5
|
+
tickSize: number;
|
|
6
|
+
tickPadding: number;
|
|
7
|
+
fontSize: number;
|
|
8
|
+
};
|
|
9
|
+
declare const MultilineYTicks: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
|
+
type MultilineYTicks = ReturnType<typeof MultilineYTicks>;
|
|
11
|
+
export default MultilineYTicks;
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import { getConfiguration } from '../../configuration/config.svelte';
|
|
7
7
|
import { NiceTicks } from './NiceTicks';
|
|
8
8
|
import { EdgeTicks } from './EdgeTicks';
|
|
9
|
+
import { TickLines } from './TickLines';
|
|
10
|
+
import MultilineYTicks from './MultilineYTicks.svelte';
|
|
9
11
|
import { DISCRETE_SCALE_TYPES } from '../../types/layout/scales';
|
|
10
12
|
import type { AxisBasedScalesConfig } from '../../types/layout/scales';
|
|
11
13
|
import type { AxisValue } from '../../types/layout/mapping';
|
|
@@ -200,6 +202,18 @@
|
|
|
200
202
|
unknown
|
|
201
203
|
>;
|
|
202
204
|
|
|
205
|
+
/**
|
|
206
|
+
* The labels of a band y axis whose `tickFormat` writes some on several lines. svelteplot's axis joins the
|
|
207
|
+
* lines of a label with spaces, so such an axis hides its labels and `MultilineYTicks` draws them.
|
|
208
|
+
*/
|
|
209
|
+
const multilineY = $derived.by(() => {
|
|
210
|
+
const format = scales?.y?.tickFormat;
|
|
211
|
+
if (typeof format !== 'function' || plot.scales.y.type !== 'band') return undefined;
|
|
212
|
+
const ticks: unknown[] = tickValues?.y ?? plot.scales.y.fn?.domain?.() ?? [];
|
|
213
|
+
const lines = TickLines.ofTicks(ticks, format);
|
|
214
|
+
return TickLines.isMultiline(lines) ? { ticks, lines } : undefined;
|
|
215
|
+
});
|
|
216
|
+
|
|
203
217
|
const yTitleFontSize = $derived(scales?.y?.titleFontSize ?? cfg.axis.titleFontSize);
|
|
204
218
|
|
|
205
219
|
/**
|
|
@@ -327,6 +341,7 @@
|
|
|
327
341
|
{#snippet axisY()}
|
|
328
342
|
<AxisY
|
|
329
343
|
ticks={(yEdgeTicks?.ticks ?? tickValues?.y) as string[] | undefined}
|
|
344
|
+
tickFormat={multilineY ? () => '' : undefined}
|
|
330
345
|
tickCount={yTickCount}
|
|
331
346
|
interval={yTickInterval}
|
|
332
347
|
tickSize={scales?.y?.tickSize ?? cfg.axis.yTickSize}
|
|
@@ -339,6 +354,15 @@
|
|
|
339
354
|
<g style:visibility="hidden">{@render axisY()}</g>
|
|
340
355
|
{:else if scales?.y?.axisDisplay !== 'none'}
|
|
341
356
|
{@render axisY()}
|
|
357
|
+
{#if multilineY}
|
|
358
|
+
<MultilineYTicks
|
|
359
|
+
ticks={multilineY.ticks}
|
|
360
|
+
lines={multilineY.lines}
|
|
361
|
+
tickSize={scales?.y?.tickSize ?? cfg.axis.yTickSize}
|
|
362
|
+
tickPadding={scales?.y?.tickPadding ?? cfg.axis.yTickPadding}
|
|
363
|
+
fontSize={cfg.axis.fontSize}
|
|
364
|
+
/>
|
|
365
|
+
{/if}
|
|
342
366
|
{/if}
|
|
343
367
|
{#if verticalYLabel && (scales?.y?.axisDisplay ?? 'visible') === 'visible'}
|
|
344
368
|
<text
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** What a `tickFormat` returns for one tick, as the lines it is written on. */
|
|
2
|
+
export declare class TickLines {
|
|
3
|
+
/** The lines of `label`: a list as it is, a text split at its line breaks. */
|
|
4
|
+
static of(label: string | string[]): string[];
|
|
5
|
+
/** The lines of every tick of `ticks` under `format`. */
|
|
6
|
+
static ofTicks(ticks: unknown[], format: (d: unknown, index: number, ticks: unknown[]) => string | string[]): string[][];
|
|
7
|
+
/**
|
|
8
|
+
* `label` on up to `lines` lines, split at its spaces so that the longest line is as short as it can be
|
|
9
|
+
* (ties go to the earlier break). A label of one word, or `lines` below 2, stays on one line.
|
|
10
|
+
*/
|
|
11
|
+
static split(label: string, lines: number): string[];
|
|
12
|
+
/**
|
|
13
|
+
* A `tickFormat` that gives the lines of its tick: `format`'s text (or the tick as it is, with none) split in
|
|
14
|
+
* `lines`, and a list `format` gives as it is.
|
|
15
|
+
*/
|
|
16
|
+
static wrapped(format: ((d: unknown, index: number, ticks: unknown[]) => string | string[]) | undefined, lines: number): (d: unknown, index: number, ticks: unknown[]) => string[];
|
|
17
|
+
/** Whether some tick needs more than one line. */
|
|
18
|
+
static isMultiline(lines: string[][]): boolean;
|
|
19
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** What a `tickFormat` returns for one tick, as the lines it is written on. */
|
|
2
|
+
export class TickLines {
|
|
3
|
+
/** The lines of `label`: a list as it is, a text split at its line breaks. */
|
|
4
|
+
static of(label) {
|
|
5
|
+
return Array.isArray(label) ? label : String(label).split('\n');
|
|
6
|
+
}
|
|
7
|
+
/** The lines of every tick of `ticks` under `format`. */
|
|
8
|
+
static ofTicks(ticks, format) {
|
|
9
|
+
return ticks.map((tick, i) => TickLines.of(format(tick, i, ticks)));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `label` on up to `lines` lines, split at its spaces so that the longest line is as short as it can be
|
|
13
|
+
* (ties go to the earlier break). A label of one word, or `lines` below 2, stays on one line.
|
|
14
|
+
*/
|
|
15
|
+
static split(label, lines) {
|
|
16
|
+
const words = label.split(' ').filter((word) => word !== '');
|
|
17
|
+
const count = Math.min(Math.floor(lines), words.length);
|
|
18
|
+
if (count < 2)
|
|
19
|
+
return [label];
|
|
20
|
+
const width = (from, to) => words.slice(from, to).reduce((sum, word) => sum + word.length, 0) + (to - from - 1);
|
|
21
|
+
// best[k][i]: the shortest the longest line can be when the first i words take k lines, and where the last begins.
|
|
22
|
+
const best = Array.from({ length: count + 1 }, () => []);
|
|
23
|
+
for (let i = 1; i <= words.length; i++)
|
|
24
|
+
best[1][i] = { cost: width(0, i), from: 0 };
|
|
25
|
+
for (let k = 2; k <= count; k++) {
|
|
26
|
+
for (let i = k; i <= words.length; i++) {
|
|
27
|
+
let choice = { cost: Infinity, from: k - 1 };
|
|
28
|
+
for (let from = k - 1; from < i; from++) {
|
|
29
|
+
const cost = Math.max(best[k - 1][from].cost, width(from, i));
|
|
30
|
+
if (cost < choice.cost)
|
|
31
|
+
choice = { cost, from };
|
|
32
|
+
}
|
|
33
|
+
best[k][i] = choice;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const out = [];
|
|
37
|
+
for (let k = count, end = words.length; k >= 1; k--) {
|
|
38
|
+
const { from } = best[k][end];
|
|
39
|
+
out.unshift(words.slice(from, end).join(' '));
|
|
40
|
+
end = from;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A `tickFormat` that gives the lines of its tick: `format`'s text (or the tick as it is, with none) split in
|
|
46
|
+
* `lines`, and a list `format` gives as it is.
|
|
47
|
+
*/
|
|
48
|
+
static wrapped(format, lines) {
|
|
49
|
+
return (d, index, ticks) => {
|
|
50
|
+
const text = format ? format(d, index, ticks) : String(d);
|
|
51
|
+
return Array.isArray(text) ? text : TickLines.split(text, lines);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Whether some tick needs more than one line. */
|
|
55
|
+
static isMultiline(lines) {
|
|
56
|
+
return lines.some((tick) => tick.length > 1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -206,7 +206,7 @@ export class ValueLabels {
|
|
|
206
206
|
x: (processed.channels.x ?? position.x),
|
|
207
207
|
y: (processed.channels.y2 ?? processed.channels.y ?? topOf),
|
|
208
208
|
grouping: SERIES_ID_KEY,
|
|
209
|
-
text: (d) => format(Number(valueOf(d)), nameOf(d)),
|
|
209
|
+
text: (d) => format(Number(valueOf(d)), nameOf(d), d),
|
|
210
210
|
styles: {
|
|
211
211
|
...font,
|
|
212
212
|
...(this.fill ? { fill: this.fill } : {}),
|
|
@@ -21,7 +21,7 @@ export function buildValueLabels(labels, state, ctx) {
|
|
|
21
21
|
data: processedData.data,
|
|
22
22
|
x: position.x,
|
|
23
23
|
y: position.y,
|
|
24
|
-
text: (d) => format(Number(valueOf(d)), nameOf(d)),
|
|
24
|
+
text: (d) => format(Number(valueOf(d)), nameOf(d), d),
|
|
25
25
|
styles: {
|
|
26
26
|
...font,
|
|
27
27
|
fill: font?.fill ?? ctx.cfg.axis.color,
|
|
@@ -41,7 +41,7 @@ export function buildValueLabels(labels, state, ctx) {
|
|
|
41
41
|
[binned]: position[binned],
|
|
42
42
|
[value]: along,
|
|
43
43
|
transforms: [{ type: 'filter', filter: (d) => countOf(d) > 0 }],
|
|
44
|
-
text: (d) => format(countOf(d), nameOf(d)),
|
|
44
|
+
text: (d) => format(countOf(d), nameOf(d), d),
|
|
45
45
|
styles: {
|
|
46
46
|
...font,
|
|
47
47
|
fill: font?.fill ?? ctx.cfg.axis.color,
|
|
@@ -57,7 +57,7 @@ export function buildValueLabels(labels, state, ctx) {
|
|
|
57
57
|
data,
|
|
58
58
|
[valueAxis]: at,
|
|
59
59
|
[categoryAxis]: series[categoryAxis],
|
|
60
|
-
text: (d) => format(magnitude(d), series.name),
|
|
60
|
+
text: (d) => format(magnitude(d), series.name, d),
|
|
61
61
|
styles: {
|
|
62
62
|
...font,
|
|
63
63
|
fill: font?.fill ?? ctx.cfg.axis.color,
|
|
@@ -124,7 +124,7 @@ export function buildValueLabels(labels, state, ctx) {
|
|
|
124
124
|
? { x: position, y: geometry.categoryOf }
|
|
125
125
|
: { x: geometry.categoryOf, y: position }),
|
|
126
126
|
bandSlice: geometry.sliceOf(s.name),
|
|
127
|
-
text: (d) => format(Number(valueOf(d)), s.name),
|
|
127
|
+
text: (d) => format(Number(valueOf(d)), s.name, d),
|
|
128
128
|
styles: {
|
|
129
129
|
...font,
|
|
130
130
|
fill: font?.fill ?? ctx.cfg.axis.color,
|
|
@@ -169,6 +169,7 @@ export type AxisScale<TData = Record<string, unknown>> = {
|
|
|
169
169
|
tickSize?: number;
|
|
170
170
|
tickPadding?: number;
|
|
171
171
|
tickRotate?: number;
|
|
172
|
+
lines?: number;
|
|
172
173
|
tickFormat?: false | Intl.NumberFormatOptions | Intl.DateTimeFormatOptions | ((d: unknown, index: number, ticks: unknown[]) => string | string[]);
|
|
173
174
|
};
|
|
174
175
|
/** Each {@link AxisChannelName}'s own scale-config shape - both use {@link AxisScale}. */
|
package/docs/scales.md
CHANGED
|
@@ -50,7 +50,8 @@ Each axis takes an `AxisScale`.
|
|
|
50
50
|
| `edgeTicks` | Ticks at the data's first and last values beside the nice ones. `false` turns them off, `true` on, `{ collision: 'drop-nice' \| 'drop-edge' \| 'extend' }` picks what happens when one crowds a nice tick. Defaults to `axis.edgeTicks.x` / `.y` in the [configuration](theming.md#edge-ticks) (on for x, off for y); ignored when the axis sets `ticks`, `tickSpacing`, `interval` or `domain` |
|
|
51
51
|
| `label`, `labelAnchor`, `labelOrientation`, `labelPosition`, `titleFontSize` | The axis title (`label: false` hides it) |
|
|
52
52
|
| `ticks`, `tickSpacing`, `tickSize`, `tickPadding`, `tickRotate` | Tick placement and size |
|
|
53
|
-
| `tickFormat` | `false`, an `Intl.NumberFormatOptions` / `Intl.DateTimeFormatOptions` object, or `(d, index, ticks) => string`
|
|
53
|
+
| `tickFormat` | `false`, an `Intl.NumberFormatOptions` / `Intl.DateTimeFormatOptions` object, or `(d, index, ticks) => string \| string[]` |
|
|
54
|
+
| `lines` | Labels of a category axis on up to this many lines, split at their spaces |
|
|
54
55
|
| `grid` | `true` or a stroke style for gridlines |
|
|
55
56
|
| `axisDisplay` | `'visible'` (default), `'hidden'` (keeps the space, so aligned panels stay aligned) or `'none'` |
|
|
56
57
|
| `padding`, `paddingInner`, `paddingOuter`, `align` | Band and point spacing |
|
|
@@ -58,6 +59,8 @@ Each axis takes an `AxisScale`.
|
|
|
58
59
|
| `interval`, `base`, `constant` | Time/discrete intervals, log base, symlog constant |
|
|
59
60
|
| `sort` | Category order: a comparator of two rows, or by what the categories hold (below) |
|
|
60
61
|
|
|
62
|
+
A category axis writes its labels on several lines with `lines`: `lines: 2` splits each label at its spaces, as evenly as it can, in up to two lines (a `tickFormat` is applied first), and the `x` labels stay upright. To split them where you choose, `tickFormat` returns a list of strings, one for each line, or a text with line breaks. A band `y` axis grows its left margin to the widest line.
|
|
63
|
+
|
|
61
64
|
A category axis whose labels do not fit side by side turns them by itself: to 45 degrees, else to 90, and when even that is not enough it writes only every few labels (and a vertical category axis, such as the one of horizontal bars, does the same). The margin grows to fit the turned labels. An axis with a `tickRotate` of its own - 0 included, which keeps the labels flat - is left as the author set it, and `axis.autoRotate: false` in the [configuration](theming.md) turns this off for every plot.
|
|
62
65
|
|
|
63
66
|
A category axis can be ordered by what its categories hold, without writing a comparator: `sort: { by: 'value', series?, order? }` orders them by their value in one `series` (or, without it, by the sum of their values) and `sort: { by: 'range', order? }` by the spread between their lowest and their highest value; `order` is `'ascending'` (default) or `'descending'`. `grouped: true` orders within each series instead of across all of them: the series stay together, in the order they first appear. On a plot with intervals (`lower`/`upper`) the range counts the interval's ends. A category with nothing to order by goes last. An explicit `domain` wins. With [facets](facets.md) the order is read off every panel's rows, so the panels share it.
|