@cfasim-ui/charts 0.7.8 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BarChart/BarChart.d.ts +9 -26
- package/dist/ChartMenu/ChartMenu.d.ts +3 -2
- package/dist/ChartTooltip/ChartTooltip.d.ts +9 -12
- package/dist/ChoroplethMap/ChoroplethMap.d.ts +42 -190
- package/dist/ChoroplethMap/ChoroplethTooltip.d.ts +20 -27
- package/dist/ChoroplethMap/canvasLayer.d.ts +23 -6
- package/dist/ChoroplethMap/cityLayout.d.ts +3 -2
- package/dist/ChoroplethMap/mixedGeo.d.ts +74 -0
- package/dist/ChoroplethMap/mixedGeo.test.d.ts +1 -0
- package/dist/DataTable/DataTable.d.ts +3 -2
- package/dist/LineChart/LineChart.d.ts +9 -26
- package/dist/_shared/ChartAnnotations.d.ts +3 -2
- package/dist/_shared/ChartZoomControls.d.ts +3 -2
- package/dist/_shared/index.d.ts +2 -1
- package/dist/_shared/mapTheme.d.ts +159 -0
- package/dist/_shared/mapTheme.test.d.ts +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1821 -1462
- package/docs/BarChart.md +776 -0
- package/docs/ChoroplethMap.md +1394 -0
- package/docs/DataTable.md +386 -0
- package/docs/LineChart.md +1267 -0
- package/docs/index.json +56 -0
- package/package.json +25 -21
- package/src/BarChart/BarChart.md +743 -0
- package/src/BarChart/BarChart.vue +1901 -0
- package/src/ChartMenu/ChartMenu.vue +220 -0
- package/src/ChartMenu/download.ts +120 -0
- package/src/ChartTooltip/ChartTooltip.vue +97 -0
- package/src/ChoroplethMap/ChoroplethMap.md +1354 -0
- package/src/ChoroplethMap/ChoroplethMap.vue +3778 -0
- package/src/ChoroplethMap/ChoroplethTooltip.vue +103 -0
- package/src/ChoroplethMap/canvasLayer.ts +373 -0
- package/src/ChoroplethMap/cityLayout.ts +262 -0
- package/src/ChoroplethMap/hsaMapping.ts +4116 -0
- package/src/ChoroplethMap/mixedGeo.ts +201 -0
- package/src/DataTable/DataTable.md +372 -0
- package/src/DataTable/DataTable.vue +406 -0
- package/src/LineChart/LineChart.md +1225 -0
- package/src/LineChart/LineChart.vue +1555 -0
- package/src/_shared/ChartAnnotations.vue +420 -0
- package/src/_shared/ChartZoomControls.vue +138 -0
- package/src/_shared/annotations.ts +106 -0
- package/src/_shared/axes.ts +69 -0
- package/src/_shared/chartProps.ts +201 -0
- package/src/_shared/computeTicks.ts +42 -0
- package/src/_shared/contrast.ts +100 -0
- package/src/_shared/dateAxis.ts +501 -0
- package/src/_shared/index.ts +78 -0
- package/src/_shared/mapTheme.ts +551 -0
- package/src/_shared/scale.ts +86 -0
- package/src/_shared/seriesCsv.ts +68 -0
- package/src/_shared/touch.ts +8 -0
- package/src/_shared/useChartFoundation.ts +175 -0
- package/src/_shared/useChartFullscreen.ts +254 -0
- package/src/_shared/useChartMenu.ts +111 -0
- package/src/_shared/useChartPadding.ts +235 -0
- package/src/_shared/useChartSize.ts +58 -0
- package/src/_shared/useChartTooltip.ts +205 -0
- package/src/env.d.ts +4 -0
- package/src/hsa-mapping.ts +1 -0
- package/src/index.ts +41 -0
- package/src/tooltip-position.ts +55 -0
- package/src/us-cities/data.ts +1371 -0
- package/src/us-cities/index.ts +122 -0
- package/src/us-cities.ts +7 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
DropdownMenuRoot,
|
|
4
|
+
DropdownMenuTrigger,
|
|
5
|
+
DropdownMenuPortal,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
} from "reka-ui";
|
|
9
|
+
|
|
10
|
+
export interface ChartMenuItem {
|
|
11
|
+
label: string;
|
|
12
|
+
action: () => void;
|
|
13
|
+
/** Sets aria-pressed on the menu item — use for toggle-style items. */
|
|
14
|
+
ariaPressed?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(
|
|
18
|
+
defineProps<{
|
|
19
|
+
items: ChartMenuItem[];
|
|
20
|
+
/** Force the dropdown style even when only one item is provided. */
|
|
21
|
+
forceDropdown?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* When the chart is expanded, the menu trigger is replaced by a
|
|
24
|
+
* persistent close (✕) button that emits `close`.
|
|
25
|
+
*/
|
|
26
|
+
isFullscreen?: boolean;
|
|
27
|
+
}>(),
|
|
28
|
+
{ forceDropdown: false, isFullscreen: false },
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const emit = defineEmits<{ (e: "close"): void }>();
|
|
32
|
+
|
|
33
|
+
const useDropdown = () => props.forceDropdown || props.items.length > 1;
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<div
|
|
38
|
+
class="chart-menu-trigger-area"
|
|
39
|
+
:class="{ 'chart-menu-trigger-area--expanded': isFullscreen }"
|
|
40
|
+
>
|
|
41
|
+
<!-- Expanded: a close button replaces the menu trigger -->
|
|
42
|
+
<button
|
|
43
|
+
v-if="isFullscreen"
|
|
44
|
+
class="chart-menu-button chart-close-button"
|
|
45
|
+
aria-label="Collapse"
|
|
46
|
+
@click="emit('close')"
|
|
47
|
+
>
|
|
48
|
+
<svg
|
|
49
|
+
width="16"
|
|
50
|
+
height="16"
|
|
51
|
+
viewBox="0 0 16 16"
|
|
52
|
+
fill="none"
|
|
53
|
+
stroke="currentColor"
|
|
54
|
+
stroke-width="1.75"
|
|
55
|
+
stroke-linecap="round"
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
>
|
|
58
|
+
<path d="M4 4l8 8M12 4l-8 8" />
|
|
59
|
+
</svg>
|
|
60
|
+
</button>
|
|
61
|
+
<!-- Single item: plain button -->
|
|
62
|
+
<button
|
|
63
|
+
v-else-if="!useDropdown()"
|
|
64
|
+
class="chart-menu-button chart-menu-single"
|
|
65
|
+
:aria-label="items[0].label"
|
|
66
|
+
@click="items[0].action"
|
|
67
|
+
>
|
|
68
|
+
<svg
|
|
69
|
+
width="14"
|
|
70
|
+
height="14"
|
|
71
|
+
viewBox="0 0 14 14"
|
|
72
|
+
fill="none"
|
|
73
|
+
stroke="currentColor"
|
|
74
|
+
stroke-width="1.5"
|
|
75
|
+
stroke-linecap="round"
|
|
76
|
+
stroke-linejoin="round"
|
|
77
|
+
aria-hidden="true"
|
|
78
|
+
>
|
|
79
|
+
<path d="M7 1v8M3 6l4 4 4-4M2 13h10" />
|
|
80
|
+
</svg>
|
|
81
|
+
</button>
|
|
82
|
+
<!-- Multiple items: dropdown menu -->
|
|
83
|
+
<DropdownMenuRoot v-else :modal="false">
|
|
84
|
+
<DropdownMenuTrigger class="chart-menu-button" aria-label="Chart options">
|
|
85
|
+
<svg
|
|
86
|
+
width="16"
|
|
87
|
+
height="16"
|
|
88
|
+
viewBox="0 0 16 16"
|
|
89
|
+
fill="currentColor"
|
|
90
|
+
aria-hidden="true"
|
|
91
|
+
>
|
|
92
|
+
<circle cx="3" cy="8" r="1.5" />
|
|
93
|
+
<circle cx="8" cy="8" r="1.5" />
|
|
94
|
+
<circle cx="13" cy="8" r="1.5" />
|
|
95
|
+
</svg>
|
|
96
|
+
</DropdownMenuTrigger>
|
|
97
|
+
<DropdownMenuPortal>
|
|
98
|
+
<DropdownMenuContent
|
|
99
|
+
class="chart-menu-content"
|
|
100
|
+
:side-offset="4"
|
|
101
|
+
align="end"
|
|
102
|
+
>
|
|
103
|
+
<DropdownMenuItem
|
|
104
|
+
v-for="item in items"
|
|
105
|
+
:key="item.label"
|
|
106
|
+
class="chart-menu-item"
|
|
107
|
+
:aria-pressed="item.ariaPressed"
|
|
108
|
+
@select="item.action"
|
|
109
|
+
>
|
|
110
|
+
{{ item.label }}
|
|
111
|
+
</DropdownMenuItem>
|
|
112
|
+
</DropdownMenuContent>
|
|
113
|
+
</DropdownMenuPortal>
|
|
114
|
+
</DropdownMenuRoot>
|
|
115
|
+
</div>
|
|
116
|
+
</template>
|
|
117
|
+
|
|
118
|
+
<style scoped>
|
|
119
|
+
.chart-menu-trigger-area {
|
|
120
|
+
position: absolute;
|
|
121
|
+
top: 0.5em;
|
|
122
|
+
right: 0.5em;
|
|
123
|
+
z-index: 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* In fullscreen the button anchors to the viewport corner (the wrapper's
|
|
127
|
+
content padding doesn't move an absolutely-positioned child), so inset it
|
|
128
|
+
further for modal-style breathing room. */
|
|
129
|
+
.chart-menu-trigger-area--expanded {
|
|
130
|
+
top: 1.25em;
|
|
131
|
+
right: 1.25em;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.chart-menu-button {
|
|
135
|
+
display: flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
justify-content: center;
|
|
138
|
+
width: 28px;
|
|
139
|
+
height: 28px;
|
|
140
|
+
border: 1px solid var(--color-border);
|
|
141
|
+
border-radius: 0.25em;
|
|
142
|
+
background: var(--color-bg-0, #fff);
|
|
143
|
+
color: var(--color-text-secondary);
|
|
144
|
+
cursor: pointer;
|
|
145
|
+
opacity: 0;
|
|
146
|
+
transition: opacity 0.15s;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.chart-menu-button[data-state="open"] {
|
|
150
|
+
opacity: 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* The close button is always visible while the chart is expanded. */
|
|
154
|
+
.chart-close-button {
|
|
155
|
+
opacity: 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.chart-menu-button:hover {
|
|
159
|
+
background: var(--color-bg-1, rgba(0, 0, 0, 0.05));
|
|
160
|
+
color: var(--color-text);
|
|
161
|
+
}
|
|
162
|
+
</style>
|
|
163
|
+
|
|
164
|
+
<style>
|
|
165
|
+
/* Reveal the menu button when the user hovers/focuses anywhere in the
|
|
166
|
+
chart. These reference the parent wrapper classes, so they can't be
|
|
167
|
+
scoped to ChartMenu. (In fullscreen the menu button is swapped for the
|
|
168
|
+
always-visible close button, so no rule is needed there.) */
|
|
169
|
+
.line-chart-wrapper:hover .chart-menu-button,
|
|
170
|
+
.bar-chart-wrapper:hover .chart-menu-button,
|
|
171
|
+
.choropleth-wrapper:hover .chart-menu-button,
|
|
172
|
+
.line-chart-wrapper:focus-within .chart-menu-button,
|
|
173
|
+
.bar-chart-wrapper:focus-within .chart-menu-button,
|
|
174
|
+
.choropleth-wrapper:focus-within .chart-menu-button {
|
|
175
|
+
opacity: 1;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* Visually-hidden live region used by each chart wrapper to announce
|
|
179
|
+
expansion. Lives here so it ships once with the shared menu. */
|
|
180
|
+
.chart-sr-only {
|
|
181
|
+
position: absolute;
|
|
182
|
+
width: 1px;
|
|
183
|
+
height: 1px;
|
|
184
|
+
padding: 0;
|
|
185
|
+
margin: -1px;
|
|
186
|
+
overflow: hidden;
|
|
187
|
+
clip: rect(0, 0, 0, 0);
|
|
188
|
+
white-space: nowrap;
|
|
189
|
+
border: 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.chart-menu-content {
|
|
193
|
+
z-index: 100;
|
|
194
|
+
background: var(--color-bg-0);
|
|
195
|
+
border: 1px solid var(--color-border);
|
|
196
|
+
border-radius: 0.25em;
|
|
197
|
+
padding: 0.25em;
|
|
198
|
+
box-shadow:
|
|
199
|
+
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
200
|
+
0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
|
201
|
+
min-width: 140px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.chart-menu-item {
|
|
205
|
+
display: flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
padding: 0.375em 0.5em;
|
|
208
|
+
border-radius: 0.25em;
|
|
209
|
+
font-size: var(--font-size-sm);
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
user-select: none;
|
|
212
|
+
outline: none;
|
|
213
|
+
white-space: nowrap;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.chart-menu-item[data-highlighted] {
|
|
217
|
+
background: var(--color-primary);
|
|
218
|
+
color: white;
|
|
219
|
+
}
|
|
220
|
+
</style>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export function downloadBlob(blob: Blob, name: string) {
|
|
2
|
+
const url = URL.createObjectURL(blob);
|
|
3
|
+
const a = document.createElement("a");
|
|
4
|
+
a.href = url;
|
|
5
|
+
a.download = name;
|
|
6
|
+
a.click();
|
|
7
|
+
URL.revokeObjectURL(url);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Inherited CSS properties propagated onto the cloned SVG root so text and
|
|
11
|
+
* `currentColor` strokes render with the same color/typography as the
|
|
12
|
+
* live chart when the SVG is opened standalone or rasterized to PNG. */
|
|
13
|
+
const ROOT_INHERITED_PROPS = [
|
|
14
|
+
"color",
|
|
15
|
+
"font-family",
|
|
16
|
+
"font-size",
|
|
17
|
+
"font-weight",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
/** Presentation attributes that may contain `var(--…)` references. The
|
|
21
|
+
* fallback inside `var(--name, #fff)` is the only thing that renders when
|
|
22
|
+
* the custom property is undefined outside the page context — so we
|
|
23
|
+
* resolve them against the document's CSS custom properties before
|
|
24
|
+
* serializing. */
|
|
25
|
+
const VAR_RESOLVABLE_ATTRS = ["fill", "stroke"] as const;
|
|
26
|
+
|
|
27
|
+
/** Resolve a single `var(--name)` or `var(--name, fallback)` expression.
|
|
28
|
+
* Looks the name up on `document.documentElement` (where theme tokens are
|
|
29
|
+
* declared); falls back to the in-expression fallback, then the original
|
|
30
|
+
* expression if neither resolves. */
|
|
31
|
+
function resolveVarExpression(expr: string): string {
|
|
32
|
+
const match = expr.match(
|
|
33
|
+
/^\s*var\(\s*(--[\w-]+)\s*(?:,\s*([^)]*?)\s*)?\)\s*$/,
|
|
34
|
+
);
|
|
35
|
+
if (!match) return expr;
|
|
36
|
+
const [, name, fallback] = match;
|
|
37
|
+
const value = window
|
|
38
|
+
.getComputedStyle(document.documentElement)
|
|
39
|
+
.getPropertyValue(name)
|
|
40
|
+
.trim();
|
|
41
|
+
if (value) return value;
|
|
42
|
+
if (fallback) return fallback.trim();
|
|
43
|
+
return expr;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Clone `svg` and inline the styles that don't survive a standalone render:
|
|
47
|
+
* inherited `color`/`font-*` on the root (so `currentColor` and unset
|
|
48
|
+
* font-family resolve), and any `var(--…)` references in fill/stroke
|
|
49
|
+
* attributes resolved against the document. */
|
|
50
|
+
export function prepareSvgForExport(svg: SVGSVGElement): SVGSVGElement {
|
|
51
|
+
const clone = svg.cloneNode(true) as SVGSVGElement;
|
|
52
|
+
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
53
|
+
|
|
54
|
+
const rootComputed = window.getComputedStyle(svg);
|
|
55
|
+
const rootStyleParts: string[] = [];
|
|
56
|
+
for (const prop of ROOT_INHERITED_PROPS) {
|
|
57
|
+
const value = rootComputed.getPropertyValue(prop);
|
|
58
|
+
if (value) rootStyleParts.push(`${prop}: ${value}`);
|
|
59
|
+
}
|
|
60
|
+
const existingRootStyle = clone.getAttribute("style") ?? "";
|
|
61
|
+
clone.setAttribute(
|
|
62
|
+
"style",
|
|
63
|
+
[existingRootStyle, ...rootStyleParts].filter(Boolean).join("; "),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
for (const target of clone.querySelectorAll<SVGElement>("*")) {
|
|
67
|
+
for (const attr of VAR_RESOLVABLE_ATTRS) {
|
|
68
|
+
const raw = target.getAttribute(attr);
|
|
69
|
+
if (!raw || !raw.includes("var(")) continue;
|
|
70
|
+
target.setAttribute(attr, resolveVarExpression(raw));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return clone;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function saveSvg(svg: SVGSVGElement, filename: string) {
|
|
78
|
+
const clone = prepareSvgForExport(svg);
|
|
79
|
+
const xml = new XMLSerializer().serializeToString(clone);
|
|
80
|
+
downloadBlob(new Blob([xml], { type: "image/svg+xml" }), `${filename}.svg`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function savePng(svg: SVGSVGElement, filename: string) {
|
|
84
|
+
const clone = prepareSvgForExport(svg);
|
|
85
|
+
const xml = new XMLSerializer().serializeToString(clone);
|
|
86
|
+
const svgBlob = new Blob([xml], { type: "image/svg+xml;charset=utf-8" });
|
|
87
|
+
const url = URL.createObjectURL(svgBlob);
|
|
88
|
+
const img = new Image();
|
|
89
|
+
const w = svg.width.baseVal.value || svg.clientWidth;
|
|
90
|
+
const h = svg.height.baseVal.value || svg.clientHeight;
|
|
91
|
+
const scale = 2;
|
|
92
|
+
img.onload = () => {
|
|
93
|
+
const canvas = document.createElement("canvas");
|
|
94
|
+
canvas.width = w * scale;
|
|
95
|
+
canvas.height = h * scale;
|
|
96
|
+
const ctx = canvas.getContext("2d")!;
|
|
97
|
+
ctx.scale(scale, scale);
|
|
98
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
99
|
+
canvas.toBlob((blob) => {
|
|
100
|
+
if (blob) downloadBlob(blob, `${filename}.png`);
|
|
101
|
+
});
|
|
102
|
+
URL.revokeObjectURL(url);
|
|
103
|
+
};
|
|
104
|
+
img.src = url;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Downloads a rendering canvas's current contents as a PNG — no SVG
|
|
109
|
+
* round-trip. The canvas backing store is already sized at
|
|
110
|
+
* devicePixelRatio, so the export is naturally high-resolution.
|
|
111
|
+
*/
|
|
112
|
+
export function saveCanvasPng(canvas: HTMLCanvasElement, filename: string) {
|
|
113
|
+
canvas.toBlob((blob) => {
|
|
114
|
+
if (blob) downloadBlob(blob, `${filename}.png`);
|
|
115
|
+
}, "image/png");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function downloadCsv(csv: string, filename: string) {
|
|
119
|
+
downloadBlob(new Blob([csv], { type: "text/csv" }), `${filename}.csv`);
|
|
120
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
PopoverRoot,
|
|
4
|
+
PopoverAnchor,
|
|
5
|
+
PopoverPortal,
|
|
6
|
+
PopoverContent,
|
|
7
|
+
} from "reka-ui";
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(
|
|
10
|
+
defineProps<{
|
|
11
|
+
/** Pixel x-coordinate relative to the positioned parent container */
|
|
12
|
+
x: number;
|
|
13
|
+
/** Pixel y-coordinate relative to the positioned parent container */
|
|
14
|
+
y: number;
|
|
15
|
+
/** Whether the tooltip is visible */
|
|
16
|
+
open: boolean;
|
|
17
|
+
/** Interaction mode. Default: 'hover' */
|
|
18
|
+
mode?: "hover" | "click";
|
|
19
|
+
/** Offset from anchor in pixels. Default: 8 */
|
|
20
|
+
sideOffset?: number;
|
|
21
|
+
}>(),
|
|
22
|
+
{
|
|
23
|
+
mode: "hover",
|
|
24
|
+
sideOffset: 16,
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
defineEmits<{
|
|
29
|
+
/** Fired in click mode when user dismisses the tooltip */
|
|
30
|
+
(e: "close"): void;
|
|
31
|
+
}>();
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<!-- Hover mode: lightweight CSS-positioned div -->
|
|
36
|
+
<div
|
|
37
|
+
v-if="mode === 'hover' && open"
|
|
38
|
+
class="chart-tooltip-content"
|
|
39
|
+
:style="{
|
|
40
|
+
position: 'absolute',
|
|
41
|
+
left: `${x + sideOffset}px`,
|
|
42
|
+
top: `${y}px`,
|
|
43
|
+
transform: 'translateY(-50%)',
|
|
44
|
+
}"
|
|
45
|
+
>
|
|
46
|
+
<slot />
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- Click mode: reka-ui Popover for dismiss-on-outside-click -->
|
|
50
|
+
<template v-else-if="mode === 'click'">
|
|
51
|
+
<PopoverRoot :open="open">
|
|
52
|
+
<PopoverAnchor as-child>
|
|
53
|
+
<div
|
|
54
|
+
class="chart-tooltip-anchor"
|
|
55
|
+
:style="{ left: `${x}px`, top: `${y}px` }"
|
|
56
|
+
/>
|
|
57
|
+
</PopoverAnchor>
|
|
58
|
+
<PopoverPortal>
|
|
59
|
+
<PopoverContent
|
|
60
|
+
v-if="open"
|
|
61
|
+
class="chart-tooltip-content"
|
|
62
|
+
side="right"
|
|
63
|
+
:side-offset="sideOffset"
|
|
64
|
+
update-position-strategy="always"
|
|
65
|
+
@pointer-down-outside="$emit('close')"
|
|
66
|
+
@escape-key-down="$emit('close')"
|
|
67
|
+
>
|
|
68
|
+
<slot />
|
|
69
|
+
</PopoverContent>
|
|
70
|
+
</PopoverPortal>
|
|
71
|
+
</PopoverRoot>
|
|
72
|
+
</template>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
<style scoped>
|
|
76
|
+
.chart-tooltip-anchor {
|
|
77
|
+
position: absolute;
|
|
78
|
+
width: 1px;
|
|
79
|
+
height: 1px;
|
|
80
|
+
pointer-events: none;
|
|
81
|
+
}
|
|
82
|
+
</style>
|
|
83
|
+
|
|
84
|
+
<style>
|
|
85
|
+
.chart-tooltip-content {
|
|
86
|
+
z-index: 100;
|
|
87
|
+
background: var(--color-bg-0, #fff);
|
|
88
|
+
border: 1px solid var(--color-border, #e5e7eb);
|
|
89
|
+
border-radius: 0.375em;
|
|
90
|
+
padding: 0.5em 0.75em;
|
|
91
|
+
font-size: var(--font-size-sm, 0.875rem);
|
|
92
|
+
box-shadow:
|
|
93
|
+
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
94
|
+
0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
|
95
|
+
pointer-events: none;
|
|
96
|
+
}
|
|
97
|
+
</style>
|