@faintshadow/flarecharts 26.3.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/LICENSE +40 -0
- package/README.md +103 -0
- package/dist/charts/AreaChart.svelte +150 -0
- package/dist/charts/AreaChart.svelte.d.ts +60 -0
- package/dist/charts/BarChart.svelte +142 -0
- package/dist/charts/BarChart.svelte.d.ts +58 -0
- package/dist/charts/BoxPlotChart.svelte +138 -0
- package/dist/charts/BoxPlotChart.svelte.d.ts +56 -0
- package/dist/charts/DonutChart.svelte +129 -0
- package/dist/charts/DonutChart.svelte.d.ts +73 -0
- package/dist/charts/LineChart.svelte +149 -0
- package/dist/charts/LineChart.svelte.d.ts +63 -0
- package/dist/charts/Sparkline.svelte +87 -0
- package/dist/charts/Sparkline.svelte.d.ts +40 -0
- package/dist/charts/StackChart.svelte +157 -0
- package/dist/charts/StackChart.svelte.d.ts +69 -0
- package/dist/components/Arc.svelte +202 -0
- package/dist/components/Arc.svelte.d.ts +50 -0
- package/dist/components/Area.svelte +264 -0
- package/dist/components/Area.svelte.d.ts +54 -0
- package/dist/components/Axis.svelte +139 -0
- package/dist/components/Axis.svelte.d.ts +26 -0
- package/dist/components/Bars.svelte +192 -0
- package/dist/components/Bars.svelte.d.ts +55 -0
- package/dist/components/Box.svelte +287 -0
- package/dist/components/Box.svelte.d.ts +48 -0
- package/dist/components/Chart.svelte +207 -0
- package/dist/components/Chart.svelte.d.ts +23 -0
- package/dist/components/Crosshair.svelte +67 -0
- package/dist/components/Crosshair.svelte.d.ts +14 -0
- package/dist/components/Grid.svelte +38 -0
- package/dist/components/Grid.svelte.d.ts +14 -0
- package/dist/components/Labels.svelte +61 -0
- package/dist/components/Labels.svelte.d.ts +35 -0
- package/dist/components/Legend.svelte +81 -0
- package/dist/components/Legend.svelte.d.ts +12 -0
- package/dist/components/Line.svelte +192 -0
- package/dist/components/Line.svelte.d.ts +47 -0
- package/dist/components/PlotBand.svelte +68 -0
- package/dist/components/PlotBand.svelte.d.ts +14 -0
- package/dist/components/PlotLine.svelte +54 -0
- package/dist/components/PlotLine.svelte.d.ts +16 -0
- package/dist/components/Points.svelte +179 -0
- package/dist/components/Points.svelte.d.ts +53 -0
- package/dist/components/Svg.svelte +36 -0
- package/dist/components/Svg.svelte.d.ts +8 -0
- package/dist/components/Tooltip.svelte +211 -0
- package/dist/components/Tooltip.svelte.d.ts +44 -0
- package/dist/core/bisect.d.ts +5 -0
- package/dist/core/bisect.js +23 -0
- package/dist/core/context.svelte.d.ts +140 -0
- package/dist/core/context.svelte.js +294 -0
- package/dist/core/curves.d.ts +4 -0
- package/dist/core/curves.js +13 -0
- package/dist/core/hit.d.ts +34 -0
- package/dist/core/hit.js +43 -0
- package/dist/core/keynav.d.ts +20 -0
- package/dist/core/keynav.js +41 -0
- package/dist/core/labels.d.ts +39 -0
- package/dist/core/labels.js +27 -0
- package/dist/core/merge.d.ts +17 -0
- package/dist/core/merge.js +46 -0
- package/dist/core/motion.svelte.d.ts +31 -0
- package/dist/core/motion.svelte.js +129 -0
- package/dist/core/normalize.d.ts +35 -0
- package/dist/core/normalize.js +97 -0
- package/dist/core/options.d.ts +113 -0
- package/dist/core/options.js +36 -0
- package/dist/core/palette.d.ts +8 -0
- package/dist/core/palette.js +24 -0
- package/dist/core/responsive.d.ts +6 -0
- package/dist/core/responsive.js +19 -0
- package/dist/core/scales.d.ts +31 -0
- package/dist/core/scales.js +89 -0
- package/dist/core/stack.d.ts +19 -0
- package/dist/core/stack.js +133 -0
- package/dist/core/stats.d.ts +45 -0
- package/dist/core/stats.js +114 -0
- package/dist/core/symbols.d.ts +8 -0
- package/dist/core/symbols.js +31 -0
- package/dist/core/types.d.ts +28 -0
- package/dist/core/types.js +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +42 -0
- package/package.json +81 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<script lang="ts" generics="T">
|
|
2
|
+
import { onDestroy, onMount } from 'svelte';
|
|
3
|
+
import { getChartContext } from '../core/context.svelte.js';
|
|
4
|
+
import { normalizePoints } from '../core/normalize.js';
|
|
5
|
+
import type { NormalizedPoint, XValue } from '../core/normalize.js';
|
|
6
|
+
import { motionValue } from '../core/motion.svelte.js';
|
|
7
|
+
import { hitBisectX } from '../core/hit.js';
|
|
8
|
+
import { symbolFor, symbolByIndex, symbolPath } from '../core/symbols.js';
|
|
9
|
+
import type { SymbolName } from '../core/symbols.js';
|
|
10
|
+
import type { SymbolType } from 'd3-shape';
|
|
11
|
+
import type { MarkerMode } from '../core/options.js';
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
data: readonly T[];
|
|
15
|
+
x?: (datum: T, index: number) => XValue;
|
|
16
|
+
y?: (datum: T, index: number) => number | null | undefined;
|
|
17
|
+
color?: string;
|
|
18
|
+
/** Per-point color override (wins over color/palette) — end of the precedence chain. */
|
|
19
|
+
colorFor?: (datum: T, index: number) => string | undefined;
|
|
20
|
+
name?: string;
|
|
21
|
+
/** Accessible series description (announced when keyboard focus enters the series). */
|
|
22
|
+
description?: string;
|
|
23
|
+
/** Per-point announcement override for keyboard navigation. */
|
|
24
|
+
describePoint?: (point: NormalizedPoint<T>) => string | undefined;
|
|
25
|
+
r?: number;
|
|
26
|
+
/** Halo stroke around each point (e.g. the chart background color). */
|
|
27
|
+
stroke?: string;
|
|
28
|
+
strokeWidth?: number;
|
|
29
|
+
/** Override the palette slot (e.g. to match a companion <Line>). */
|
|
30
|
+
index?: number;
|
|
31
|
+
/** Symbol shape: name string or raw d3 SymbolType. */
|
|
32
|
+
symbol?: SymbolName | SymbolType;
|
|
33
|
+
/** Symbol area in px² (default 64). */
|
|
34
|
+
symbolSize?: number;
|
|
35
|
+
/** Marker visibility mode. */
|
|
36
|
+
markers?: MarkerMode;
|
|
37
|
+
class?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let {
|
|
41
|
+
data,
|
|
42
|
+
x,
|
|
43
|
+
y,
|
|
44
|
+
color,
|
|
45
|
+
colorFor,
|
|
46
|
+
name,
|
|
47
|
+
description,
|
|
48
|
+
describePoint,
|
|
49
|
+
r = 3,
|
|
50
|
+
stroke,
|
|
51
|
+
strokeWidth = 1.5,
|
|
52
|
+
index: indexProp,
|
|
53
|
+
symbol: symbolProp,
|
|
54
|
+
symbolSize = 64,
|
|
55
|
+
markers = 'always',
|
|
56
|
+
class: klass = ''
|
|
57
|
+
}: Props = $props();
|
|
58
|
+
|
|
59
|
+
const ctx = getChartContext();
|
|
60
|
+
|
|
61
|
+
const points = $derived(normalizePoints(data, { x, y }));
|
|
62
|
+
const registration = ctx.registerSeries(() => ({
|
|
63
|
+
points,
|
|
64
|
+
name,
|
|
65
|
+
color: resolvedColor,
|
|
66
|
+
description,
|
|
67
|
+
describePoint: describePoint as ((p: NormalizedPoint<unknown>) => string | undefined) | undefined
|
|
68
|
+
}));
|
|
69
|
+
onDestroy(registration.unregister);
|
|
70
|
+
|
|
71
|
+
const seriesIndex = $derived(indexProp ?? registration.index);
|
|
72
|
+
const resolvedColor = $derived(ctx.seriesColor(seriesIndex, color));
|
|
73
|
+
const visible = $derived(ctx.isSeriesVisible(registration.index));
|
|
74
|
+
|
|
75
|
+
const resolvedSymbol = $derived(
|
|
76
|
+
symbolProp ? symbolFor(symbolProp) : symbolByIndex(seriesIndex)
|
|
77
|
+
);
|
|
78
|
+
const pathD = $derived(symbolPath(resolvedSymbol, symbolSize));
|
|
79
|
+
|
|
80
|
+
interface Dot {
|
|
81
|
+
cx: number;
|
|
82
|
+
cy: number;
|
|
83
|
+
point: NormalizedPoint<T>;
|
|
84
|
+
}
|
|
85
|
+
const dots = $derived.by((): Dot[] => {
|
|
86
|
+
if (markers === 'none') return [];
|
|
87
|
+
const out: Dot[] = [];
|
|
88
|
+
for (const p of points) {
|
|
89
|
+
const cx = ctx.xPos(p.x);
|
|
90
|
+
if (p.y == null || !Number.isFinite(cx)) continue;
|
|
91
|
+
out.push({ cx, cy: ctx.yPos(p.y), point: p });
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
});
|
|
95
|
+
const flat = $derived(dots.flatMap((d) => [d.cx, d.cy]));
|
|
96
|
+
|
|
97
|
+
const hoveredIndex = $derived.by(() => {
|
|
98
|
+
if (markers !== 'hover') return -1;
|
|
99
|
+
const ptr = ctx.hoverPointer;
|
|
100
|
+
if (!ptr) return -1;
|
|
101
|
+
const hit = hitBisectX(points, ctx.xPos, ctx.yPos, ptr.x);
|
|
102
|
+
return hit ? hit.point.index : -1;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
let rootEl: SVGGElement | undefined = $state();
|
|
106
|
+
let mounted = $state(false);
|
|
107
|
+
onMount(() => {
|
|
108
|
+
mounted = true;
|
|
109
|
+
});
|
|
110
|
+
const anim = motionValue(
|
|
111
|
+
() => flat,
|
|
112
|
+
() => flat,
|
|
113
|
+
() => rootEl
|
|
114
|
+
);
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
{#if markers !== 'none' && visible}
|
|
118
|
+
<g
|
|
119
|
+
class="fc-series fc-points {klass}"
|
|
120
|
+
class:fc-pop={mounted && markers === 'always'}
|
|
121
|
+
class:fc-markers-hover={markers === 'hover'}
|
|
122
|
+
data-name={name}
|
|
123
|
+
bind:this={rootEl}
|
|
124
|
+
>
|
|
125
|
+
{#each dots as d, i (d.point.index)}
|
|
126
|
+
<path
|
|
127
|
+
class="fc-point"
|
|
128
|
+
class:fc-marker-active={markers === 'always' || d.point.index === hoveredIndex}
|
|
129
|
+
d={pathD}
|
|
130
|
+
transform="translate({anim.current[i * 2]},{anim.current[i * 2 + 1]})"
|
|
131
|
+
fill={colorFor?.(d.point.datum, d.point.index) ?? resolvedColor}
|
|
132
|
+
stroke={stroke}
|
|
133
|
+
stroke-width={stroke ? strokeWidth : undefined}
|
|
134
|
+
/>
|
|
135
|
+
{/each}
|
|
136
|
+
</g>
|
|
137
|
+
{/if}
|
|
138
|
+
|
|
139
|
+
<style>
|
|
140
|
+
.fc-point {
|
|
141
|
+
transform-box: fill-box;
|
|
142
|
+
transform-origin: center;
|
|
143
|
+
transition: opacity var(--fc-duration-hover, 120ms) var(--fc-ease, ease);
|
|
144
|
+
}
|
|
145
|
+
/* Hover mode: dots hidden until active */
|
|
146
|
+
.fc-markers-hover .fc-point {
|
|
147
|
+
opacity: 0;
|
|
148
|
+
transition: opacity var(--fc-marker-out, 300ms) var(--fc-ease, ease);
|
|
149
|
+
}
|
|
150
|
+
.fc-markers-hover .fc-marker-active {
|
|
151
|
+
opacity: 1;
|
|
152
|
+
transition: opacity var(--fc-marker-in, 250ms) var(--fc-ease, ease);
|
|
153
|
+
}
|
|
154
|
+
/* Always mode: pop-in animation */
|
|
155
|
+
.fc-pop .fc-point {
|
|
156
|
+
animation: fc-pop-in var(--fc-duration, 500ms) var(--fc-ease, ease);
|
|
157
|
+
}
|
|
158
|
+
@keyframes fc-pop-in {
|
|
159
|
+
from {
|
|
160
|
+
opacity: 0;
|
|
161
|
+
transform: scale(0.2);
|
|
162
|
+
}
|
|
163
|
+
to {
|
|
164
|
+
opacity: 1;
|
|
165
|
+
transform: scale(1);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
:global(.fc-svg:has(.fc-series:hover)) .fc-series:not(:hover) .fc-point {
|
|
169
|
+
opacity: 0.3;
|
|
170
|
+
}
|
|
171
|
+
@media (prefers-reduced-motion: reduce) {
|
|
172
|
+
.fc-pop .fc-point {
|
|
173
|
+
animation: none;
|
|
174
|
+
}
|
|
175
|
+
.fc-point {
|
|
176
|
+
transition: none;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
</style>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { NormalizedPoint, XValue } from '../core/normalize.js';
|
|
2
|
+
import type { SymbolName } from '../core/symbols.js';
|
|
3
|
+
import type { SymbolType } from 'd3-shape';
|
|
4
|
+
import type { MarkerMode } from '../core/options.js';
|
|
5
|
+
declare function $$render<T>(): {
|
|
6
|
+
props: {
|
|
7
|
+
data: readonly T[];
|
|
8
|
+
x?: (datum: T, index: number) => XValue;
|
|
9
|
+
y?: (datum: T, index: number) => number | null | undefined;
|
|
10
|
+
color?: string;
|
|
11
|
+
/** Per-point color override (wins over color/palette) — end of the precedence chain. */
|
|
12
|
+
colorFor?: (datum: T, index: number) => string | undefined;
|
|
13
|
+
name?: string;
|
|
14
|
+
/** Accessible series description (announced when keyboard focus enters the series). */
|
|
15
|
+
description?: string;
|
|
16
|
+
/** Per-point announcement override for keyboard navigation. */
|
|
17
|
+
describePoint?: (point: NormalizedPoint<T>) => string | undefined;
|
|
18
|
+
r?: number;
|
|
19
|
+
/** Halo stroke around each point (e.g. the chart background color). */
|
|
20
|
+
stroke?: string;
|
|
21
|
+
strokeWidth?: number;
|
|
22
|
+
/** Override the palette slot (e.g. to match a companion <Line>). */
|
|
23
|
+
index?: number;
|
|
24
|
+
/** Symbol shape: name string or raw d3 SymbolType. */
|
|
25
|
+
symbol?: SymbolName | SymbolType;
|
|
26
|
+
/** Symbol area in px² (default 64). */
|
|
27
|
+
symbolSize?: number;
|
|
28
|
+
/** Marker visibility mode. */
|
|
29
|
+
markers?: MarkerMode;
|
|
30
|
+
class?: string;
|
|
31
|
+
};
|
|
32
|
+
exports: {};
|
|
33
|
+
bindings: "";
|
|
34
|
+
slots: {};
|
|
35
|
+
events: {};
|
|
36
|
+
};
|
|
37
|
+
declare class __sveltets_Render<T> {
|
|
38
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
39
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
40
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
41
|
+
bindings(): "";
|
|
42
|
+
exports(): {};
|
|
43
|
+
}
|
|
44
|
+
interface $$IsomorphicComponent {
|
|
45
|
+
new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
46
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
47
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
48
|
+
<T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
49
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
50
|
+
}
|
|
51
|
+
declare const Points: $$IsomorphicComponent;
|
|
52
|
+
type Points<T> = InstanceType<typeof Points<T>>;
|
|
53
|
+
export default Points;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { getChartContext } from '../core/context.svelte.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
class?: string;
|
|
7
|
+
children?: Snippet;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { class: klass = '', children }: Props = $props();
|
|
11
|
+
|
|
12
|
+
const ctx = getChartContext();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<!-- aria-hidden: the <Chart> group carries the label, sr-only table and keyboard
|
|
16
|
+
navigation; the SVG itself is decoration to assistive technology. -->
|
|
17
|
+
<svg
|
|
18
|
+
class="fc-svg {klass}"
|
|
19
|
+
width={ctx.width}
|
|
20
|
+
height={ctx.height}
|
|
21
|
+
viewBox="0 0 {ctx.width} {ctx.height}"
|
|
22
|
+
aria-hidden="true"
|
|
23
|
+
>
|
|
24
|
+
<g transform="translate({ctx.padding.left}, {ctx.padding.top})">
|
|
25
|
+
{@render children?.()}
|
|
26
|
+
</g>
|
|
27
|
+
</svg>
|
|
28
|
+
|
|
29
|
+
<style>
|
|
30
|
+
.fc-svg {
|
|
31
|
+
position: absolute;
|
|
32
|
+
inset: 0;
|
|
33
|
+
display: block;
|
|
34
|
+
overflow: visible;
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<script lang="ts" generics="T">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { ScaleBand } from 'd3-scale';
|
|
4
|
+
import { getChartContext } from '../core/context.svelte.js';
|
|
5
|
+
import type { SeriesEntry } from '../core/context.svelte.js';
|
|
6
|
+
import { hitBisectX, hitNearest, hitsAtBand } from '../core/hit.js';
|
|
7
|
+
import type { HitResult, TooltipData, TooltipMode, TooltipPoint } from '../core/hit.js';
|
|
8
|
+
import { bandInvert, isBandScale } from '../core/scales.js';
|
|
9
|
+
import type { XValue } from '../core/normalize.js';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/**
|
|
13
|
+
* Hit-testing mode:
|
|
14
|
+
* - 'bisect-x' (default): nearest point by x — time/linear series.
|
|
15
|
+
* - 'nearest': nearest point by euclidean distance — scatter.
|
|
16
|
+
* - 'band': the hovered category column — band x scales (falls back to bisect-x otherwise).
|
|
17
|
+
*/
|
|
18
|
+
mode?: TooltipMode;
|
|
19
|
+
/** true → one row per visible series at the anchor x; false (default) → single point. */
|
|
20
|
+
shared?: boolean;
|
|
21
|
+
/** Replace the default content. Receives typed tooltip data. */
|
|
22
|
+
children?: Snippet<[TooltipData<T>]>;
|
|
23
|
+
/** Format the x header in the default renderer. */
|
|
24
|
+
formatX?: (x: XValue) => string;
|
|
25
|
+
/** Format y values in the default renderer. */
|
|
26
|
+
formatY?: (y: number) => string;
|
|
27
|
+
class?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
mode = 'bisect-x',
|
|
32
|
+
shared = false,
|
|
33
|
+
children,
|
|
34
|
+
formatX,
|
|
35
|
+
formatY,
|
|
36
|
+
class: klass = ''
|
|
37
|
+
}: Props = $props();
|
|
38
|
+
|
|
39
|
+
const ctx = getChartContext();
|
|
40
|
+
|
|
41
|
+
function toTooltipPoint(entry: SeriesEntry, hit: HitResult): TooltipPoint<T> {
|
|
42
|
+
return {
|
|
43
|
+
x: hit.point.x,
|
|
44
|
+
y: hit.point.y,
|
|
45
|
+
datum: hit.point.datum as T,
|
|
46
|
+
pointIndex: hit.point.index,
|
|
47
|
+
seriesIndex: entry.index,
|
|
48
|
+
seriesName: entry.name,
|
|
49
|
+
color: entry.color,
|
|
50
|
+
px: hit.px,
|
|
51
|
+
py: hit.py
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const data = $derived.by((): TooltipData<T> | null => {
|
|
56
|
+
const ptr = ctx.hoverPointer;
|
|
57
|
+
if (!ptr) return null;
|
|
58
|
+
const entries = ctx.seriesEntries.filter((e) => e.visible && e.points.length > 0);
|
|
59
|
+
if (entries.length === 0) return null;
|
|
60
|
+
|
|
61
|
+
const effectiveMode: TooltipMode =
|
|
62
|
+
mode === 'band' && !isBandScale(ctx.xScale) ? 'bisect-x' : mode;
|
|
63
|
+
|
|
64
|
+
if (effectiveMode === 'band') {
|
|
65
|
+
const category = bandInvert(ctx.xScale as ScaleBand<string>, ptr.x);
|
|
66
|
+
if (category == null) return null;
|
|
67
|
+
const px = ctx.xPos(category);
|
|
68
|
+
let points: TooltipPoint<T>[] = [];
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
for (const p of hitsAtBand(entry.points, category)) {
|
|
71
|
+
points.push({
|
|
72
|
+
x: p.x,
|
|
73
|
+
y: p.y,
|
|
74
|
+
datum: p.datum as T,
|
|
75
|
+
pointIndex: p.index,
|
|
76
|
+
seriesIndex: entry.index,
|
|
77
|
+
seriesName: entry.name,
|
|
78
|
+
color: entry.color,
|
|
79
|
+
px,
|
|
80
|
+
py: ctx.yPos(p.y as number)
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (points.length === 0) return null;
|
|
85
|
+
if (!shared) {
|
|
86
|
+
points = [
|
|
87
|
+
points.reduce((a, b) => (Math.abs(a.py - ptr.y) <= Math.abs(b.py - ptr.y) ? a : b))
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
return { x: category, px, py: points[0].py, points };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (effectiveMode === 'nearest') {
|
|
94
|
+
let best: { entry: SeriesEntry; hit: HitResult } | null = null;
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const hit = hitNearest(entry.points, ctx.xPos, ctx.yPos, ptr.x, ptr.y);
|
|
97
|
+
if (hit && (!best || hit.distance < best.hit.distance)) best = { entry, hit };
|
|
98
|
+
}
|
|
99
|
+
if (!best) return null;
|
|
100
|
+
if (!shared) {
|
|
101
|
+
return {
|
|
102
|
+
x: best.hit.point.x,
|
|
103
|
+
px: best.hit.px,
|
|
104
|
+
py: best.hit.py,
|
|
105
|
+
points: [toTooltipPoint(best.entry, best.hit)]
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const points: TooltipPoint<T>[] = [];
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
const hit = hitBisectX(entry.points, ctx.xPos, ctx.yPos, best.hit.px);
|
|
111
|
+
if (hit) points.push(toTooltipPoint(entry, hit));
|
|
112
|
+
}
|
|
113
|
+
return { x: best.hit.point.x, px: best.hit.px, py: best.hit.py, points };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// bisect-x
|
|
117
|
+
let best: { entry: SeriesEntry; hit: HitResult } | null = null;
|
|
118
|
+
const candidates: { entry: SeriesEntry; hit: HitResult }[] = [];
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const hit = hitBisectX(entry.points, ctx.xPos, ctx.yPos, ptr.x);
|
|
121
|
+
if (!hit) continue;
|
|
122
|
+
candidates.push({ entry, hit });
|
|
123
|
+
if (!best || hit.distance < best.hit.distance) best = { entry, hit };
|
|
124
|
+
}
|
|
125
|
+
if (!best) return null;
|
|
126
|
+
const chosen = shared ? candidates : [best];
|
|
127
|
+
return {
|
|
128
|
+
x: best.hit.point.x,
|
|
129
|
+
px: best.hit.px,
|
|
130
|
+
py: best.hit.py,
|
|
131
|
+
points: chosen.map(({ entry, hit }) => toTooltipPoint(entry, hit))
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const positionStyle = $derived.by(() => {
|
|
136
|
+
const ptr = ctx.hoverPointer;
|
|
137
|
+
if (!data || !ptr) return '';
|
|
138
|
+
const flip = data.px > ctx.innerWidth * 0.55;
|
|
139
|
+
const left = ctx.padding.left + data.px + (flip ? -12 : 12);
|
|
140
|
+
const top = ctx.padding.top + Math.max(0, Math.min(ctx.innerHeight, ptr.y)) + 12;
|
|
141
|
+
return `left: ${left}px; top: ${top}px;${flip ? ' transform: translateX(-100%);' : ''}`;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const fmtX = $derived(
|
|
145
|
+
formatX ??
|
|
146
|
+
((x: XValue) =>
|
|
147
|
+
x instanceof Date
|
|
148
|
+
? x.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
|
|
149
|
+
: String(x))
|
|
150
|
+
);
|
|
151
|
+
const fmtY = $derived(formatY ?? ((y: number) => String(Math.round(y * 100) / 100)));
|
|
152
|
+
</script>
|
|
153
|
+
|
|
154
|
+
{#if data}
|
|
155
|
+
<div class="fc-tooltip {klass}" style={positionStyle} aria-hidden="true">
|
|
156
|
+
{#if children}
|
|
157
|
+
{@render children(data)}
|
|
158
|
+
{:else}
|
|
159
|
+
<div class="fc-tooltip-title">{fmtX(data.x)}</div>
|
|
160
|
+
{#each data.points as p (p.seriesIndex + '-' + p.pointIndex)}
|
|
161
|
+
<div class="fc-tooltip-row">
|
|
162
|
+
<span class="fc-tooltip-swatch" style="background: {p.color}"></span>
|
|
163
|
+
<span class="fc-tooltip-name">{p.seriesName ?? `Series ${p.seriesIndex + 1}`}</span>
|
|
164
|
+
<span class="fc-tooltip-value">{p.y == null ? '—' : fmtY(p.y)}</span>
|
|
165
|
+
</div>
|
|
166
|
+
{/each}
|
|
167
|
+
{/if}
|
|
168
|
+
</div>
|
|
169
|
+
{/if}
|
|
170
|
+
|
|
171
|
+
<style>
|
|
172
|
+
.fc-tooltip {
|
|
173
|
+
position: absolute;
|
|
174
|
+
z-index: 10;
|
|
175
|
+
pointer-events: none;
|
|
176
|
+
background: var(--fc-tooltip-bg, #ffffff);
|
|
177
|
+
border: 1px solid var(--fc-tooltip-border, #e2e8f0);
|
|
178
|
+
color: var(--fc-tooltip-text, #0f172a);
|
|
179
|
+
border-radius: 8px;
|
|
180
|
+
padding: 6px 10px;
|
|
181
|
+
font-family: var(--fc-font, ui-sans-serif, system-ui, sans-serif);
|
|
182
|
+
font-size: 12px;
|
|
183
|
+
line-height: 1.45;
|
|
184
|
+
box-shadow: var(--fc-tooltip-shadow, 0 2px 8px rgba(15, 23, 42, 0.08));
|
|
185
|
+
white-space: nowrap;
|
|
186
|
+
}
|
|
187
|
+
.fc-tooltip-title {
|
|
188
|
+
font-weight: 600;
|
|
189
|
+
margin-bottom: 2px;
|
|
190
|
+
}
|
|
191
|
+
.fc-tooltip-row {
|
|
192
|
+
display: flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
gap: 6px;
|
|
195
|
+
}
|
|
196
|
+
.fc-tooltip-swatch {
|
|
197
|
+
width: 8px;
|
|
198
|
+
height: 8px;
|
|
199
|
+
border-radius: 2px;
|
|
200
|
+
flex: none;
|
|
201
|
+
}
|
|
202
|
+
.fc-tooltip-name {
|
|
203
|
+
color: var(--fc-axis-label, #64748b);
|
|
204
|
+
}
|
|
205
|
+
.fc-tooltip-value {
|
|
206
|
+
margin-left: auto;
|
|
207
|
+
padding-left: 10px;
|
|
208
|
+
font-variant-numeric: tabular-nums;
|
|
209
|
+
font-family: var(--fc-font-mono, ui-monospace, monospace);
|
|
210
|
+
}
|
|
211
|
+
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { TooltipData, TooltipMode } from '../core/hit.js';
|
|
3
|
+
import type { XValue } from '../core/normalize.js';
|
|
4
|
+
declare function $$render<T>(): {
|
|
5
|
+
props: {
|
|
6
|
+
/**
|
|
7
|
+
* Hit-testing mode:
|
|
8
|
+
* - 'bisect-x' (default): nearest point by x — time/linear series.
|
|
9
|
+
* - 'nearest': nearest point by euclidean distance — scatter.
|
|
10
|
+
* - 'band': the hovered category column — band x scales (falls back to bisect-x otherwise).
|
|
11
|
+
*/
|
|
12
|
+
mode?: TooltipMode;
|
|
13
|
+
/** true → one row per visible series at the anchor x; false (default) → single point. */
|
|
14
|
+
shared?: boolean;
|
|
15
|
+
/** Replace the default content. Receives typed tooltip data. */
|
|
16
|
+
children?: Snippet<[TooltipData<T>]>;
|
|
17
|
+
/** Format the x header in the default renderer. */
|
|
18
|
+
formatX?: (x: XValue) => string;
|
|
19
|
+
/** Format y values in the default renderer. */
|
|
20
|
+
formatY?: (y: number) => string;
|
|
21
|
+
class?: string;
|
|
22
|
+
};
|
|
23
|
+
exports: {};
|
|
24
|
+
bindings: "";
|
|
25
|
+
slots: {};
|
|
26
|
+
events: {};
|
|
27
|
+
};
|
|
28
|
+
declare class __sveltets_Render<T> {
|
|
29
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
30
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
31
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
32
|
+
bindings(): "";
|
|
33
|
+
exports(): {};
|
|
34
|
+
}
|
|
35
|
+
interface $$IsomorphicComponent {
|
|
36
|
+
new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
37
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
38
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
39
|
+
<T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
40
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
41
|
+
}
|
|
42
|
+
declare const Tooltip: $$IsomorphicComponent;
|
|
43
|
+
type Tooltip<T> = InstanceType<typeof Tooltip<T>>;
|
|
44
|
+
export default Tooltip;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nearest-value binary search over an ASCENDING array of pixel positions.
|
|
3
|
+
* The hot path for tooltip/crosshair hit-testing: O(log n) per series per move.
|
|
4
|
+
*/
|
|
5
|
+
export function bisectNearest(values, target) {
|
|
6
|
+
const n = values.length;
|
|
7
|
+
if (n === 0)
|
|
8
|
+
return -1;
|
|
9
|
+
if (target <= values[0])
|
|
10
|
+
return 0;
|
|
11
|
+
if (target >= values[n - 1])
|
|
12
|
+
return n - 1;
|
|
13
|
+
let lo = 0;
|
|
14
|
+
let hi = n - 1;
|
|
15
|
+
while (hi - lo > 1) {
|
|
16
|
+
const mid = (lo + hi) >> 1;
|
|
17
|
+
if (values[mid] <= target)
|
|
18
|
+
lo = mid;
|
|
19
|
+
else
|
|
20
|
+
hi = mid;
|
|
21
|
+
}
|
|
22
|
+
return target - values[lo] <= values[hi] - target ? lo : hi;
|
|
23
|
+
}
|