@fundar/data-chart-telling 0.0.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/README.md +250 -0
- package/dist/charts/Chart.svelte +125 -0
- package/dist/charts/Chart.svelte.d.ts +42 -0
- package/dist/charts/bar/Chart.svelte +104 -0
- package/dist/charts/bar/Chart.svelte.d.ts +27 -0
- package/dist/charts/container/ChartContainer.svelte +127 -0
- package/dist/charts/container/ChartContainer.svelte.d.ts +14 -0
- package/dist/charts/heatmap/Chart.svelte +107 -0
- package/dist/charts/heatmap/Chart.svelte.d.ts +30 -0
- package/dist/charts/line/Chart.svelte +106 -0
- package/dist/charts/line/Chart.svelte.d.ts +27 -0
- package/dist/charts/pyramid/Chart.svelte +127 -0
- package/dist/charts/pyramid/Chart.svelte.d.ts +31 -0
- package/dist/configuration/config.svelte.d.ts +12 -0
- package/dist/configuration/config.svelte.js +159 -0
- package/dist/configuration/themes/compact.d.ts +2 -0
- package/dist/configuration/themes/compact.js +9 -0
- package/dist/configuration/themes/dark.d.ts +2 -0
- package/dist/configuration/themes/dark.js +17 -0
- package/dist/configuration/themes/default.d.ts +3 -0
- package/dist/configuration/themes/default.js +2 -0
- package/dist/configuration/themes/editorial.d.ts +2 -0
- package/dist/configuration/themes/editorial.js +14 -0
- package/dist/configuration/themes/index.d.ts +481 -0
- package/dist/configuration/themes/index.js +16 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +27 -0
- package/dist/layout/facet/FacetLayout.svelte +97 -0
- package/dist/layout/facet/FacetLayout.svelte.d.ts +35 -0
- package/dist/layout/legend/ContinuousSection.svelte +95 -0
- package/dist/layout/legend/ContinuousSection.svelte.d.ts +7 -0
- package/dist/layout/legend/DiscreteSection.svelte +87 -0
- package/dist/layout/legend/DiscreteSection.svelte.d.ts +7 -0
- package/dist/layout/legend/LegendLayout.svelte +47 -0
- package/dist/layout/legend/LegendLayout.svelte.d.ts +7 -0
- package/dist/layout/plot/PlotLayout.svelte +236 -0
- package/dist/layout/plot/PlotLayout.svelte.d.ts +70 -0
- package/dist/layout/timeline/Timeline.svelte +86 -0
- package/dist/layout/timeline/Timeline.svelte.d.ts +11 -0
- package/dist/layout/timeline/TimelineLayout.svelte +181 -0
- package/dist/layout/timeline/TimelineLayout.svelte.d.ts +38 -0
- package/dist/layout/tooltip/Tooltip.svelte +55 -0
- package/dist/layout/tooltip/Tooltip.svelte.d.ts +12 -0
- package/dist/layout/tooltip/controller.svelte.d.ts +52 -0
- package/dist/layout/tooltip/controller.svelte.js +128 -0
- package/dist/layout/tooltip/hover.svelte.d.ts +7 -0
- package/dist/layout/tooltip/hover.svelte.js +28 -0
- package/dist/layout/tooltip/utils.d.ts +41 -0
- package/dist/layout/tooltip/utils.js +78 -0
- package/dist/plots/bar/Plot.svelte +176 -0
- package/dist/plots/bar/Plot.svelte.d.ts +26 -0
- package/dist/plots/heatmap/Plot.svelte +175 -0
- package/dist/plots/heatmap/Plot.svelte.d.ts +26 -0
- package/dist/plots/line/Plot.svelte +232 -0
- package/dist/plots/line/Plot.svelte.d.ts +26 -0
- package/dist/plots/markers/DeltaMarker.svelte +104 -0
- package/dist/plots/markers/DeltaMarker.svelte.d.ts +30 -0
- package/dist/plots/markers/DotMarker.svelte +41 -0
- package/dist/plots/markers/DotMarker.svelte.d.ts +31 -0
- package/dist/plots/markers/HoverMarker.svelte +78 -0
- package/dist/plots/markers/HoverMarker.svelte.d.ts +22 -0
- package/dist/plots/markers/PeakMarker.svelte +80 -0
- package/dist/plots/markers/PeakMarker.svelte.d.ts +13 -0
- package/dist/plots/markers/TextMarker.svelte +55 -0
- package/dist/plots/markers/TextMarker.svelte.d.ts +32 -0
- package/dist/plots/pyramid/Plot.svelte +282 -0
- package/dist/plots/pyramid/Plot.svelte.d.ts +26 -0
- package/dist/plots/utils/accessors.d.ts +6 -0
- package/dist/plots/utils/accessors.js +10 -0
- package/dist/plots/utils/delta.d.ts +12 -0
- package/dist/plots/utils/delta.js +69 -0
- package/dist/plots/utils/segments.d.ts +33 -0
- package/dist/plots/utils/segments.js +153 -0
- package/dist/stores/device.d.ts +7 -0
- package/dist/stores/device.js +39 -0
- package/dist/types/charts/common.d.ts +64 -0
- package/dist/types/charts/common.js +1 -0
- package/dist/types/charts/interpolate.d.ts +2 -0
- package/dist/types/charts/interpolate.js +1 -0
- package/dist/types/charts/legend.d.ts +29 -0
- package/dist/types/charts/legend.js +1 -0
- package/dist/types/charts/props.d.ts +35 -0
- package/dist/types/charts/props.js +1 -0
- package/dist/types/configuration/styling.d.ts +97 -0
- package/dist/types/configuration/styling.js +1 -0
- package/dist/types/layout/tooltip.d.ts +88 -0
- package/dist/types/layout/tooltip.js +1 -0
- package/dist/types/plots/common.d.ts +100 -0
- package/dist/types/plots/common.js +1 -0
- package/dist/types/plots/delta.d.ts +19 -0
- package/dist/types/plots/delta.js +1 -0
- package/dist/types/plots/markers.d.ts +56 -0
- package/dist/types/plots/markers.js +1 -0
- package/dist/types/plots/props.d.ts +23 -0
- package/dist/types/plots/props.js +1 -0
- package/dist/types/plots/styles.d.ts +176 -0
- package/dist/types/plots/styles.js +1 -0
- package/dist/types/viewport.d.ts +4 -0
- package/dist/types/viewport.js +1 -0
- package/dist/utils/color.d.ts +5 -0
- package/dist/utils/color.js +42 -0
- package/dist/utils/grouping.d.ts +14 -0
- package/dist/utils/grouping.js +36 -0
- package/dist/utils/interpolate.d.ts +30 -0
- package/dist/utils/interpolate.js +71 -0
- package/package.json +76 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FacetColumns, FacetCellSnippet } from '../../types/charts/common';
|
|
2
|
+
import type { Accessor } from '../../types/plots/common';
|
|
3
|
+
declare function $$render<TRow extends Record<string, unknown>>(): {
|
|
4
|
+
props: {
|
|
5
|
+
data: TRow[];
|
|
6
|
+
by: Accessor<TRow, unknown>;
|
|
7
|
+
columns?: FacetColumns;
|
|
8
|
+
values?: string[];
|
|
9
|
+
gap?: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
cell: FacetCellSnippet<TRow>;
|
|
13
|
+
};
|
|
14
|
+
exports: {};
|
|
15
|
+
bindings: "";
|
|
16
|
+
slots: {};
|
|
17
|
+
events: {};
|
|
18
|
+
};
|
|
19
|
+
declare class __sveltets_Render<TRow extends Record<string, unknown>> {
|
|
20
|
+
props(): ReturnType<typeof $$render<TRow>>['props'];
|
|
21
|
+
events(): ReturnType<typeof $$render<TRow>>['events'];
|
|
22
|
+
slots(): ReturnType<typeof $$render<TRow>>['slots'];
|
|
23
|
+
bindings(): "";
|
|
24
|
+
exports(): {};
|
|
25
|
+
}
|
|
26
|
+
interface $$IsomorphicComponent {
|
|
27
|
+
new <TRow extends Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<TRow>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<TRow>['props']>, ReturnType<__sveltets_Render<TRow>['events']>, ReturnType<__sveltets_Render<TRow>['slots']>> & {
|
|
28
|
+
$$bindings?: ReturnType<__sveltets_Render<TRow>['bindings']>;
|
|
29
|
+
} & ReturnType<__sveltets_Render<TRow>['exports']>;
|
|
30
|
+
<TRow extends Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<TRow>['props']> & {}): ReturnType<__sveltets_Render<TRow>['exports']>;
|
|
31
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
32
|
+
}
|
|
33
|
+
declare const FacetLayout: $$IsomorphicComponent;
|
|
34
|
+
type FacetLayout<TRow extends Record<string, unknown>> = InstanceType<typeof FacetLayout<TRow>>;
|
|
35
|
+
export default FacetLayout;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ContinuousLegendSection } from '../../types/charts/legend';
|
|
3
|
+
import { makeColorScale } from '../../utils/color';
|
|
4
|
+
import { getConfiguration } from '../../configuration/config.svelte';
|
|
5
|
+
|
|
6
|
+
let { section }: { section: ContinuousLegendSection } = $props();
|
|
7
|
+
|
|
8
|
+
const cfg = $derived(getConfiguration());
|
|
9
|
+
const gradientId = `dct-grad-${Math.random().toString(36).slice(2)}`;
|
|
10
|
+
const barWidth = $derived(section.width ?? 170);
|
|
11
|
+
const colorScale = $derived(
|
|
12
|
+
makeColorScale(
|
|
13
|
+
section.min,
|
|
14
|
+
section.max,
|
|
15
|
+
section.colorMin ?? cfg.continuous.min,
|
|
16
|
+
section.colorMax ?? cfg.continuous.max,
|
|
17
|
+
),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const fmt = $derived(section.format ?? ((v: number) => String(v)));
|
|
21
|
+
|
|
22
|
+
const tickPositions = $derived(
|
|
23
|
+
(section.ticks ?? []).map((v) => ({
|
|
24
|
+
v,
|
|
25
|
+
t: (v - section.min) / (section.max - section.min || 1),
|
|
26
|
+
label: fmt(v),
|
|
27
|
+
})),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const stops = $derived(
|
|
31
|
+
Array.from({ length: 20 }, (_, i) => {
|
|
32
|
+
const t = i / 19;
|
|
33
|
+
const v = section.min + t * (section.max - section.min);
|
|
34
|
+
return { offset: t * 100, color: colorScale(v) };
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<div class="dct-continuous" style:width="{barWidth}px">
|
|
40
|
+
<svg width={barWidth} height="20" style="overflow: visible">
|
|
41
|
+
<defs>
|
|
42
|
+
<linearGradient id={gradientId} x1="0" x2="1" y1="0" y2="0">
|
|
43
|
+
{#each stops as s (s.offset)}
|
|
44
|
+
<stop offset="{s.offset}%" stop-color={s.color} />
|
|
45
|
+
{/each}
|
|
46
|
+
</linearGradient>
|
|
47
|
+
</defs>
|
|
48
|
+
<rect width={barWidth} height="12" rx="2" fill="url(#{gradientId})" />
|
|
49
|
+
{#each tickPositions as tick (tick.v)}
|
|
50
|
+
<line
|
|
51
|
+
x1={tick.t * barWidth}
|
|
52
|
+
y1="12"
|
|
53
|
+
x2={tick.t * barWidth}
|
|
54
|
+
y2="18"
|
|
55
|
+
stroke="currentColor"
|
|
56
|
+
stroke-width="1"
|
|
57
|
+
/>
|
|
58
|
+
{/each}
|
|
59
|
+
</svg>
|
|
60
|
+
|
|
61
|
+
<div class="dct-continuous__labels" style:width="{barWidth}px">
|
|
62
|
+
<span class="dct-continuous__edge dct-continuous__edge--start">{fmt(section.min)}</span>
|
|
63
|
+
{#each tickPositions as tick (tick.v)}
|
|
64
|
+
<span class="dct-continuous__tick" style:left="{tick.t * barWidth}px">{tick.label}</span>
|
|
65
|
+
{/each}
|
|
66
|
+
<span class="dct-continuous__edge dct-continuous__edge--end">{fmt(section.max)}</span>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<style>
|
|
71
|
+
.dct-continuous {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
gap: 0.25rem;
|
|
75
|
+
}
|
|
76
|
+
.dct-continuous__labels {
|
|
77
|
+
position: relative;
|
|
78
|
+
height: 1rem;
|
|
79
|
+
font-size: var(--dct-legend-size, 0.75rem);
|
|
80
|
+
user-select: none;
|
|
81
|
+
}
|
|
82
|
+
.dct-continuous__edge {
|
|
83
|
+
position: absolute;
|
|
84
|
+
}
|
|
85
|
+
.dct-continuous__edge--start {
|
|
86
|
+
left: 0;
|
|
87
|
+
}
|
|
88
|
+
.dct-continuous__edge--end {
|
|
89
|
+
right: 0;
|
|
90
|
+
}
|
|
91
|
+
.dct-continuous__tick {
|
|
92
|
+
position: absolute;
|
|
93
|
+
transform: translateX(-50%);
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ContinuousLegendSection } from '../../types/charts/legend';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
section: ContinuousLegendSection;
|
|
4
|
+
};
|
|
5
|
+
declare const ContinuousSection: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type ContinuousSection = ReturnType<typeof ContinuousSection>;
|
|
7
|
+
export default ContinuousSection;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getConfiguration } from '../../configuration/config.svelte';
|
|
3
|
+
import { paletteColor } from '../../utils/color';
|
|
4
|
+
import type { DiscreteLegendSection } from '../../types/charts/legend';
|
|
5
|
+
import type { LineStyle } from '../../types/plots/styles';
|
|
6
|
+
|
|
7
|
+
let { section }: { section: DiscreteLegendSection } = $props();
|
|
8
|
+
|
|
9
|
+
const cfg = $derived(getConfiguration());
|
|
10
|
+
const columns = $derived(section.columns ?? 1);
|
|
11
|
+
|
|
12
|
+
function dashArray(style?: LineStyle): string | undefined {
|
|
13
|
+
switch (style) {
|
|
14
|
+
case 'dashed':
|
|
15
|
+
return '2,5';
|
|
16
|
+
case 'dotted':
|
|
17
|
+
return '1,4';
|
|
18
|
+
case 'dashdot':
|
|
19
|
+
return '5,3,1,3';
|
|
20
|
+
default:
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<div class="dct-discrete" style:grid-template-columns="repeat({columns}, max-content)">
|
|
27
|
+
{#each section.items as item, i (item.name)}
|
|
28
|
+
{@const color = item.color ?? paletteColor(i, cfg.palette)}
|
|
29
|
+
<div class="dct-discrete__item" style:opacity={item.opacity ?? 1}>
|
|
30
|
+
<svg class="dct-discrete__swatch" viewBox="0 0 15 10">
|
|
31
|
+
{#if item.symbol === 'circle'}
|
|
32
|
+
<circle cx="5" cy="5" r="4" fill={color} />
|
|
33
|
+
{:else if item.symbol === 'square'}
|
|
34
|
+
<rect x="1" y="1" width="8" height="8" rx="2" fill={color} />
|
|
35
|
+
{:else if item.symbol === 'triangle'}
|
|
36
|
+
<path
|
|
37
|
+
d="M 6 2 L 9 7.5 A 1.5 1.5 0 0 1 7.8 10 L 2.2 10 A 1.5 1.5 0 0 1 1 7.5 L 4 2 A 1.5 1.5 0 0 1 6 2"
|
|
38
|
+
fill={color}
|
|
39
|
+
transform="translate(0,-1)"
|
|
40
|
+
/>
|
|
41
|
+
{:else if item.symbol === 'line'}
|
|
42
|
+
<line
|
|
43
|
+
x1="2"
|
|
44
|
+
y1="5"
|
|
45
|
+
x2="13"
|
|
46
|
+
y2="5"
|
|
47
|
+
stroke={color}
|
|
48
|
+
stroke-width="3"
|
|
49
|
+
stroke-linecap="round"
|
|
50
|
+
stroke-dasharray={dashArray(item.lineStyle)}
|
|
51
|
+
/>
|
|
52
|
+
{:else}
|
|
53
|
+
<rect x="1" y="1" width="8" height="8" fill={color} />
|
|
54
|
+
{/if}
|
|
55
|
+
</svg>
|
|
56
|
+
<span class="dct-discrete__label">{item.label || item.name}</span>
|
|
57
|
+
</div>
|
|
58
|
+
{/each}
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<style>
|
|
62
|
+
.dct-discrete {
|
|
63
|
+
display: grid;
|
|
64
|
+
column-gap: 1.25rem;
|
|
65
|
+
row-gap: 0.4rem;
|
|
66
|
+
max-width: 100%;
|
|
67
|
+
}
|
|
68
|
+
.dct-discrete__item {
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
gap: 0.35rem;
|
|
72
|
+
min-width: 0;
|
|
73
|
+
}
|
|
74
|
+
.dct-discrete__swatch {
|
|
75
|
+
width: var(--dct-legend-swatch, 1.25rem);
|
|
76
|
+
height: var(--dct-legend-swatch, 1.25rem);
|
|
77
|
+
flex-shrink: 0;
|
|
78
|
+
}
|
|
79
|
+
.dct-discrete__label {
|
|
80
|
+
font-size: var(--dct-legend-size, 0.875rem);
|
|
81
|
+
font-weight: var(--dct-legend-weight, 400);
|
|
82
|
+
white-space: nowrap;
|
|
83
|
+
overflow: hidden;
|
|
84
|
+
text-overflow: ellipsis;
|
|
85
|
+
min-width: 0;
|
|
86
|
+
}
|
|
87
|
+
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DiscreteLegendSection } from '../../types/charts/legend';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
section: DiscreteLegendSection;
|
|
4
|
+
};
|
|
5
|
+
declare const DiscreteSection: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type DiscreteSection = ReturnType<typeof DiscreteSection>;
|
|
7
|
+
export default DiscreteSection;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { LegendSection } from '../../types/charts/legend';
|
|
3
|
+
import DiscreteSection from './DiscreteSection.svelte';
|
|
4
|
+
import ContinuousSection from './ContinuousSection.svelte';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Renders one or more legend sections side by side, dispatching each to the
|
|
8
|
+
* matching section renderer. Discrete and continuous sections can be mixed.
|
|
9
|
+
*/
|
|
10
|
+
let { sections }: { sections: LegendSection[] } = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<div class="dct-legend">
|
|
14
|
+
{#each sections as section, i (section.title ?? i)}
|
|
15
|
+
<div class="dct-legend__section">
|
|
16
|
+
{#if section.title}
|
|
17
|
+
<div class="dct-legend__title">{section.title}</div>
|
|
18
|
+
{/if}
|
|
19
|
+
{#if section.type === 'discrete'}
|
|
20
|
+
<DiscreteSection {section} />
|
|
21
|
+
{:else if section.type === 'continuous'}
|
|
22
|
+
<ContinuousSection {section} />
|
|
23
|
+
{/if}
|
|
24
|
+
</div>
|
|
25
|
+
{/each}
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<style>
|
|
29
|
+
.dct-legend {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-wrap: wrap;
|
|
32
|
+
gap: var(--dct-legend-gap, 0.5rem 2rem);
|
|
33
|
+
align-items: flex-start;
|
|
34
|
+
font-family: var(--dct-legend-font, inherit);
|
|
35
|
+
color: var(--dct-legend-color, inherit);
|
|
36
|
+
}
|
|
37
|
+
.dct-legend__section {
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
gap: 0.5rem;
|
|
41
|
+
min-width: 0;
|
|
42
|
+
}
|
|
43
|
+
.dct-legend__title {
|
|
44
|
+
font-size: var(--dct-legend-title-size, 0.875rem);
|
|
45
|
+
font-weight: var(--dct-legend-title-weight, 500);
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LegendSection } from '../../types/charts/legend';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
sections: LegendSection[];
|
|
4
|
+
};
|
|
5
|
+
declare const LegendLayout: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type LegendLayout = ReturnType<typeof LegendLayout>;
|
|
7
|
+
export default LegendLayout;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
<script lang="ts" generics="TData extends Record<string, unknown>">
|
|
2
|
+
import type { ComponentProps, Snippet } from 'svelte';
|
|
3
|
+
import { Plot, Pointer, RuleX, RuleY, AxisX, AxisY } from 'svelteplot';
|
|
4
|
+
import { getConfiguration } from '../../configuration/config.svelte';
|
|
5
|
+
import { getHover, createHover } from '../tooltip/hover.svelte';
|
|
6
|
+
import { createHoverController } from '../tooltip/controller.svelte';
|
|
7
|
+
import { hasHoverMarker, findHoverMarker } from '../tooltip/utils';
|
|
8
|
+
import HoverMarker from '../../plots/markers/HoverMarker.svelte';
|
|
9
|
+
import Tooltip from '../tooltip/Tooltip.svelte';
|
|
10
|
+
import type { AxisValue, Series, ScalesConfig, MarginConfig } from '../../types/plots/common';
|
|
11
|
+
import type { Marker } from '../../types/plots/markers';
|
|
12
|
+
import type { HoverStrategy, HoverDisplayPoint, HoverPoint, TooltipRuntime } from '../../types/layout/tooltip';
|
|
13
|
+
|
|
14
|
+
type SveltePlotProps = ComponentProps<typeof Plot>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Shared chrome around every plot kind's svelteplot markup: the pointer-event
|
|
18
|
+
* wrapper, the hover/tooltip state machine, the `<Plot>` element itself plus
|
|
19
|
+
* standalone markers (`ruleX`/`ruleY`/un-scoped `component`) and the floating
|
|
20
|
+
* tooltip. A plot kind only supplies its data/accessors and its marks
|
|
21
|
+
* (`<Line>`/`<BarY>`/`<Cell>`, …) through `children`.
|
|
22
|
+
*/
|
|
23
|
+
let {
|
|
24
|
+
width,
|
|
25
|
+
height,
|
|
26
|
+
x,
|
|
27
|
+
y,
|
|
28
|
+
data,
|
|
29
|
+
getX,
|
|
30
|
+
getY,
|
|
31
|
+
matchY,
|
|
32
|
+
hoverPoints,
|
|
33
|
+
label,
|
|
34
|
+
tooltip = undefined,
|
|
35
|
+
markers = [],
|
|
36
|
+
seriesData = [],
|
|
37
|
+
showHoverLabels = true,
|
|
38
|
+
scales,
|
|
39
|
+
margins,
|
|
40
|
+
defaultStrategy = 'x',
|
|
41
|
+
children,
|
|
42
|
+
}: {
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
x?: SveltePlotProps['x'];
|
|
46
|
+
y?: SveltePlotProps['y'];
|
|
47
|
+
/** Flat rows fed to svelteplot's `<Pointer>` for nearest-point matching. */
|
|
48
|
+
data: TData[];
|
|
49
|
+
getX: (d: TData) => AxisValue;
|
|
50
|
+
getY: (d: TData) => AxisValue;
|
|
51
|
+
/** Pointer's `y` channel — only needed when matching must be 2D (e.g. a heatmap grid). */
|
|
52
|
+
matchY?: (d: TData) => AxisValue;
|
|
53
|
+
hoverPoints: HoverPoint<TData>[];
|
|
54
|
+
label: (row: TData) => string;
|
|
55
|
+
tooltip?: TooltipRuntime<TData>;
|
|
56
|
+
markers?: Marker[];
|
|
57
|
+
/** All resolved series, forwarded to unscoped component markers (e.g. DeltaMarker). */
|
|
58
|
+
seriesData?: Series<TData>[];
|
|
59
|
+
/** Scale config forwarded from the plot. */
|
|
60
|
+
scales?: ScalesConfig;
|
|
61
|
+
/** Partial margin override — merged on top of the theme before tickRotate adjustment. */
|
|
62
|
+
margins?: MarginConfig;
|
|
63
|
+
showHoverLabels?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* The plot kind's default hover strategy when none is set on the tooltip or
|
|
66
|
+
* hover marker. Line/bar → `'x'`, pyramid → `'y'`, heatmap → `'punctual'`.
|
|
67
|
+
*/
|
|
68
|
+
defaultStrategy?: HoverStrategy;
|
|
69
|
+
/**
|
|
70
|
+
* `matchedPoints` carries only display info (+ `series`), not `row` — a
|
|
71
|
+
* per-series-scoped `HoverMarker` never needs the source row, just enough
|
|
72
|
+
* to filter and place dots/labels, which sidesteps forcing a concrete
|
|
73
|
+
* `TData` resolution through this snippet's type for every plot kind.
|
|
74
|
+
* `impliesRuleX`/`impliesRuleY` tell the plot kind whether the active
|
|
75
|
+
* strategy implies a crosshair rule on each axis by default.
|
|
76
|
+
*/
|
|
77
|
+
children: Snippet<
|
|
78
|
+
[{
|
|
79
|
+
matchedPoints: HoverDisplayPoint[];
|
|
80
|
+
ruleX: AxisValue | null;
|
|
81
|
+
ruleY: AxisValue | null;
|
|
82
|
+
impliesRuleX: boolean;
|
|
83
|
+
impliesRuleY: boolean;
|
|
84
|
+
}]
|
|
85
|
+
>;
|
|
86
|
+
} = $props();
|
|
87
|
+
|
|
88
|
+
const cfg = $derived(getConfiguration());
|
|
89
|
+
|
|
90
|
+
const computedMargins = $derived.by(() => {
|
|
91
|
+
const base = { ...cfg.margins, ...margins };
|
|
92
|
+
const xRotate = scales?.x?.tickRotate;
|
|
93
|
+
if (!xRotate) return base;
|
|
94
|
+
const rad = Math.abs(xRotate) * Math.PI / 180;
|
|
95
|
+
return {
|
|
96
|
+
...base,
|
|
97
|
+
bottom: base.bottom + Math.ceil(Math.sin(rad) * 20),
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ── Hover / tooltip ───────────────────────────────────────────────────────
|
|
102
|
+
const hoverActive = $derived(tooltip != null || hasHoverMarker(markers));
|
|
103
|
+
|
|
104
|
+
const sharedHover = getHover();
|
|
105
|
+
const localHover = createHover();
|
|
106
|
+
const hover = $derived(sharedHover ?? localHover);
|
|
107
|
+
|
|
108
|
+
$effect(() => {
|
|
109
|
+
if (sharedHover) return;
|
|
110
|
+
const hoverMarker = findHoverMarker(markers);
|
|
111
|
+
localHover.strategy = tooltip?.strategy ?? hoverMarker?.strategy ?? defaultStrategy;
|
|
112
|
+
localHover.sync = tooltip?.sync ?? hoverMarker?.sync ?? false;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const ctrl = createHoverController<TData>({
|
|
116
|
+
hover: () => hover,
|
|
117
|
+
facetId: () => tooltip?.facetId ?? '',
|
|
118
|
+
points: () => hoverPoints,
|
|
119
|
+
getX: () => getX,
|
|
120
|
+
getY: () => getY,
|
|
121
|
+
label: () => label,
|
|
122
|
+
format: () => tooltip?.format ?? label,
|
|
123
|
+
seriesFilter: () => tooltip?.series,
|
|
124
|
+
width: () => width,
|
|
125
|
+
height: () => height,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const topLevelMarkers = $derived(
|
|
129
|
+
markers.filter(
|
|
130
|
+
(marker) =>
|
|
131
|
+
marker.type === 'ruleX' ||
|
|
132
|
+
marker.type === 'ruleY' ||
|
|
133
|
+
(marker.type === 'component' && !marker.series),
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
/** The unscoped (no `series`) hover marker, if any. */
|
|
138
|
+
const unscopedHoverMarker = $derived(
|
|
139
|
+
markers.find((m) => m.type === 'hover' && !m.series) as
|
|
140
|
+
| Extract<Marker, { type: 'hover' }>
|
|
141
|
+
| undefined,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Gate ruleX display: shows when the strategy implies it (default) or the
|
|
146
|
+
* marker explicitly requests it (`ruleX: true`), and is never suppressed
|
|
147
|
+
* unless the marker explicitly disables it (`ruleX: false`).
|
|
148
|
+
*/
|
|
149
|
+
const showRuleX = $derived(
|
|
150
|
+
unscopedHoverMarker != null &&
|
|
151
|
+
unscopedHoverMarker.ruleX !== false &&
|
|
152
|
+
(ctrl.impliesRuleX || unscopedHoverMarker.ruleX === true),
|
|
153
|
+
);
|
|
154
|
+
const showRuleY = $derived(
|
|
155
|
+
unscopedHoverMarker != null &&
|
|
156
|
+
unscopedHoverMarker.ruleY !== false &&
|
|
157
|
+
(ctrl.impliesRuleY || unscopedHoverMarker.ruleY === true),
|
|
158
|
+
);
|
|
159
|
+
</script>
|
|
160
|
+
|
|
161
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
162
|
+
<div
|
|
163
|
+
class="dct-plot"
|
|
164
|
+
style:color={cfg.axis.color}
|
|
165
|
+
onpointermove={hoverActive ? ctrl.onMove : undefined}
|
|
166
|
+
onpointerleave={hoverActive ? ctrl.onLeave : undefined}
|
|
167
|
+
>
|
|
168
|
+
<Plot {width} {height} margin={computedMargins} {x} {y}>
|
|
169
|
+
<AxisX tickSize={scales?.x?.tickSize ?? cfg.axis.xTickSize} tickPadding={scales?.x?.tickPadding ?? cfg.axis.xTickPadding} tickFontSize={cfg.axis.fontSize} titleFontSize={scales?.x?.titleFontSize ?? cfg.axis.titleFontSize} />
|
|
170
|
+
<AxisY tickSize={scales?.y?.tickSize ?? cfg.axis.yTickSize} tickPadding={scales?.y?.tickPadding ?? cfg.axis.yTickPadding} tickFontSize={cfg.axis.fontSize} titleFontSize={scales?.y?.titleFontSize ?? cfg.axis.titleFontSize} />
|
|
171
|
+
{@render children({
|
|
172
|
+
matchedPoints: ctrl.points,
|
|
173
|
+
ruleX: ctrl.ruleX,
|
|
174
|
+
ruleY: ctrl.ruleY,
|
|
175
|
+
impliesRuleX: ctrl.impliesRuleX,
|
|
176
|
+
impliesRuleY: ctrl.impliesRuleY,
|
|
177
|
+
})}
|
|
178
|
+
|
|
179
|
+
{#each topLevelMarkers as marker, i (marker.type === 'ruleX' ? `${marker.type}-${marker.x}` : marker.type === 'ruleY' ? `${marker.type}-${marker.y}` : `${marker.type}-${i}`)}
|
|
180
|
+
{#if marker.type === 'ruleX'}
|
|
181
|
+
<RuleX
|
|
182
|
+
data={[{ xValue: marker.x }]}
|
|
183
|
+
x={(d) => d.xValue}
|
|
184
|
+
stroke={marker.style?.stroke ?? 'currentColor'}
|
|
185
|
+
strokeWidth={marker.style?.strokeWidth ?? 1}
|
|
186
|
+
strokeOpacity={marker.style?.strokeOpacity ?? 1}
|
|
187
|
+
strokeDasharray={marker.style?.strokeDasharray}
|
|
188
|
+
/>
|
|
189
|
+
{:else if marker.type === 'ruleY'}
|
|
190
|
+
<RuleY
|
|
191
|
+
data={[{ yValue: marker.y }]}
|
|
192
|
+
y={(d) => d.yValue}
|
|
193
|
+
stroke={marker.style?.stroke ?? 'currentColor'}
|
|
194
|
+
strokeWidth={marker.style?.strokeWidth ?? 1}
|
|
195
|
+
strokeOpacity={marker.style?.strokeOpacity ?? 1}
|
|
196
|
+
strokeDasharray={marker.style?.strokeDasharray}
|
|
197
|
+
/>
|
|
198
|
+
{:else if marker.type === 'component'}
|
|
199
|
+
<marker.component {...marker.props} data={marker.data ?? seriesData} />
|
|
200
|
+
{/if}
|
|
201
|
+
{/each}
|
|
202
|
+
|
|
203
|
+
{#if hoverActive}
|
|
204
|
+
<Pointer {data} x={getX} y={matchY} maxDistance={400} onupdate={ctrl.onPointer} />
|
|
205
|
+
{/if}
|
|
206
|
+
|
|
207
|
+
{#if unscopedHoverMarker}
|
|
208
|
+
<HoverMarker
|
|
209
|
+
data={ctrl.points}
|
|
210
|
+
ruleX={showRuleX ? ctrl.ruleX : null}
|
|
211
|
+
ruleY={showRuleY ? ctrl.ruleY : null}
|
|
212
|
+
showLabels={unscopedHoverMarker.showLabels ?? showHoverLabels}
|
|
213
|
+
ruleStyle={unscopedHoverMarker.ruleStyle}
|
|
214
|
+
dotStyle={unscopedHoverMarker.dotStyle}
|
|
215
|
+
fontStyle={unscopedHoverMarker.fontStyle}
|
|
216
|
+
/>
|
|
217
|
+
{/if}
|
|
218
|
+
</Plot>
|
|
219
|
+
|
|
220
|
+
{#if tooltip && ctrl.showTooltip && ctrl.tooltipPosition}
|
|
221
|
+
<Tooltip x={ctrl.tooltipPosition.x} y={ctrl.tooltipPosition.y}>
|
|
222
|
+
{#if tooltip.content}
|
|
223
|
+
{@render tooltip.content({ rows: ctrl.matchedRows.map((p) => p.row) })}
|
|
224
|
+
{:else}
|
|
225
|
+
{ctrl.tooltipText}
|
|
226
|
+
{/if}
|
|
227
|
+
</Tooltip>
|
|
228
|
+
{/if}
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<style>
|
|
232
|
+
.dct-plot {
|
|
233
|
+
position: relative;
|
|
234
|
+
line-height: 0;
|
|
235
|
+
}
|
|
236
|
+
</style>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { AxisValue, Series, ScalesConfig, MarginConfig } from '../../types/plots/common';
|
|
3
|
+
import type { Marker } from '../../types/plots/markers';
|
|
4
|
+
import type { HoverStrategy, HoverDisplayPoint, HoverPoint, TooltipRuntime } from '../../types/layout/tooltip';
|
|
5
|
+
declare function $$render<TData extends Record<string, unknown>>(): {
|
|
6
|
+
props: {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
x?: false | Partial<import("svelteplot/types/scale.js").XScaleOptions> | import("svelteplot/types/data.js").RawValue[] | undefined;
|
|
10
|
+
y?: false | import("svelteplot/types/data.js").RawValue[] | Partial<import("svelteplot/types/scale.js").YScaleOptions> | undefined;
|
|
11
|
+
/** Flat rows fed to svelteplot's `<Pointer>` for nearest-point matching. */
|
|
12
|
+
data: TData[];
|
|
13
|
+
getX: (d: TData) => AxisValue;
|
|
14
|
+
getY: (d: TData) => AxisValue;
|
|
15
|
+
/** Pointer's `y` channel — only needed when matching must be 2D (e.g. a heatmap grid). */
|
|
16
|
+
matchY?: (d: TData) => AxisValue;
|
|
17
|
+
hoverPoints: HoverPoint<TData>[];
|
|
18
|
+
label: (row: TData) => string;
|
|
19
|
+
tooltip?: TooltipRuntime<TData>;
|
|
20
|
+
markers?: Marker[];
|
|
21
|
+
/** All resolved series, forwarded to unscoped component markers (e.g. DeltaMarker). */
|
|
22
|
+
seriesData?: Series<TData>[];
|
|
23
|
+
/** Scale config forwarded from the plot. */
|
|
24
|
+
scales?: ScalesConfig;
|
|
25
|
+
/** Partial margin override — merged on top of the theme before tickRotate adjustment. */
|
|
26
|
+
margins?: MarginConfig;
|
|
27
|
+
showHoverLabels?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* The plot kind's default hover strategy when none is set on the tooltip or
|
|
30
|
+
* hover marker. Line/bar → `'x'`, pyramid → `'y'`, heatmap → `'punctual'`.
|
|
31
|
+
*/
|
|
32
|
+
defaultStrategy?: HoverStrategy;
|
|
33
|
+
/**
|
|
34
|
+
* `matchedPoints` carries only display info (+ `series`), not `row` — a
|
|
35
|
+
* per-series-scoped `HoverMarker` never needs the source row, just enough
|
|
36
|
+
* to filter and place dots/labels, which sidesteps forcing a concrete
|
|
37
|
+
* `TData` resolution through this snippet's type for every plot kind.
|
|
38
|
+
* `impliesRuleX`/`impliesRuleY` tell the plot kind whether the active
|
|
39
|
+
* strategy implies a crosshair rule on each axis by default.
|
|
40
|
+
*/
|
|
41
|
+
children: Snippet<[{
|
|
42
|
+
matchedPoints: HoverDisplayPoint[];
|
|
43
|
+
ruleX: AxisValue | null;
|
|
44
|
+
ruleY: AxisValue | null;
|
|
45
|
+
impliesRuleX: boolean;
|
|
46
|
+
impliesRuleY: boolean;
|
|
47
|
+
}]>;
|
|
48
|
+
};
|
|
49
|
+
exports: {};
|
|
50
|
+
bindings: "";
|
|
51
|
+
slots: {};
|
|
52
|
+
events: {};
|
|
53
|
+
};
|
|
54
|
+
declare class __sveltets_Render<TData extends Record<string, unknown>> {
|
|
55
|
+
props(): ReturnType<typeof $$render<TData>>['props'];
|
|
56
|
+
events(): ReturnType<typeof $$render<TData>>['events'];
|
|
57
|
+
slots(): ReturnType<typeof $$render<TData>>['slots'];
|
|
58
|
+
bindings(): "";
|
|
59
|
+
exports(): {};
|
|
60
|
+
}
|
|
61
|
+
interface $$IsomorphicComponent {
|
|
62
|
+
new <TData extends Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<TData>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<TData>['props']>, ReturnType<__sveltets_Render<TData>['events']>, ReturnType<__sveltets_Render<TData>['slots']>> & {
|
|
63
|
+
$$bindings?: ReturnType<__sveltets_Render<TData>['bindings']>;
|
|
64
|
+
} & ReturnType<__sveltets_Render<TData>['exports']>;
|
|
65
|
+
<TData extends Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<TData>['props']> & {}): ReturnType<__sveltets_Render<TData>['exports']>;
|
|
66
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
67
|
+
}
|
|
68
|
+
declare const PlotLayout: $$IsomorphicComponent;
|
|
69
|
+
type PlotLayout<TData extends Record<string, unknown>> = InstanceType<typeof PlotLayout<TData>>;
|
|
70
|
+
export default PlotLayout;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Plot, RuleX, RuleY, Text } from 'svelteplot';
|
|
3
|
+
import type { Orientation, TimeValue } from '../../types/charts/common';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Purely visual time axis: a baseline rule plus the ordered step labels with
|
|
7
|
+
* the current step emphasised. It has no notion of data — it just draws the
|
|
8
|
+
* `values` it is handed and highlights `currentValue`. Colours come from CSS
|
|
9
|
+
* variables so a host app can theme it.
|
|
10
|
+
*/
|
|
11
|
+
let {
|
|
12
|
+
currentValue,
|
|
13
|
+
values,
|
|
14
|
+
orientation = 'vertical',
|
|
15
|
+
width,
|
|
16
|
+
height,
|
|
17
|
+
}: {
|
|
18
|
+
currentValue: TimeValue;
|
|
19
|
+
values: TimeValue[];
|
|
20
|
+
orientation?: Orientation;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
} = $props();
|
|
24
|
+
|
|
25
|
+
const isHorizontal = $derived(orientation === 'horizontal');
|
|
26
|
+
const crossKey = $derived(isHorizontal ? 'y' : 'x');
|
|
27
|
+
|
|
28
|
+
const domain = $derived.by(() => {
|
|
29
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
30
|
+
return [sorted[0], sorted[sorted.length - 1]];
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const baseValues = $derived(values.filter((v) => v !== currentValue));
|
|
34
|
+
const identity = (d: TimeValue) => d;
|
|
35
|
+
|
|
36
|
+
const Rule = $derived(isHorizontal ? RuleY : RuleX);
|
|
37
|
+
|
|
38
|
+
const textOffset = $derived(
|
|
39
|
+
isHorizontal
|
|
40
|
+
? { dy: -12, dx: 0, textAnchor: 'middle' as const }
|
|
41
|
+
: { dx: -8, dy: 0, textAnchor: 'end' as const },
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const axisConfig = $derived(
|
|
45
|
+
isHorizontal
|
|
46
|
+
? { x: { domain, ticks: [] }, y: { axis: false as const } }
|
|
47
|
+
: { y: { domain, ticks: [], reverse: true }, x: { axis: false as const } },
|
|
48
|
+
);
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<Plot
|
|
52
|
+
{width}
|
|
53
|
+
{height}
|
|
54
|
+
{...axisConfig}
|
|
55
|
+
marginTop={isHorizontal ? 32 : 0}
|
|
56
|
+
marginBottom={0}
|
|
57
|
+
marginLeft={isHorizontal ? 30 : 50}
|
|
58
|
+
marginRight={isHorizontal ? 30 : 0}
|
|
59
|
+
>
|
|
60
|
+
<Rule
|
|
61
|
+
{...{ [crossKey]: 0 }}
|
|
62
|
+
stroke="var(--dct-timeline-axis, currentColor)"
|
|
63
|
+
strokeOpacity={0.7}
|
|
64
|
+
strokeWidth={1.5}
|
|
65
|
+
/>
|
|
66
|
+
|
|
67
|
+
<Text
|
|
68
|
+
data={baseValues}
|
|
69
|
+
{...(isHorizontal ? { x: identity, y: 0 } : { x: 0, y: identity })}
|
|
70
|
+
text={(d: TimeValue) => `${d}`}
|
|
71
|
+
{...textOffset}
|
|
72
|
+
fontSize={12}
|
|
73
|
+
fill="var(--dct-timeline-axis, currentColor)"
|
|
74
|
+
fillOpacity={0.55}
|
|
75
|
+
/>
|
|
76
|
+
|
|
77
|
+
<Text
|
|
78
|
+
data={[currentValue]}
|
|
79
|
+
{...(isHorizontal ? { x: identity, y: 0 } : { x: 0, y: identity })}
|
|
80
|
+
text={(d: TimeValue) => `${d}`}
|
|
81
|
+
{...textOffset}
|
|
82
|
+
fontSize={20}
|
|
83
|
+
fontWeight="bold"
|
|
84
|
+
fill="var(--dct-timeline-active, #e6580d)"
|
|
85
|
+
/>
|
|
86
|
+
</Plot>
|