@fundar/data-chart-telling 1.0.0 → 1.1.0
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 +21 -0
- package/README.md +57 -18
- package/dist/configuration/config.svelte.js +13 -1
- package/dist/configuration/themes/index.d.ts +124 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/layout/base/BasePlotLayout.svelte +18 -5
- package/dist/layout/hover/HoverSettings.d.ts +20 -6
- package/dist/layout/hover/HoverSettings.js +29 -4
- package/dist/layout/legend/LegendState.svelte.d.ts +5 -0
- package/dist/layout/legend/LegendState.svelte.js +4 -0
- package/dist/layout/plot/Plot.svelte +6 -3
- package/dist/layout/plot/PublicPlotContext.d.ts +5 -4
- package/dist/layout/plot/PublicPlotContext.js +7 -3
- package/dist/layout/plot/Slice.js +4 -2
- package/dist/layout/scales/EdgeTicks.d.ts +39 -0
- package/dist/layout/scales/EdgeTicks.js +82 -0
- package/dist/layout/scales/NiceTicks.d.ts +40 -0
- package/dist/layout/scales/NiceTicks.js +72 -0
- package/dist/layout/scales/ScalesLayout.svelte +64 -30
- package/dist/layout/scales/ScalesLayout.svelte.d.ts +5 -0
- package/dist/layout/scales/SveltePlotScale.d.ts +15 -0
- package/dist/layout/scales/SveltePlotScale.js +79 -0
- package/dist/layout/styles/ValueLabels.js +3 -2
- package/dist/layout/timeline/Timeline.svelte +5 -2
- package/dist/layout/timeline/Timeline.svelte.d.ts +2 -0
- package/dist/layout/timeline/TimelineLayout.svelte +38 -12
- package/dist/layout/timeline/TimelineLayout.svelte.d.ts +7 -5
- package/dist/layout/timeline/TimelineRule.svelte +1 -0
- package/dist/layout/timeline/TimelineState.svelte.d.ts +13 -5
- package/dist/layout/timeline/TimelineState.svelte.js +42 -8
- package/dist/layout/timeline/TimelineTime.d.ts +18 -0
- package/dist/layout/timeline/TimelineTime.js +117 -0
- package/dist/layout/tooltip/Tooltip.svelte +6 -3
- package/dist/layout/tooltip/Tooltip.svelte.d.ts +2 -0
- package/dist/layout/tooltip/TooltipState.svelte.d.ts +8 -2
- package/dist/layout/tooltip/TooltipState.svelte.js +22 -3
- package/dist/markers/delta/DeltaMarker.svelte +9 -7
- package/dist/phases/Pipeline.js +3 -2
- package/dist/phases/data/DataPhase.d.ts +5 -11
- package/dist/phases/data/DataPhase.js +11 -8
- package/dist/phases/facet/FacetPanels.d.ts +54 -0
- package/dist/phases/facet/FacetPanels.js +95 -0
- package/dist/phases/facet/FacetPhase.d.ts +1 -42
- package/dist/phases/facet/FacetPhase.js +1 -87
- package/dist/phases/hover/HoverPhase.d.ts +6 -43
- package/dist/phases/hover/HoverPhase.js +19 -54
- package/dist/phases/mapping/MappingPhase.d.ts +1 -1
- package/dist/phases/mapping/MappingPhase.js +2 -4
- package/dist/phases/scales/ScalesPhase.d.ts +1 -3
- package/dist/phases/scales/ScalesPhase.js +17 -21
- package/dist/phases/sharedScales/SharedScalesPhase.d.ts +4 -1
- package/dist/phases/sharedScales/SharedScalesPhase.js +10 -7
- package/dist/phases/statistics/StatisticsPhase.js +3 -4
- package/dist/phases/tooltip/TooltipPhase.d.ts +4 -2
- package/dist/phases/tooltip/TooltipPhase.js +10 -3
- package/dist/phases/transforms/ProcessedPositions.d.ts +30 -0
- package/dist/phases/transforms/ProcessedPositions.js +74 -0
- package/dist/phases/transforms/TransformsPhase.d.ts +5 -42
- package/dist/phases/transforms/TransformsPhase.js +20 -57
- package/dist/plots/area/markers.js +1 -2
- package/dist/plots/bar/defaults.js +6 -10
- package/dist/plots/bar/markers.js +5 -4
- package/dist/plots/box/BoxSummaries.d.ts +19 -0
- package/dist/plots/box/BoxSummaries.js +101 -0
- package/dist/plots/box/Plot.svelte +42 -0
- package/dist/plots/box/Plot.svelte.d.ts +26 -0
- package/dist/plots/box/defaults.d.ts +12 -0
- package/dist/plots/box/defaults.js +34 -0
- package/dist/plots/box/markers.d.ts +12 -0
- package/dist/plots/box/markers.js +34 -0
- package/dist/plots/geo/phases/hover.js +5 -5
- package/dist/plots/geo/phases/markers.js +3 -4
- package/dist/plots/geo/slices.js +6 -3
- package/dist/plots/heatmap/markers.js +2 -1
- package/dist/plots/histogram/defaults.js +1 -1
- package/dist/plots/histogram/markers.js +4 -4
- package/dist/plots/line/markers.js +1 -2
- package/dist/plots/pyramid/defaults.js +3 -2
- package/dist/plots/pyramid/markers.js +3 -6
- package/dist/plots/scatter/defaults.js +3 -2
- package/dist/plots/scatter/markers.js +4 -3
- package/dist/types/configuration/styling.d.ts +24 -2
- package/dist/types/layout/composer.d.ts +5 -2
- package/dist/types/layout/scales.d.ts +16 -0
- package/dist/types/layout/timeline.d.ts +14 -5
- package/dist/types/phases/hover/phase.d.ts +6 -0
- package/dist/types/phases/state.d.ts +5 -2
- package/dist/types/phases/statistics/phase.d.ts +3 -1
- package/dist/types/phases/transforms/phase.d.ts +10 -0
- package/dist/types/plots/box/props.d.ts +10 -0
- package/dist/types/plots/box/props.js +1 -0
- package/dist/types/plots/box/state.d.ts +23 -0
- package/dist/types/plots/box/state.js +1 -0
- package/dist/types/plots/props.d.ts +2 -0
- package/docs/chrome.md +39 -8
- package/docs/composer.md +19 -3
- package/docs/concepts.md +1 -1
- package/docs/development.md +14 -5
- package/docs/facets.md +62 -8
- package/docs/getting-started.md +8 -9
- package/docs/legend.md +62 -15
- package/docs/markers.md +17 -6
- package/docs/plots.md +168 -42
- package/docs/scales.md +36 -19
- package/docs/state.md +1 -1
- package/docs/styles-and-segments.md +85 -31
- package/docs/theming.md +30 -19
- package/docs/timeline.md +62 -28
- package/docs/tooltip-and-hover.md +72 -24
- package/docs/transforms-and-statistics.md +47 -17
- package/examples/BarValues.svelte +2 -2
- package/examples/Box.svelte +7 -0
- package/examples/Chrome.svelte +17 -0
- package/examples/ChromeOrder.svelte +18 -0
- package/examples/Composer.svelte +23 -0
- package/examples/CustomTooltip.svelte +21 -14
- package/examples/Facets.svelte +3 -3
- package/examples/FacetsCarousel.svelte +14 -0
- package/examples/FacetsRowsCols.svelte +23 -0
- package/examples/FirstPlot.svelte +7 -0
- package/examples/Gaps.svelte +21 -0
- package/examples/GroupedBars.svelte +15 -0
- package/examples/Heatmap.svelte +7 -0
- package/examples/HeatmapSegments.svelte +26 -0
- package/examples/Histogram.svelte +14 -0
- package/examples/HorizontalBars.svelte +15 -0
- package/examples/LegendSections.svelte +27 -0
- package/examples/LegendToggle.svelte +3 -3
- package/examples/LineByRegion.svelte +18 -0
- package/examples/LineDates.svelte +15 -0
- package/examples/Markers.svelte +27 -0
- package/examples/MovingAverage.svelte +15 -0
- package/examples/Pyramid.svelte +7 -0
- package/examples/Regression.svelte +14 -0
- package/examples/ResponsiveLine.svelte +7 -7
- package/examples/Scales.svelte +26 -0
- package/examples/Scatter.svelte +16 -0
- package/examples/SegmentHighlight.svelte +22 -0
- package/examples/Segments.svelte +11 -3
- package/examples/SegmentsBySeries.svelte +25 -0
- package/examples/StackedArea.svelte +16 -0
- package/examples/StackedBars.svelte +15 -0
- package/examples/TimelinePlayer.svelte +8 -7
- package/examples/TimelineRule.svelte +19 -0
- package/examples/Tooltip.svelte +3 -3
- package/examples/ValueLabels.svelte +21 -0
- package/examples/data.ts +320 -8
- package/llms-full.txt +33 -5
- package/llms.txt +30 -2
- package/package.json +3 -2
- package/dist/layout/scales/buildScale.d.ts +0 -8
- package/dist/layout/scales/buildScale.js +0 -67
- package/dist/layout/scales/niceDomain.d.ts +0 -23
- package/dist/layout/scales/niceDomain.js +0 -59
|
@@ -104,7 +104,7 @@ export class PublicTimeline {
|
|
|
104
104
|
}
|
|
105
105
|
/** Every step it moves across, ascending. */
|
|
106
106
|
get steps() {
|
|
107
|
-
return this.#state.steps;
|
|
107
|
+
return this.#state.steps.map((step) => this.#state.step(step));
|
|
108
108
|
}
|
|
109
109
|
/** The position across the steps, `0`-`1`. */
|
|
110
110
|
get progress() {
|
|
@@ -112,7 +112,8 @@ export class PublicTimeline {
|
|
|
112
112
|
}
|
|
113
113
|
/** The step the position lands on, or `undefined` without steps. */
|
|
114
114
|
get value() {
|
|
115
|
-
|
|
115
|
+
const { value } = this.#state;
|
|
116
|
+
return value === undefined ? undefined : this.#state.step(value);
|
|
116
117
|
}
|
|
117
118
|
get index() {
|
|
118
119
|
return this.#state.index;
|
|
@@ -139,8 +140,11 @@ export class PublicTimeline {
|
|
|
139
140
|
seekIndex(index) {
|
|
140
141
|
this.#state.seekIndex(index);
|
|
141
142
|
}
|
|
143
|
+
/** Moves to the step closest to `value`. */
|
|
142
144
|
seekValue(value) {
|
|
143
|
-
this.#state.
|
|
145
|
+
const time = this.#state.timeOf(value);
|
|
146
|
+
if (time !== undefined)
|
|
147
|
+
this.#state.seekValue(time);
|
|
144
148
|
}
|
|
145
149
|
next() {
|
|
146
150
|
this.#state.next();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveAccessor } from '../../plots/utils/accessors';
|
|
2
2
|
import { merge } from '../../utils/merge';
|
|
3
3
|
import { TimelineSteps } from '../timeline/TimelineSteps';
|
|
4
|
+
import { TimelineTime } from '../timeline/TimelineTime';
|
|
4
5
|
import { buildFacetAxesDisplay } from '../facet/axes';
|
|
5
6
|
import { buildFacetPlaceKey, isFacetWrap } from '../facet/config';
|
|
6
7
|
/**
|
|
@@ -55,10 +56,11 @@ export class Slice {
|
|
|
55
56
|
};
|
|
56
57
|
if (timeline && place.time !== undefined) {
|
|
57
58
|
const get = resolveAccessor(timeline.accessor);
|
|
59
|
+
const reader = new TimelineTime(timeline.dateFormat);
|
|
58
60
|
const time = place.time;
|
|
59
61
|
tests.push((row) => {
|
|
60
|
-
const value = get(row);
|
|
61
|
-
if (value
|
|
62
|
+
const value = reader.toTime(get(row));
|
|
63
|
+
if (value === undefined)
|
|
62
64
|
return true;
|
|
63
65
|
return TimelineSteps.matches(value, time, this.filterMode);
|
|
64
66
|
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AxisChannelName } from '../../types/layout/mapping';
|
|
2
|
+
import type { AxisScale, EdgeTicksOptions } from '../../types/layout/scales';
|
|
3
|
+
/** What {@link EdgeTicks.build} measures an axis with. */
|
|
4
|
+
export type EdgeTicksInput = {
|
|
5
|
+
/** The axis measured: labels sit side by side along x, stacked along y. */
|
|
6
|
+
axis: AxisChannelName;
|
|
7
|
+
/** The axis' numeric values; their extent is the axis' domain. */
|
|
8
|
+
values: number[];
|
|
9
|
+
/** The axis' length in pixels. */
|
|
10
|
+
span: number;
|
|
11
|
+
/** The pixels between ticks the axis aims for, and the fewest ticks it takes. */
|
|
12
|
+
spacing: number;
|
|
13
|
+
min: number;
|
|
14
|
+
/** The tick labels' font size, in pixels. */
|
|
15
|
+
fontSize: number;
|
|
16
|
+
options: EdgeTicksOptions;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The ticks of a continuous axis: its nice ticks plus the data's first and
|
|
20
|
+
* last values, so a reader sees where the data starts and ends. When an edge
|
|
21
|
+
* tick lands too close to the nice tick next to it for both labels to fit,
|
|
22
|
+
* `options.collision` decides which gives way, or widens the axis instead.
|
|
23
|
+
*/
|
|
24
|
+
export declare class EdgeTicks {
|
|
25
|
+
/** The share of the font size one label character takes, on average. */
|
|
26
|
+
static readonly CHARACTER_WIDTH = 0.6;
|
|
27
|
+
/** The pixels left between two neighbouring labels. */
|
|
28
|
+
static readonly LABEL_GAP = 6;
|
|
29
|
+
/** A plot's `scales.<axis>.edgeTicks` over the configuration's `axis.edgeTicks.<axis>`. */
|
|
30
|
+
static resolve(option: AxisScale['edgeTicks'], configured: EdgeTicksOptions): EdgeTicksOptions;
|
|
31
|
+
/**
|
|
32
|
+
* The axis' ticks, and the domain it widens to when `'extend'` resolved a
|
|
33
|
+
* collision. `undefined` when edge ticks are off or the values span nothing.
|
|
34
|
+
*/
|
|
35
|
+
static build({ axis, values, span, spacing, min, fontSize, options }: EdgeTicksInput): {
|
|
36
|
+
ticks: number[];
|
|
37
|
+
domain?: [number, number];
|
|
38
|
+
} | undefined;
|
|
39
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { NiceTicks } from './NiceTicks';
|
|
2
|
+
/**
|
|
3
|
+
* The ticks of a continuous axis: its nice ticks plus the data's first and
|
|
4
|
+
* last values, so a reader sees where the data starts and ends. When an edge
|
|
5
|
+
* tick lands too close to the nice tick next to it for both labels to fit,
|
|
6
|
+
* `options.collision` decides which gives way, or widens the axis instead.
|
|
7
|
+
*/
|
|
8
|
+
export class EdgeTicks {
|
|
9
|
+
/** The share of the font size one label character takes, on average. */
|
|
10
|
+
static CHARACTER_WIDTH = 0.6;
|
|
11
|
+
/** The pixels left between two neighbouring labels. */
|
|
12
|
+
static LABEL_GAP = 6;
|
|
13
|
+
/** A plot's `scales.<axis>.edgeTicks` over the configuration's `axis.edgeTicks.<axis>`. */
|
|
14
|
+
static resolve(option, configured) {
|
|
15
|
+
if (option === undefined)
|
|
16
|
+
return configured;
|
|
17
|
+
if (typeof option === 'boolean')
|
|
18
|
+
return { ...configured, enabled: option };
|
|
19
|
+
return { enabled: true, collision: option.collision ?? configured.collision };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The axis' ticks, and the domain it widens to when `'extend'` resolved a
|
|
23
|
+
* collision. `undefined` when edge ticks are off or the values span nothing.
|
|
24
|
+
*/
|
|
25
|
+
static build({ axis, values, span, spacing, min, fontSize, options }) {
|
|
26
|
+
if (!options.enabled || values.length < 2)
|
|
27
|
+
return undefined;
|
|
28
|
+
const lo = Math.min(...values);
|
|
29
|
+
const hi = Math.max(...values);
|
|
30
|
+
if (!(hi > lo))
|
|
31
|
+
return undefined;
|
|
32
|
+
const count = NiceTicks.count(span, spacing, min);
|
|
33
|
+
// Whole-number values (years, counts) never get ticks between them.
|
|
34
|
+
const step = values.every(Number.isInteger)
|
|
35
|
+
? Math.max(1, NiceTicks.step(lo, hi, count))
|
|
36
|
+
: NiceTicks.step(lo, hi, count);
|
|
37
|
+
const nice = NiceTicks.inRange(lo, hi, step);
|
|
38
|
+
if (!nice.length)
|
|
39
|
+
return { ticks: [lo, hi] };
|
|
40
|
+
const eps = step * 1e-6;
|
|
41
|
+
const pixels = (value) => ((value - lo) / (hi - lo)) * span;
|
|
42
|
+
// Side by side, two labels need half of each one's width; stacked, one line's height.
|
|
43
|
+
const labelWidth = (value) => String(value).length * fontSize * EdgeTicks.CHARACTER_WIDTH;
|
|
44
|
+
const room = (edge, neighbour) => axis === 'x' ? (labelWidth(edge) + labelWidth(neighbour)) / 2 : fontSize;
|
|
45
|
+
const crowds = (edge, neighbour) => Math.abs(pixels(edge) - pixels(neighbour)) < room(edge, neighbour) + EdgeTicks.LABEL_GAP;
|
|
46
|
+
const inner = [...nice];
|
|
47
|
+
const before = [];
|
|
48
|
+
const after = [];
|
|
49
|
+
let domainLo = lo;
|
|
50
|
+
let domainHi = hi;
|
|
51
|
+
const first = nice[0];
|
|
52
|
+
if (first - lo > eps) {
|
|
53
|
+
if (!crowds(lo, first))
|
|
54
|
+
before.push(lo);
|
|
55
|
+
else if (options.collision === 'drop-nice') {
|
|
56
|
+
before.push(lo);
|
|
57
|
+
inner.shift();
|
|
58
|
+
}
|
|
59
|
+
else if (options.collision === 'extend') {
|
|
60
|
+
domainLo = first - step;
|
|
61
|
+
before.push(domainLo);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const last = nice[nice.length - 1];
|
|
65
|
+
if (hi - last > eps) {
|
|
66
|
+
if (!crowds(hi, last))
|
|
67
|
+
after.push(hi);
|
|
68
|
+
else if (options.collision === 'drop-nice') {
|
|
69
|
+
after.push(hi);
|
|
70
|
+
if (inner[inner.length - 1] === last)
|
|
71
|
+
inner.pop();
|
|
72
|
+
}
|
|
73
|
+
else if (options.collision === 'extend') {
|
|
74
|
+
domainHi = last + step;
|
|
75
|
+
after.push(domainHi);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const ticks = [...before, ...inner, ...after];
|
|
79
|
+
const widened = domainLo !== lo || domainHi !== hi;
|
|
80
|
+
return widened ? { ticks, domain: [domainLo, domainHi] } : { ticks };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AxisChannelName } from '../../types/layout/mapping';
|
|
2
|
+
/**
|
|
3
|
+
* "Nice" (1/2/5×10ⁿ) tick steps and the domains they round to, compatible with
|
|
4
|
+
* d3-array's and reimplemented here to avoid depending on internals svelteplot
|
|
5
|
+
* itself pulls in (not one of our declared dependencies) - see
|
|
6
|
+
* https://d3js.org/d3-array/ticks.
|
|
7
|
+
*/
|
|
8
|
+
export declare class NiceTicks {
|
|
9
|
+
/**
|
|
10
|
+
* How far past a tick, as a share of the tick step, a domain still ends on
|
|
11
|
+
* it. Plain `Math.ceil`/`Math.floor` (what d3's `nice()` uses) has no
|
|
12
|
+
* tolerance: parts that add up to `100`, but come out `100.00000000000001`
|
|
13
|
+
* after summing or `100.2` after each was rounded at the source, round a
|
|
14
|
+
* full step further (`100` → `120`). 5% of the step catches that noise and
|
|
15
|
+
* lets a mark pass the last tick by a few pixels at most.
|
|
16
|
+
*/
|
|
17
|
+
static readonly TOLERANCE = 0.05;
|
|
18
|
+
/** The pixels between ticks an axis aims for, along x and along y. */
|
|
19
|
+
static readonly SPACING: {
|
|
20
|
+
readonly x: 80;
|
|
21
|
+
readonly y: 50;
|
|
22
|
+
};
|
|
23
|
+
/** The step between nice ticks for an approximate `count` over `[start, stop]`. */
|
|
24
|
+
static step(start: number, stop: number, count: number): number;
|
|
25
|
+
/** Multiples of `step` covering `[start, stop]`, inclusive of either endpoint when it lands exactly on one. */
|
|
26
|
+
static inRange(start: number, stop: number, step: number): number[];
|
|
27
|
+
/** How many ticks fit `span` pixels at one per `spacing`, and at least `min`. */
|
|
28
|
+
static count(span: number, spacing: number, min: number): number;
|
|
29
|
+
/**
|
|
30
|
+
* `[lo, hi]` widened outward to the nearest nice step for about `count`
|
|
31
|
+
* ticks, like d3's `scale.nice(count)` - except a value within
|
|
32
|
+
* `tolerance × step` past a tick stays on it (see {@link NiceTicks.TOLERANCE}).
|
|
33
|
+
*/
|
|
34
|
+
static domain(lo: number, hi: number, count?: number, tolerance?: number): [number, number];
|
|
35
|
+
/** {@link NiceTicks.domain} with as many ticks as fit `axis` in a plot of this size. */
|
|
36
|
+
static fit(size: {
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
}, axis: AxisChannelName, lo: number, hi: number, tolerance?: number): [number, number];
|
|
40
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Margins } from '../margins/Margins';
|
|
2
|
+
/**
|
|
3
|
+
* "Nice" (1/2/5×10ⁿ) tick steps and the domains they round to, compatible with
|
|
4
|
+
* d3-array's and reimplemented here to avoid depending on internals svelteplot
|
|
5
|
+
* itself pulls in (not one of our declared dependencies) - see
|
|
6
|
+
* https://d3js.org/d3-array/ticks.
|
|
7
|
+
*/
|
|
8
|
+
export class NiceTicks {
|
|
9
|
+
/**
|
|
10
|
+
* How far past a tick, as a share of the tick step, a domain still ends on
|
|
11
|
+
* it. Plain `Math.ceil`/`Math.floor` (what d3's `nice()` uses) has no
|
|
12
|
+
* tolerance: parts that add up to `100`, but come out `100.00000000000001`
|
|
13
|
+
* after summing or `100.2` after each was rounded at the source, round a
|
|
14
|
+
* full step further (`100` → `120`). 5% of the step catches that noise and
|
|
15
|
+
* lets a mark pass the last tick by a few pixels at most.
|
|
16
|
+
*/
|
|
17
|
+
static TOLERANCE = 0.05;
|
|
18
|
+
/** The pixels between ticks an axis aims for, along x and along y. */
|
|
19
|
+
static SPACING = { x: 80, y: 50 };
|
|
20
|
+
/** The step between nice ticks for an approximate `count` over `[start, stop]`. */
|
|
21
|
+
static step(start, stop, count) {
|
|
22
|
+
const e10 = Math.sqrt(50);
|
|
23
|
+
const e5 = Math.sqrt(10);
|
|
24
|
+
const e2 = Math.sqrt(2);
|
|
25
|
+
const step0 = Math.abs(stop - start) / Math.max(0, count);
|
|
26
|
+
let step1 = 10 ** Math.floor(Math.log10(step0));
|
|
27
|
+
const error = step0 / step1;
|
|
28
|
+
if (error >= e10)
|
|
29
|
+
step1 *= 10;
|
|
30
|
+
else if (error >= e5)
|
|
31
|
+
step1 *= 5;
|
|
32
|
+
else if (error >= e2)
|
|
33
|
+
step1 *= 2;
|
|
34
|
+
return step1;
|
|
35
|
+
}
|
|
36
|
+
/** Multiples of `step` covering `[start, stop]`, inclusive of either endpoint when it lands exactly on one. */
|
|
37
|
+
static inRange(start, stop, step) {
|
|
38
|
+
if (!(step > 0) || !Number.isFinite(step))
|
|
39
|
+
return [];
|
|
40
|
+
const lo = Math.ceil(start / step);
|
|
41
|
+
const hi = Math.floor(stop / step);
|
|
42
|
+
const out = [];
|
|
43
|
+
for (let i = lo; i <= hi; i++)
|
|
44
|
+
out.push(i * step);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/** How many ticks fit `span` pixels at one per `spacing`, and at least `min`. */
|
|
48
|
+
static count(span, spacing, min) {
|
|
49
|
+
return Math.max(min, Math.round(span / spacing));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* `[lo, hi]` widened outward to the nearest nice step for about `count`
|
|
53
|
+
* ticks, like d3's `scale.nice(count)` - except a value within
|
|
54
|
+
* `tolerance × step` past a tick stays on it (see {@link NiceTicks.TOLERANCE}).
|
|
55
|
+
*/
|
|
56
|
+
static domain(lo, hi, count = 5, tolerance = NiceTicks.TOLERANCE) {
|
|
57
|
+
if (!(hi > lo))
|
|
58
|
+
return [lo, hi];
|
|
59
|
+
const step = NiceTicks.step(lo, hi, count);
|
|
60
|
+
const slack = step * tolerance;
|
|
61
|
+
const niceLo = Math.floor((lo + slack) / step) * step;
|
|
62
|
+
const niceHi = Math.ceil((hi - slack) / step) * step;
|
|
63
|
+
// `|| 0` turns a `-0` (a bound at zero, rounded toward it) into `0`, which an axis writes without a sign.
|
|
64
|
+
return [niceLo || 0, niceHi || 0];
|
|
65
|
+
}
|
|
66
|
+
/** {@link NiceTicks.domain} with as many ticks as fit `axis` in a plot of this size. */
|
|
67
|
+
static fit(size, axis, lo, hi, tolerance) {
|
|
68
|
+
const box = Margins.AXIS_ESTIMATE.contentBox(size.width, size.height);
|
|
69
|
+
const span = axis === 'x' ? box.width : box.height;
|
|
70
|
+
return NiceTicks.domain(lo, hi, NiceTicks.count(span, NiceTicks.SPACING[axis], 2), tolerance);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
import { AxisX, AxisY, GridX, GridY } from 'svelteplot';
|
|
5
5
|
import { usePlot } from '../plot/usePlot';
|
|
6
6
|
import { getConfiguration } from '../../configuration/config.svelte';
|
|
7
|
-
import {
|
|
7
|
+
import { NiceTicks } from './NiceTicks';
|
|
8
|
+
import { EdgeTicks } from './EdgeTicks';
|
|
8
9
|
import { DISCRETE_SCALE_TYPES } from '../../types/layout/scales';
|
|
9
10
|
import type { AxisBasedScalesConfig } from '../../types/layout/scales';
|
|
10
11
|
import type { AxisValue } from '../../types/layout/mapping';
|
|
@@ -17,7 +18,8 @@
|
|
|
17
18
|
width,
|
|
18
19
|
height,
|
|
19
20
|
numericMargins,
|
|
20
|
-
scales
|
|
21
|
+
scales,
|
|
22
|
+
edgeDomains = undefined
|
|
21
23
|
}: {
|
|
22
24
|
data: TData[];
|
|
23
25
|
getX: (d: TData) => AxisValue;
|
|
@@ -26,6 +28,11 @@
|
|
|
26
28
|
height: number;
|
|
27
29
|
numericMargins: { top: number; right: number; bottom: number; left: number };
|
|
28
30
|
scales?: AxisBasedScalesConfig<TData>;
|
|
31
|
+
/**
|
|
32
|
+
* Where to leave the domain an axis widens to when an edge tick's
|
|
33
|
+
* `'extend'` resolved a collision (see `EdgeTicks`), for the plot to give its scale.
|
|
34
|
+
*/
|
|
35
|
+
edgeDomains?: Map<'x' | 'y', [number, number]>;
|
|
29
36
|
} = $props();
|
|
30
37
|
|
|
31
38
|
const cfg = $derived(getConfiguration());
|
|
@@ -50,7 +57,7 @@
|
|
|
50
57
|
return hi > lo ? [lo, hi] : undefined;
|
|
51
58
|
}
|
|
52
59
|
|
|
53
|
-
/** An axis paired with a discrete scale plays the "index" role for the pair, so *this* axis is the plain magnitude/value one (see `autoTickCount`, `
|
|
60
|
+
/** An axis paired with a discrete scale plays the "index" role for the pair, so *this* axis is the plain magnitude/value one (see `autoTickCount`, `edgeTicksOf`). */
|
|
54
61
|
const isDiscreteType = (type: string) =>
|
|
55
62
|
(DISCRETE_SCALE_TYPES as readonly string[]).includes(type);
|
|
56
63
|
|
|
@@ -75,7 +82,7 @@
|
|
|
75
82
|
return undefined;
|
|
76
83
|
if (oppositeType != null && isDiscreteType(oppositeType)) return undefined;
|
|
77
84
|
const uniqueCount = new Set(values.map(String)).size;
|
|
78
|
-
const natural =
|
|
85
|
+
const natural = NiceTicks.count(span, spacing, min);
|
|
79
86
|
return uniqueCount >= 2 && uniqueCount < natural ? uniqueCount : undefined;
|
|
80
87
|
}
|
|
81
88
|
|
|
@@ -94,32 +101,40 @@
|
|
|
94
101
|
if (!extent) return undefined;
|
|
95
102
|
const [lo, hi] = extent;
|
|
96
103
|
if (!Number.isInteger(lo) || !Number.isInteger(hi)) return undefined;
|
|
97
|
-
return hi - lo <
|
|
104
|
+
return hi - lo < NiceTicks.count(span, spacing, min) ? 1 : undefined;
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
const xSpan = $derived(Math.max(0, width - numericMargins.left - numericMargins.right));
|
|
101
108
|
const ySpan = $derived(Math.max(0, height - numericMargins.top - numericMargins.bottom));
|
|
102
109
|
|
|
103
|
-
const xTickCount = $derived(
|
|
104
|
-
|
|
110
|
+
const xTickCount = $derived(
|
|
111
|
+
autoTickCount(plot.scales.y.type, scales?.x, xValues, xSpan, NiceTicks.SPACING.x, 3)
|
|
112
|
+
);
|
|
113
|
+
const yTickCount = $derived(
|
|
114
|
+
autoTickCount(plot.scales.x.type, scales?.y, yValues, ySpan, NiceTicks.SPACING.y, 2)
|
|
115
|
+
);
|
|
105
116
|
|
|
106
|
-
const xTickInterval = $derived(
|
|
107
|
-
|
|
117
|
+
const xTickInterval = $derived(
|
|
118
|
+
integerTickInterval(scales?.x, xValues, xSpan, NiceTicks.SPACING.x, 3)
|
|
119
|
+
);
|
|
120
|
+
const yTickInterval = $derived(
|
|
121
|
+
integerTickInterval(scales?.y, yValues, ySpan, NiceTicks.SPACING.y, 2)
|
|
122
|
+
);
|
|
108
123
|
|
|
109
124
|
/**
|
|
110
|
-
* A continuous axis' nice ticks plus
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* or the domain.
|
|
125
|
+
* A continuous axis' nice ticks plus the data's first and last values (see
|
|
126
|
+
* {@link EdgeTicks}), when its other axis is continuous too and the author
|
|
127
|
+
* left its density and domain to the plot. Unset otherwise.
|
|
114
128
|
*/
|
|
115
|
-
function
|
|
129
|
+
function edgeTicksOf(
|
|
130
|
+
axis: 'x' | 'y',
|
|
131
|
+
type: string | undefined,
|
|
116
132
|
oppositeType: string | undefined,
|
|
117
133
|
scale: AxisBasedScalesConfig<TData>['x'],
|
|
118
134
|
values: AxisValue[],
|
|
119
135
|
span: number,
|
|
120
|
-
spacing: number,
|
|
121
136
|
min: number
|
|
122
|
-
)
|
|
137
|
+
) {
|
|
123
138
|
if (
|
|
124
139
|
scale?.ticks != null ||
|
|
125
140
|
scale?.tickSpacing != null ||
|
|
@@ -127,25 +142,43 @@
|
|
|
127
142
|
scale?.domain != null
|
|
128
143
|
)
|
|
129
144
|
return undefined;
|
|
145
|
+
if (type != null && isDiscreteType(type)) return undefined;
|
|
130
146
|
if (oppositeType != null && isDiscreteType(oppositeType)) return undefined;
|
|
131
147
|
if (scale?.type != null && scale.type !== 'linear' && scale.type !== 'auto') return undefined;
|
|
132
|
-
const
|
|
133
|
-
if (!
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
const present = values.filter((v) => v != null);
|
|
149
|
+
if (!present.every((v) => typeof v === 'number')) return undefined;
|
|
150
|
+
return EdgeTicks.build({
|
|
151
|
+
axis,
|
|
152
|
+
values: (present as number[]).filter(Number.isFinite),
|
|
153
|
+
span,
|
|
154
|
+
spacing: NiceTicks.SPACING[axis],
|
|
155
|
+
min,
|
|
156
|
+
fontSize: cfg.axis.fontSize,
|
|
157
|
+
options: EdgeTicks.resolve(scale?.edgeTicks, cfg.axis.edgeTicks[axis])
|
|
158
|
+
});
|
|
143
159
|
}
|
|
144
160
|
|
|
145
|
-
const
|
|
146
|
-
|
|
161
|
+
const xEdgeTicks = $derived(
|
|
162
|
+
edgeTicksOf('x', plot.scales.x.type, plot.scales.y.type, scales?.x, xValues, xSpan, 3)
|
|
163
|
+
);
|
|
164
|
+
const yEdgeTicks = $derived(
|
|
165
|
+
edgeTicksOf('y', plot.scales.y.type, plot.scales.x.type, scales?.y, yValues, ySpan, 2)
|
|
147
166
|
);
|
|
148
167
|
|
|
168
|
+
// Hands the plot the domain an axis widens to, only when it changes.
|
|
169
|
+
$effect(() => {
|
|
170
|
+
const next = { x: xEdgeTicks?.domain, y: yEdgeTicks?.domain };
|
|
171
|
+
untrack(() => {
|
|
172
|
+
for (const axis of ['x', 'y'] as const) {
|
|
173
|
+
const domain = next[axis];
|
|
174
|
+
const current = edgeDomains?.get(axis);
|
|
175
|
+
if (domain?.[0] === current?.[0] && domain?.[1] === current?.[1]) continue;
|
|
176
|
+
if (domain) edgeDomains?.set(axis, domain);
|
|
177
|
+
else edgeDomains?.delete(axis);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
149
182
|
const yTitleFontSize = $derived(scales?.y?.titleFontSize ?? cfg.axis.titleFontSize);
|
|
150
183
|
|
|
151
184
|
/**
|
|
@@ -191,7 +224,7 @@
|
|
|
191
224
|
|
|
192
225
|
{#snippet axisX()}
|
|
193
226
|
<AxisX
|
|
194
|
-
ticks={
|
|
227
|
+
ticks={xEdgeTicks?.ticks}
|
|
195
228
|
tickCount={xTickCount}
|
|
196
229
|
interval={xTickInterval}
|
|
197
230
|
tickSize={scales?.x?.tickSize ?? cfg.axis.xTickSize}
|
|
@@ -207,6 +240,7 @@
|
|
|
207
240
|
{/if}
|
|
208
241
|
{#snippet axisY()}
|
|
209
242
|
<AxisY
|
|
243
|
+
ticks={yEdgeTicks?.ticks}
|
|
210
244
|
tickCount={yTickCount}
|
|
211
245
|
interval={yTickInterval}
|
|
212
246
|
tickSize={scales?.y?.tickSize ?? cfg.axis.yTickSize}
|
|
@@ -14,6 +14,11 @@ declare function $$render<TData extends Record<string, unknown>>(): {
|
|
|
14
14
|
left: number;
|
|
15
15
|
};
|
|
16
16
|
scales?: AxisBasedScalesConfig<TData>;
|
|
17
|
+
/**
|
|
18
|
+
* Where to leave the domain an axis widens to when an edge tick's
|
|
19
|
+
* `'extend'` resolved a collision (see `EdgeTicks`), for the plot to give its scale.
|
|
20
|
+
*/
|
|
21
|
+
edgeDomains?: Map<"x" | "y", [number, number]>;
|
|
17
22
|
};
|
|
18
23
|
exports: {};
|
|
19
24
|
bindings: "";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AxisScale } from '../../types/layout/scales';
|
|
2
|
+
import type { AxisValue } from '../../types/layout/mapping';
|
|
3
|
+
/** An {@link AxisScale} turned into the options svelteplot's `<Plot x>`/`<Plot y>` accepts. */
|
|
4
|
+
export declare class SveltePlotScale {
|
|
5
|
+
#private;
|
|
6
|
+
/** Scale types the default `nice` applies to - every other type has its own tick semantics (or isn't numeric at all). */
|
|
7
|
+
static readonly CONTINUOUS_NUMERIC_TYPES: ReadonlySet<string>;
|
|
8
|
+
/**
|
|
9
|
+
* The scale without what the package handles itself - `grid`, `axisDisplay`
|
|
10
|
+
* and a vertical `label`, which `ScalesLayout` draws, and `niceTolerance`,
|
|
11
|
+
* which the "scales" phase applies - with a custom `sort` resolved into a
|
|
12
|
+
* `domain`, and `nice: true` on an unpinned numeric axis.
|
|
13
|
+
*/
|
|
14
|
+
static build<TData>(scale: AxisScale<TData> | undefined, rows: TData[], getAxisValue: (d: TData) => AxisValue): AxisScale<TData> | undefined;
|
|
15
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
2
|
+
/** An {@link AxisScale} turned into the options svelteplot's `<Plot x>`/`<Plot y>` accepts. */
|
|
3
|
+
export class SveltePlotScale {
|
|
4
|
+
/** Scale types the default `nice` applies to - every other type has its own tick semantics (or isn't numeric at all). */
|
|
5
|
+
static CONTINUOUS_NUMERIC_TYPES = new Set([
|
|
6
|
+
'auto',
|
|
7
|
+
'linear',
|
|
8
|
+
'pow',
|
|
9
|
+
'sqrt',
|
|
10
|
+
'log',
|
|
11
|
+
'symlog'
|
|
12
|
+
]);
|
|
13
|
+
/**
|
|
14
|
+
* The scale without what the package handles itself - `grid`, `axisDisplay`
|
|
15
|
+
* and a vertical `label`, which `ScalesLayout` draws, and `niceTolerance`,
|
|
16
|
+
* which the "scales" phase applies - with a custom `sort` resolved into a
|
|
17
|
+
* `domain`, and `nice: true` on an unpinned numeric axis.
|
|
18
|
+
*/
|
|
19
|
+
static build(scale, rows, getAxisValue) {
|
|
20
|
+
const sorted = SveltePlotScale.#sorted(scale, rows, getAxisValue);
|
|
21
|
+
const wantsNice = scale?.domain == null &&
|
|
22
|
+
scale?.ticks == null &&
|
|
23
|
+
scale?.nice == null &&
|
|
24
|
+
(scale?.type == null || SveltePlotScale.CONTINUOUS_NUMERIC_TYPES.has(scale.type)) &&
|
|
25
|
+
SveltePlotScale.#isNumeric(rows, getAxisValue);
|
|
26
|
+
const removesAxis = scale?.axisDisplay === 'none';
|
|
27
|
+
if (!sorted && !wantsNice && !removesAxis)
|
|
28
|
+
return sorted;
|
|
29
|
+
const out = { ...sorted };
|
|
30
|
+
delete out.grid;
|
|
31
|
+
delete out.axisDisplay;
|
|
32
|
+
delete out.niceTolerance;
|
|
33
|
+
if (removesAxis)
|
|
34
|
+
out.axis = false;
|
|
35
|
+
if (scale?.labelOrientation === 'vertical')
|
|
36
|
+
out.label = false;
|
|
37
|
+
if (wantsNice)
|
|
38
|
+
out.nice = true;
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/** svelteplot's `sort` option only does a fixed ascending sort; a custom `scale.sort` comparator is resolved into an explicit `domain` instead, which svelteplot honors as-is. */
|
|
42
|
+
static #sorted(scale, rows, getAxisValue) {
|
|
43
|
+
const sort = scale?.sort;
|
|
44
|
+
if (!sort || scale?.domain)
|
|
45
|
+
return scale;
|
|
46
|
+
const reps = SveltePlotScale.#uniqueRows(rows, getAxisValue);
|
|
47
|
+
if (reps.length === 0 || !reps.every((r) => typeof getAxisValue(r) === 'string'))
|
|
48
|
+
return scale;
|
|
49
|
+
const domain = [...reps].sort(sort).map(getAxisValue);
|
|
50
|
+
return { ...scale, domain };
|
|
51
|
+
}
|
|
52
|
+
/** One representative row per unique axis value (first occurrence in `rows`). */
|
|
53
|
+
static #uniqueRows(rows, getAxisValue) {
|
|
54
|
+
const seen = new SvelteSet();
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const row of rows) {
|
|
57
|
+
const v = getAxisValue(row);
|
|
58
|
+
if (seen.has(v))
|
|
59
|
+
continue;
|
|
60
|
+
seen.add(v);
|
|
61
|
+
out.push(row);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
/** Whether the axis' values are numeric; `nice` on a categorical axis would make svelteplot treat it as linear. */
|
|
66
|
+
static #isNumeric(rows, getAxisValue) {
|
|
67
|
+
let numericSeen = 0;
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
const v = getAxisValue(row);
|
|
70
|
+
if (v == null)
|
|
71
|
+
continue;
|
|
72
|
+
if (typeof v !== 'number')
|
|
73
|
+
return false;
|
|
74
|
+
if (++numericSeen >= 2)
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return numericSeen > 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { ProcessedPositions } from '../../phases/transforms/ProcessedPositions';
|
|
1
2
|
import { Box } from '../margins/Box';
|
|
2
3
|
import { resolveAccessor } from '../../plots/utils/accessors';
|
|
3
|
-
import { SERIES_ID_KEY, SERIES_KEY
|
|
4
|
+
import { SERIES_ID_KEY, SERIES_KEY } from '../../phases/transforms/TransformsPhase';
|
|
4
5
|
import { seriesIdOf } from '../../utils/grouping';
|
|
5
6
|
import { Transforms } from '../../phases/transforms/Transforms';
|
|
6
7
|
import { isGapValue } from '../../plots/utils/gaps';
|
|
@@ -139,7 +140,7 @@ export class ValueLabels {
|
|
|
139
140
|
if (!processed)
|
|
140
141
|
return [];
|
|
141
142
|
const { font, anchor, format } = this;
|
|
142
|
-
const position =
|
|
143
|
+
const position = ProcessedPositions.of(processed);
|
|
143
144
|
const topOf = (d) => position.y(d);
|
|
144
145
|
const nameOf = (d) => String(d[SERIES_KEY]);
|
|
145
146
|
const valueOf = ValueLabels.valueOf(state);
|
|
@@ -15,12 +15,15 @@
|
|
|
15
15
|
currentValue,
|
|
16
16
|
values,
|
|
17
17
|
orientation = 'vertical',
|
|
18
|
+
format = (value: TimeValue) => String(value),
|
|
18
19
|
width,
|
|
19
20
|
height
|
|
20
21
|
}: {
|
|
21
22
|
currentValue: TimeValue;
|
|
22
23
|
values: TimeValue[];
|
|
23
24
|
orientation?: Orientation;
|
|
25
|
+
/** How a step's label reads. */
|
|
26
|
+
format?: (value: TimeValue) => string;
|
|
24
27
|
width: number;
|
|
25
28
|
height: number;
|
|
26
29
|
} = $props();
|
|
@@ -72,7 +75,7 @@
|
|
|
72
75
|
<Text
|
|
73
76
|
data={baseValues}
|
|
74
77
|
{...isHorizontal ? { x: identity, y: 0 } : { x: 0, y: identity }}
|
|
75
|
-
text={(d: TimeValue) =>
|
|
78
|
+
text={(d: TimeValue) => format(d)}
|
|
76
79
|
{...textOffset}
|
|
77
80
|
fontSize={rule.fontSize}
|
|
78
81
|
fill="var(--dct-timeline-axis, currentColor)"
|
|
@@ -82,7 +85,7 @@
|
|
|
82
85
|
<Text
|
|
83
86
|
data={[currentValue]}
|
|
84
87
|
{...isHorizontal ? { x: identity, y: 0 } : { x: 0, y: identity }}
|
|
85
|
-
text={(d: TimeValue) =>
|
|
88
|
+
text={(d: TimeValue) => format(d)}
|
|
86
89
|
{...textOffset}
|
|
87
90
|
fontSize={rule.activeFontSize}
|
|
88
91
|
fontWeight={rule.activeFontWeight}
|