astro-component-kit 0.3.0 → 0.4.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/README.md +159 -176
- package/package.json +2 -2
- package/src/components/index.ts +16 -202
- package/src/components/lib/buttons/NeonBorderButton.astro +162 -0
- package/src/components/lib/buttons/RippleButton.astro +199 -0
- package/src/components/lib/buttons/SlideRevealButton.astro +159 -0
- package/src/components/lib/buttons/index.ts +4 -0
- package/src/components/lib/cards/GithubRepoCard.astro +176 -0
- package/src/components/lib/cards/PricingCompareMiniCard.astro +196 -0
- package/src/components/lib/cards/StatsTrendCard.astro +149 -0
- package/src/components/lib/cards/index.ts +3 -0
- package/src/components/lib/charts/ActivityRings.astro +105 -0
- package/src/components/lib/charts/BoxPlotChart.astro +109 -0
- package/src/components/lib/charts/BubbleChart.astro +101 -0
- package/src/components/lib/charts/BulletChart.astro +160 -0
- package/src/components/lib/charts/BumpChart.astro +92 -0
- package/src/components/lib/charts/CandlestickChart.astro +105 -0
- package/src/components/lib/charts/ChartFrame.astro +373 -0
- package/src/components/lib/charts/DivergingBarChart.astro +98 -0
- package/src/components/lib/charts/DumbbellChart.astro +92 -0
- package/src/components/lib/charts/ForecastLineChart.astro +125 -0
- package/src/components/lib/charts/FunnelChart.astro +122 -0
- package/src/components/lib/charts/GaugeChart.astro +111 -0
- package/src/components/lib/charts/GoalProgressChart.astro +97 -0
- package/src/components/lib/charts/GroupedBarChart.astro +125 -0
- package/src/components/lib/charts/HeatmapChart.astro +131 -0
- package/src/components/lib/charts/HistogramChart.astro +111 -0
- package/src/components/lib/charts/HorizontalBarChart.astro +155 -0
- package/src/components/lib/charts/KpiStatTile.astro +116 -0
- package/src/components/lib/charts/KpiTrendGrid.astro +60 -0
- package/src/components/lib/charts/LineChart.astro +160 -0
- package/src/components/lib/charts/LinearProgressGauge.astro +136 -0
- package/src/components/lib/charts/LollipopChart.astro +100 -0
- package/src/components/lib/charts/MeterGauge.astro +122 -0
- package/src/components/lib/charts/MetricDeltaTable.astro +125 -0
- package/src/components/lib/charts/PieChart.astro +135 -0
- package/src/components/lib/charts/PolarAreaChart.astro +85 -0
- package/src/components/lib/charts/PopulationPyramid.astro +122 -0
- package/src/components/lib/charts/PunchCardChart.astro +89 -0
- package/src/components/lib/charts/RadarChart.astro +94 -0
- package/src/components/lib/charts/RadarMetricChart.astro +168 -0
- package/src/components/lib/charts/RadialBarChart.astro +124 -0
- package/src/components/lib/charts/RangeAreaChart.astro +113 -0
- package/src/components/lib/charts/RetentionCohortTable.astro +126 -0
- package/src/components/lib/charts/SankeyChart.astro +128 -0
- package/src/components/lib/charts/ScatterPlot.astro +139 -0
- package/src/components/lib/charts/SegmentedBarChart.astro +119 -0
- package/src/components/lib/charts/SlopeChart.astro +105 -0
- package/src/components/lib/charts/SparklineChart.astro +87 -0
- package/src/components/lib/charts/StackedAreaChart.astro +130 -0
- package/src/components/lib/charts/StackedBarChart.astro +136 -0
- package/src/components/lib/charts/SunburstChart.astro +103 -0
- package/src/components/lib/charts/TreemapChart.astro +94 -0
- package/src/components/lib/charts/WaffleChart.astro +121 -0
- package/src/components/lib/charts/WaterfallChart.astro +118 -0
- package/src/components/lib/charts/chart-tooltip.ts +75 -0
- package/src/components/lib/charts/chart-utils.ts +271 -0
- package/src/components/lib/charts/index.ts +46 -0
- package/src/components/lib/data/ActivityHeatmapGrid.astro +157 -0
- package/src/components/lib/data/MetricComparisonRow.astro +109 -0
- package/src/components/lib/data/StatusTimelineFeed.astro +168 -0
- package/src/components/lib/data/index.ts +3 -0
- package/src/components/lib/feedback/ProgressRingCounter.astro +118 -0
- package/src/components/lib/feedback/ToastNotificationGroup.astro +173 -0
- package/src/components/lib/feedback/index.ts +2 -0
- package/src/components/lib/forms/GlassInlineToggleGroup.astro +161 -0
- package/src/components/lib/forms/PasswordStrengthInput.astro +242 -0
- package/src/components/lib/forms/PhoneCountryInput.astro +145 -0
- package/src/components/lib/forms/TagsAutocompleteInput.astro +249 -0
- package/src/components/lib/forms/index.ts +5 -0
- package/src/components/lib/layout/DiagonalSectionDivider.astro +69 -0
- package/src/components/lib/layout/HolyGrailGrid.astro +125 -0
- package/src/components/lib/layout/index.ts +3 -1
- package/src/components/lib/navigation/QuickJumpDock.astro +125 -0
- package/src/components/lib/navigation/StepBreadcrumbBar.astro +151 -0
- package/src/components/lib/navigation/SubNavbarPills.astro +102 -0
- package/src/components/lib/navigation/index.ts +3 -0
- package/src/components/lib/overlays/CookiePreferencesDrawer.astro +226 -0
- package/src/components/lib/overlays/FloatingFeedbackWidget.astro +203 -0
- package/src/components/lib/overlays/ImageCompareSlider.astro +172 -0
- package/src/components/lib/overlays/index.ts +3 -0
- package/src/components/lib/sections/BlogGridSection.astro +131 -0
- package/src/components/lib/sections/CareersOpenings.astro +139 -0
- package/src/components/lib/sections/ChangelogSection.astro +144 -0
- package/src/components/lib/sections/ContactSplitMap.astro +139 -0
- package/src/components/lib/sections/CtaNewsletterSection.astro +163 -0
- package/src/components/lib/sections/CtaSplitImage.astro +147 -0
- package/src/components/lib/sections/FaqTwoColumn.astro +119 -0
- package/src/components/lib/sections/FeatureAlternatingRows.astro +190 -0
- package/src/components/lib/sections/FeatureIconGrid.astro +155 -0
- package/src/components/lib/sections/FeatureTabsShowcase.astro +180 -0
- package/src/components/lib/sections/FooterMegaSection.astro +155 -0
- package/src/components/lib/sections/HeroDashboardPreview.astro +180 -0
- package/src/components/lib/sections/HeroMinimalCentered.astro +175 -0
- package/src/components/lib/sections/HeroShowcaseSplit.astro +304 -0
- package/src/components/lib/sections/HeroTerminal.astro +251 -0
- package/src/components/lib/sections/HeroWaitlist.astro +229 -0
- package/src/components/lib/sections/HowItWorksSteps.astro +178 -0
- package/src/components/lib/sections/IntegrationsGrid.astro +167 -0
- package/src/components/lib/sections/LogoCloudSection.astro +96 -0
- package/src/components/lib/sections/LogoWallGrid.astro +91 -0
- package/src/components/lib/sections/PricingSinglePlan.astro +157 -0
- package/src/components/lib/sections/PricingToggleCards.astro +171 -0
- package/src/components/lib/sections/StatsCounterBand.astro +122 -0
- package/src/components/lib/sections/StatsDashboardSection.astro +83 -0
- package/src/components/lib/sections/TestimonialCarousel.astro +165 -0
- package/src/components/lib/sections/TestimonialFeatured.astro +120 -0
- package/src/components/lib/sections/TestimonialGridSection.astro +188 -0
- package/src/components/lib/sections/index.ts +29 -0
- package/src/components/lib/typography/AnimatedCounterText.astro +102 -0
- package/src/components/lib/typography/CurvedPathHeading.astro +86 -0
- package/src/components/lib/typography/GradientTextMarquee.astro +105 -0
- package/src/components/lib/typography/HackerDecodeText.astro +85 -0
- package/src/components/lib/typography/index.ts +4 -0
- package/src/styles/global.css +18 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* WaffleChart — 10×10 grid of squares where each square is 1% of the whole.
|
|
4
|
+
* An intuitive part-to-whole view for percentages.
|
|
5
|
+
*
|
|
6
|
+
* @param {Array<{label: string, value: number}>} parts - Parts of the whole (rounded to 100 cells).
|
|
7
|
+
* @param {number} columns - Cells per row (default 10).
|
|
8
|
+
* @param {string[]} colors - Optional per-part colors.
|
|
9
|
+
* @param {string} variant - Card style (glass|solid|ghost).
|
|
10
|
+
* @param {string} size - Density (sm|md|lg).
|
|
11
|
+
*/
|
|
12
|
+
import ChartFrame from './ChartFrame.astro';
|
|
13
|
+
import { colorAt, sum } from './chart-utils';
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
title?: string;
|
|
17
|
+
subtitle?: string;
|
|
18
|
+
parts?: Array<{ label: string; value: number }>;
|
|
19
|
+
columns?: number;
|
|
20
|
+
colors?: string[];
|
|
21
|
+
variant?: 'glass' | 'solid' | 'ghost';
|
|
22
|
+
size?: 'sm' | 'md' | 'lg';
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
title = 'Device share',
|
|
28
|
+
subtitle = 'Each square = 1% of sessions',
|
|
29
|
+
parts = [
|
|
30
|
+
{ label: 'Desktop', value: 54 },
|
|
31
|
+
{ label: 'Mobile', value: 38 },
|
|
32
|
+
{ label: 'Tablet', value: 8 },
|
|
33
|
+
],
|
|
34
|
+
columns = 10,
|
|
35
|
+
colors,
|
|
36
|
+
variant = 'glass',
|
|
37
|
+
size = 'md',
|
|
38
|
+
className = '',
|
|
39
|
+
} = Astro.props;
|
|
40
|
+
|
|
41
|
+
const total = sum(parts.map((p) => p.value)) || 1;
|
|
42
|
+
// Largest-remainder rounding so cells always add up to exactly 100.
|
|
43
|
+
const raw = parts.map((p) => (p.value / total) * 100);
|
|
44
|
+
const counts = raw.map(Math.floor);
|
|
45
|
+
let remaining = 100 - sum(counts);
|
|
46
|
+
raw
|
|
47
|
+
.map((v, i) => ({ i, r: v - Math.floor(v) }))
|
|
48
|
+
.sort((a, b) => b.r - a.r)
|
|
49
|
+
.forEach(({ i }) => {
|
|
50
|
+
if (remaining > 0) {
|
|
51
|
+
counts[i] += 1;
|
|
52
|
+
remaining -= 1;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
const cells: number[] = counts.flatMap((c, i) => Array.from({ length: c }, () => i));
|
|
56
|
+
const items = parts.slice(0, 8).map((p, i) => ({ ...p, pct: counts[i], color: colorAt(i, colors) }));
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
<ChartFrame
|
|
60
|
+
title={title}
|
|
61
|
+
subtitle={subtitle}
|
|
62
|
+
variant={variant}
|
|
63
|
+
size={size}
|
|
64
|
+
className={className}
|
|
65
|
+
table={{ headers: ['Part', 'Share'], rows: items.map((p) => [p.label, `${p.pct}%`]) }}
|
|
66
|
+
>
|
|
67
|
+
<div class="wf">
|
|
68
|
+
<div class="wf__grid" style={`--wf-cols:${columns}`} role="img" aria-label={`${title}: ${items.map((p) => `${p.label} ${p.pct}%`).join(', ')}`}>
|
|
69
|
+
{cells.map((pi, idx) => (
|
|
70
|
+
<span class="wf__cell" style={`background:${items[pi]?.color}; --wf-i:${idx}`} data-tip-title={items[pi]?.label} data-tip={`${items[pi]?.color}::${items[pi]?.pct}%`}></span>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
<ul class="wf__legend">
|
|
74
|
+
{items.map((p) => (
|
|
75
|
+
<li>
|
|
76
|
+
<span class="wf__big">{p.pct}<small>%</small></span>
|
|
77
|
+
<span class="wf__name"><i style={`background:${p.color}`} aria-hidden="true"></i>{p.label}</span>
|
|
78
|
+
</li>
|
|
79
|
+
))}
|
|
80
|
+
</ul>
|
|
81
|
+
</div>
|
|
82
|
+
</ChartFrame>
|
|
83
|
+
|
|
84
|
+
<style>
|
|
85
|
+
.wf {
|
|
86
|
+
display: grid;
|
|
87
|
+
grid-template-columns: minmax(0, 260px) 1fr;
|
|
88
|
+
gap: 2rem;
|
|
89
|
+
align-items: center;
|
|
90
|
+
}
|
|
91
|
+
.wf__grid {
|
|
92
|
+
display: grid;
|
|
93
|
+
grid-template-columns: repeat(var(--wf-cols), 1fr);
|
|
94
|
+
gap: 3px;
|
|
95
|
+
}
|
|
96
|
+
.wf__cell {
|
|
97
|
+
aspect-ratio: 1;
|
|
98
|
+
border-radius: 3px;
|
|
99
|
+
animation: wf-in 0.4s ease both;
|
|
100
|
+
animation-delay: calc(var(--wf-i) * 6ms);
|
|
101
|
+
}
|
|
102
|
+
.wf__cell:hover { filter: brightness(1.25); }
|
|
103
|
+
@keyframes wf-in { from { opacity: 0; transform: scale(0.4); } }
|
|
104
|
+
.wf__legend { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
|
|
105
|
+
.wf__legend li { display: flex; flex-direction: column; }
|
|
106
|
+
.wf__big {
|
|
107
|
+
font-size: 1.75rem;
|
|
108
|
+
font-weight: 750;
|
|
109
|
+
color: var(--c-text-1, #fff);
|
|
110
|
+
line-height: 1.1;
|
|
111
|
+
font-variant-numeric: tabular-nums;
|
|
112
|
+
}
|
|
113
|
+
.wf__big small { font-size: 0.9rem; color: var(--c-text-2, #a3a3a3); margin-left: 2px; }
|
|
114
|
+
.wf__name { display: flex; align-items: center; gap: 0.45rem; font-size: 0.8125rem; color: var(--c-text-2, #a3a3a3); }
|
|
115
|
+
.wf__name i { width: 10px; height: 10px; border-radius: 3px; }
|
|
116
|
+
@media (max-width: 560px) {
|
|
117
|
+
.wf { grid-template-columns: 1fr; }
|
|
118
|
+
.wf__legend { grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); }
|
|
119
|
+
}
|
|
120
|
+
@media (prefers-reduced-motion: reduce) { .wf__cell { animation: none; } }
|
|
121
|
+
</style>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* WaterfallChart — Shows how a starting value is built up or eroded by a
|
|
4
|
+
* sequence of positive and negative steps, ending in a total.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} startLabel - Label for the opening bar.
|
|
7
|
+
* @param {number} start - Opening value.
|
|
8
|
+
* @param {Array<{label: string, value: number}>} steps - Signed changes.
|
|
9
|
+
* @param {string} endLabel - Label for the closing total bar.
|
|
10
|
+
* @param {string} prefix - Value prefix.
|
|
11
|
+
* @param {string} suffix - Value suffix.
|
|
12
|
+
* @param {string} variant - Card style (glass|solid|ghost).
|
|
13
|
+
* @param {string} size - Density (sm|md|lg).
|
|
14
|
+
*/
|
|
15
|
+
import ChartFrame from './ChartFrame.astro';
|
|
16
|
+
import { scaleLinear, niceTicks, formatValue, POSITIVE, NEGATIVE } from './chart-utils';
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
title?: string;
|
|
20
|
+
subtitle?: string;
|
|
21
|
+
startLabel?: string;
|
|
22
|
+
start?: number;
|
|
23
|
+
steps?: Array<{ label: string; value: number }>;
|
|
24
|
+
endLabel?: string;
|
|
25
|
+
prefix?: string;
|
|
26
|
+
suffix?: string;
|
|
27
|
+
height?: number;
|
|
28
|
+
variant?: 'glass' | 'solid' | 'ghost';
|
|
29
|
+
size?: 'sm' | 'md' | 'lg';
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
title = 'ARR bridge',
|
|
35
|
+
subtitle = 'From January to June',
|
|
36
|
+
startLabel = 'Jan ARR',
|
|
37
|
+
start = 1200000,
|
|
38
|
+
steps = [
|
|
39
|
+
{ label: 'New', value: 380000 },
|
|
40
|
+
{ label: 'Expansion', value: 210000 },
|
|
41
|
+
{ label: 'Contraction', value: -90000 },
|
|
42
|
+
{ label: 'Churn', value: -160000 },
|
|
43
|
+
{ label: 'Reactivation', value: 45000 },
|
|
44
|
+
],
|
|
45
|
+
endLabel = 'Jun ARR',
|
|
46
|
+
prefix = '$',
|
|
47
|
+
suffix = '',
|
|
48
|
+
height = 290,
|
|
49
|
+
variant = 'glass',
|
|
50
|
+
size = 'md',
|
|
51
|
+
className = '',
|
|
52
|
+
} = Astro.props;
|
|
53
|
+
|
|
54
|
+
const W = 640;
|
|
55
|
+
const H = height;
|
|
56
|
+
const m = { t: 22, r: 12, b: 30, l: 52 };
|
|
57
|
+
let run = start;
|
|
58
|
+
const bars = [
|
|
59
|
+
{ label: startLabel, from: 0, to: start, kind: 'total' as const, value: start },
|
|
60
|
+
...steps.map((s) => {
|
|
61
|
+
const from = run;
|
|
62
|
+
run += s.value;
|
|
63
|
+
return { label: s.label, from, to: run, kind: s.value >= 0 ? ('up' as const) : ('down' as const), value: s.value };
|
|
64
|
+
}),
|
|
65
|
+
{ label: endLabel, from: 0, to: run, kind: 'total' as const, value: run },
|
|
66
|
+
];
|
|
67
|
+
const ticks = niceTicks(0, Math.max(...bars.map((b) => Math.max(b.from, b.to))), 4);
|
|
68
|
+
const y = scaleLinear(0, ticks[ticks.length - 1], H - m.b, m.t);
|
|
69
|
+
const band = (W - m.l - m.r) / bars.length;
|
|
70
|
+
const bw = band * 0.62;
|
|
71
|
+
const colorOf = (k: string) => (k === 'up' ? POSITIVE : k === 'down' ? NEGATIVE : 'var(--chart-1, #3987e5)');
|
|
72
|
+
const fmt = (v: number) => formatValue(v, prefix, suffix);
|
|
73
|
+
const fmtSigned = (v: number) => `${v >= 0 ? '+' : '−'}${formatValue(Math.abs(v), prefix, suffix)}`;
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
<ChartFrame
|
|
77
|
+
title={title}
|
|
78
|
+
subtitle={subtitle}
|
|
79
|
+
variant={variant}
|
|
80
|
+
size={size}
|
|
81
|
+
className={className}
|
|
82
|
+
legend={[
|
|
83
|
+
{ label: 'Total', color: 'var(--chart-1, #3987e5)' },
|
|
84
|
+
{ label: 'Increase', color: POSITIVE },
|
|
85
|
+
{ label: 'Decrease', color: NEGATIVE },
|
|
86
|
+
]}
|
|
87
|
+
table={{ headers: ['Step', 'Change', 'Running total'], rows: bars.map((b) => [b.label, b.kind === 'total' ? '—' : fmtSigned(b.value), fmt(b.to)]) }}
|
|
88
|
+
>
|
|
89
|
+
<svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: waterfall chart from ${fmt(start)} to ${fmt(run)}`}>
|
|
90
|
+
{ticks.map((t) => (
|
|
91
|
+
<g>
|
|
92
|
+
<line class={t === 0 ? 'cf-axis' : 'cf-grid'} x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} />
|
|
93
|
+
<text class="cf-label" x={m.l - 8} y={y(t) + 4} text-anchor="end">{fmt(t)}</text>
|
|
94
|
+
</g>
|
|
95
|
+
))}
|
|
96
|
+
{bars.map((b, i) => {
|
|
97
|
+
const bx = m.l + i * band + (band - bw) / 2;
|
|
98
|
+
const top = y(Math.max(b.from, b.to));
|
|
99
|
+
const h = Math.max(1, Math.abs(y(b.from) - y(b.to)));
|
|
100
|
+
const c = colorOf(b.kind);
|
|
101
|
+
const next = bars[i + 1];
|
|
102
|
+
return (
|
|
103
|
+
<g>
|
|
104
|
+
{next && <line class="cf-axis" x1={bx + bw} x2={bx + band} y1={y(b.to)} y2={y(b.to)} opacity="0.7" />}
|
|
105
|
+
<rect class="cf-mark cf-dim" x={bx} y={top} width={bw} height={h} rx="3" fill={c}
|
|
106
|
+
tabindex="0" role="img"
|
|
107
|
+
aria-label={`${b.label}: ${b.kind === 'total' ? fmt(b.value) : fmtSigned(b.value)}, total ${fmt(b.to)}`}
|
|
108
|
+
data-tip-title={b.label}
|
|
109
|
+
data-tip={b.kind === 'total' ? `${c}::Total: ${fmt(b.value)}` : `${c}::Change: ${fmtSigned(b.value)}|Running: ${fmt(b.to)}`} />
|
|
110
|
+
<text class="cf-label cf-label--strong" x={bx + bw / 2} y={top - 6} text-anchor="middle">
|
|
111
|
+
{b.kind === 'total' ? fmt(b.value) : fmtSigned(b.value)}
|
|
112
|
+
</text>
|
|
113
|
+
<text class="cf-label" x={bx + bw / 2} y={H - 10} text-anchor="middle">{b.label}</text>
|
|
114
|
+
</g>
|
|
115
|
+
);
|
|
116
|
+
})}
|
|
117
|
+
</svg>
|
|
118
|
+
</ChartFrame>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// src/components/lib/charts/chart-tooltip.ts
|
|
2
|
+
// Tiny, dependency-free tooltip controller shared by every SVG chart.
|
|
3
|
+
// Any element inside a `[data-chart]` root that carries `data-tip` becomes a
|
|
4
|
+
// hover/focus target. Lines in `data-tip` are separated by "|"; an optional
|
|
5
|
+
// `data-tip-title` renders as the bold heading.
|
|
6
|
+
|
|
7
|
+
function show(root: HTMLElement, tip: HTMLElement, target: Element, clientX?: number, clientY?: number) {
|
|
8
|
+
const title = target.getAttribute('data-tip-title') || '';
|
|
9
|
+
const body = (target.getAttribute('data-tip') || '').split('|').filter(Boolean);
|
|
10
|
+
tip.replaceChildren();
|
|
11
|
+
if (title) {
|
|
12
|
+
const h = document.createElement('strong');
|
|
13
|
+
h.textContent = title;
|
|
14
|
+
tip.appendChild(h);
|
|
15
|
+
}
|
|
16
|
+
body.forEach((line) => {
|
|
17
|
+
const row = document.createElement('span');
|
|
18
|
+
const [swatch, text] = line.includes('::') ? line.split('::') : ['', line];
|
|
19
|
+
if (swatch) {
|
|
20
|
+
const dot = document.createElement('i');
|
|
21
|
+
dot.style.background = swatch;
|
|
22
|
+
row.appendChild(dot);
|
|
23
|
+
}
|
|
24
|
+
row.appendChild(document.createTextNode(text));
|
|
25
|
+
tip.appendChild(row);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const box = root.getBoundingClientRect();
|
|
29
|
+
let x: number;
|
|
30
|
+
let y: number;
|
|
31
|
+
if (clientX !== undefined && clientY !== undefined) {
|
|
32
|
+
x = clientX - box.left;
|
|
33
|
+
y = clientY - box.top;
|
|
34
|
+
} else {
|
|
35
|
+
const r = target.getBoundingClientRect();
|
|
36
|
+
x = r.left + r.width / 2 - box.left;
|
|
37
|
+
y = r.top - box.top;
|
|
38
|
+
}
|
|
39
|
+
tip.style.opacity = '1';
|
|
40
|
+
const tw = tip.offsetWidth;
|
|
41
|
+
const th = tip.offsetHeight;
|
|
42
|
+
const left = Math.min(Math.max(8, x + 14), box.width - tw - 8);
|
|
43
|
+
const top = y - th - 12 < 4 ? y + 18 : y - th - 12;
|
|
44
|
+
tip.style.transform = `translate(${left}px, ${top}px)`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hide(tip: HTMLElement) {
|
|
48
|
+
tip.style.opacity = '0';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function initChartTooltips() {
|
|
52
|
+
document.querySelectorAll<HTMLElement>('[data-chart]:not([data-chart-ready])').forEach((root) => {
|
|
53
|
+
root.setAttribute('data-chart-ready', '');
|
|
54
|
+
const tip = root.querySelector<HTMLElement>('[data-chart-tip]');
|
|
55
|
+
if (!tip) return;
|
|
56
|
+
|
|
57
|
+
root.addEventListener('pointermove', (e) => {
|
|
58
|
+
const target = (e.target as Element).closest('[data-tip]');
|
|
59
|
+
if (target && root.contains(target)) show(root, tip, target, e.clientX, e.clientY);
|
|
60
|
+
else hide(tip);
|
|
61
|
+
});
|
|
62
|
+
root.addEventListener('pointerleave', () => hide(tip));
|
|
63
|
+
root.addEventListener('focusin', (e) => {
|
|
64
|
+
const target = (e.target as Element).closest('[data-tip]');
|
|
65
|
+
if (target) show(root, tip, target);
|
|
66
|
+
});
|
|
67
|
+
root.addEventListener('focusout', () => hide(tip));
|
|
68
|
+
root.addEventListener('keydown', (e) => {
|
|
69
|
+
if (e.key === 'Escape') hide(tip);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
initChartTooltips();
|
|
75
|
+
document.addEventListener('astro:page-load', initChartTooltips);
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// src/components/lib/charts/chart-utils.ts
|
|
2
|
+
// Shared, dependency-free math helpers for the SVG chart components.
|
|
3
|
+
// Everything here runs at build time (server-side) — no client JS needed.
|
|
4
|
+
|
|
5
|
+
/** Fixed-order categorical palette (CSS tokens with hex fallbacks). Never cycled past 8. */
|
|
6
|
+
export const CHART_COLORS = [
|
|
7
|
+
'var(--chart-1, #3987e5)',
|
|
8
|
+
'var(--chart-2, #d95926)',
|
|
9
|
+
'var(--chart-3, #199e70)',
|
|
10
|
+
'var(--chart-4, #c98500)',
|
|
11
|
+
'var(--chart-5, #d55181)',
|
|
12
|
+
'var(--chart-6, #2f9e2f)',
|
|
13
|
+
'var(--chart-7, #9085e9)',
|
|
14
|
+
'var(--chart-8, #e66767)',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export const POSITIVE = 'var(--chart-pos, #34c38f)';
|
|
18
|
+
export const NEGATIVE = 'var(--chart-neg, #f06a6a)';
|
|
19
|
+
|
|
20
|
+
/** Returns the color for a series index, honouring user overrides. */
|
|
21
|
+
export function colorAt(i: number, overrides?: string[]): string {
|
|
22
|
+
if (overrides && overrides[i]) return overrides[i];
|
|
23
|
+
return CHART_COLORS[Math.min(i, CHART_COLORS.length - 1)];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Linear scale factory: maps a domain [d0, d1] onto a range [r0, r1]. */
|
|
27
|
+
export function scaleLinear(d0: number, d1: number, r0: number, r1: number) {
|
|
28
|
+
const span = d1 - d0 || 1;
|
|
29
|
+
return (v: number) => r0 + ((v - d0) / span) * (r1 - r0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Rounds a raw step to a "nice" 1/2/2.5/5/10 × 10^n value. */
|
|
33
|
+
function niceStep(raw: number): number {
|
|
34
|
+
const exp = Math.floor(Math.log10(Math.abs(raw) || 1));
|
|
35
|
+
const f = raw / 10 ** exp;
|
|
36
|
+
const nf = f <= 1 ? 1 : f <= 2 ? 2 : f <= 2.5 ? 2.5 : f <= 5 ? 5 : 10;
|
|
37
|
+
return nf * 10 ** exp;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Produces nice, evenly spaced axis ticks covering [min, max]. */
|
|
41
|
+
export function niceTicks(min: number, max: number, count = 5): number[] {
|
|
42
|
+
if (min === max) {
|
|
43
|
+
max = min + 1;
|
|
44
|
+
}
|
|
45
|
+
const step = niceStep((max - min) / Math.max(1, count));
|
|
46
|
+
const start = Math.floor(min / step) * step;
|
|
47
|
+
const end = Math.ceil(max / step) * step;
|
|
48
|
+
const ticks: number[] = [];
|
|
49
|
+
for (let v = start; v <= end + step / 2; v += step) {
|
|
50
|
+
ticks.push(Number(v.toFixed(10)));
|
|
51
|
+
}
|
|
52
|
+
return ticks;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Compact number formatter: 1200 → 1.2k, 3400000 → 3.4M. */
|
|
56
|
+
export function formatCompact(v: number, decimals = 1): string {
|
|
57
|
+
const abs = Math.abs(v);
|
|
58
|
+
if (abs >= 1e9) return `${(v / 1e9).toFixed(decimals).replace(/\.0+$/, '')}B`;
|
|
59
|
+
if (abs >= 1e6) return `${(v / 1e6).toFixed(decimals).replace(/\.0+$/, '')}M`;
|
|
60
|
+
if (abs >= 1e3) return `${(v / 1e3).toFixed(decimals).replace(/\.0+$/, '')}k`;
|
|
61
|
+
return `${Number(v.toFixed(2))}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Formats a value with an optional prefix/suffix (e.g. '$', '%'). */
|
|
65
|
+
export function formatValue(v: number, prefix = '', suffix = '', compact = true): string {
|
|
66
|
+
const body = compact ? formatCompact(v) : v.toLocaleString('en-US');
|
|
67
|
+
return `${prefix}${body}${suffix}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type Point = [number, number];
|
|
71
|
+
|
|
72
|
+
/** Straight polyline path. */
|
|
73
|
+
export function linePath(points: Point[]): string {
|
|
74
|
+
return points.map(([x, y], i) => `${i ? 'L' : 'M'}${x.toFixed(2)},${y.toFixed(2)}`).join(' ');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Step path (horizontal then vertical). */
|
|
78
|
+
export function stepPath(points: Point[]): string {
|
|
79
|
+
if (!points.length) return '';
|
|
80
|
+
let d = `M${points[0][0].toFixed(2)},${points[0][1].toFixed(2)}`;
|
|
81
|
+
for (let i = 1; i < points.length; i++) {
|
|
82
|
+
const [x, y] = points[i];
|
|
83
|
+
d += ` H${x.toFixed(2)} V${y.toFixed(2)}`;
|
|
84
|
+
}
|
|
85
|
+
return d;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Monotone cubic interpolation — smooth curve that never overshoots the data. */
|
|
89
|
+
export function smoothPath(points: Point[]): string {
|
|
90
|
+
const n = points.length;
|
|
91
|
+
if (n < 3) return linePath(points);
|
|
92
|
+
const dx: number[] = [];
|
|
93
|
+
const slope: number[] = [];
|
|
94
|
+
for (let i = 0; i < n - 1; i++) {
|
|
95
|
+
dx[i] = points[i + 1][0] - points[i][0];
|
|
96
|
+
slope[i] = (points[i + 1][1] - points[i][1]) / (dx[i] || 1);
|
|
97
|
+
}
|
|
98
|
+
const m: number[] = [slope[0]];
|
|
99
|
+
for (let i = 1; i < n - 1; i++) {
|
|
100
|
+
m[i] = slope[i - 1] * slope[i] <= 0 ? 0 : (slope[i - 1] + slope[i]) / 2;
|
|
101
|
+
}
|
|
102
|
+
m[n - 1] = slope[n - 2];
|
|
103
|
+
for (let i = 0; i < n - 1; i++) {
|
|
104
|
+
if (slope[i] === 0) {
|
|
105
|
+
m[i] = 0;
|
|
106
|
+
m[i + 1] = 0;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const a = m[i] / slope[i];
|
|
110
|
+
const b = m[i + 1] / slope[i];
|
|
111
|
+
const h = a * a + b * b;
|
|
112
|
+
if (h > 9) {
|
|
113
|
+
const t = 3 / Math.sqrt(h);
|
|
114
|
+
m[i] = t * a * slope[i];
|
|
115
|
+
m[i + 1] = t * b * slope[i];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
let d = `M${points[0][0].toFixed(2)},${points[0][1].toFixed(2)}`;
|
|
119
|
+
for (let i = 0; i < n - 1; i++) {
|
|
120
|
+
const [x0, y0] = points[i];
|
|
121
|
+
const [x1, y1] = points[i + 1];
|
|
122
|
+
const h = dx[i] / 3;
|
|
123
|
+
d += ` C${(x0 + h).toFixed(2)},${(y0 + m[i] * h).toFixed(2)} ${(x1 - h).toFixed(2)},${(y1 - m[i + 1] * h).toFixed(2)} ${x1.toFixed(2)},${y1.toFixed(2)}`;
|
|
124
|
+
}
|
|
125
|
+
return d;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Picks the path builder for a curve type. */
|
|
129
|
+
export function curvePath(points: Point[], curve: 'linear' | 'smooth' | 'step' = 'smooth'): string {
|
|
130
|
+
if (curve === 'step') return stepPath(points);
|
|
131
|
+
if (curve === 'linear') return linePath(points);
|
|
132
|
+
return smoothPath(points);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Closes a line path down to a baseline to form an area. */
|
|
136
|
+
export function areaPath(points: Point[], baseline: number, curve: 'linear' | 'smooth' | 'step' = 'smooth'): string {
|
|
137
|
+
if (!points.length) return '';
|
|
138
|
+
const top = curvePath(points, curve);
|
|
139
|
+
const last = points[points.length - 1];
|
|
140
|
+
const first = points[0];
|
|
141
|
+
return `${top} L${last[0].toFixed(2)},${baseline.toFixed(2)} L${first[0].toFixed(2)},${baseline.toFixed(2)} Z`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Area between an upper and lower line (for bands / stacked areas). */
|
|
145
|
+
export function bandPath(upper: Point[], lower: Point[], curve: 'linear' | 'smooth' | 'step' = 'smooth'): string {
|
|
146
|
+
if (!upper.length) return '';
|
|
147
|
+
const top = curvePath(upper, curve);
|
|
148
|
+
const reversed = [...lower].reverse();
|
|
149
|
+
const bottom = curvePath(reversed, curve).replace(/^M/, 'L');
|
|
150
|
+
return `${top} ${bottom} Z`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Polar → cartesian, with 0 rad at 12 o'clock and clockwise rotation. */
|
|
154
|
+
export function polar(cx: number, cy: number, r: number, angle: number): Point {
|
|
155
|
+
return [cx + r * Math.sin(angle), cy - r * Math.cos(angle)];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Annular sector path (donut slice). Angles in radians, 0 at 12 o'clock. */
|
|
159
|
+
export function arcPath(cx: number, cy: number, rOuter: number, rInner: number, a0: number, a1: number): string {
|
|
160
|
+
const sweep = a1 - a0;
|
|
161
|
+
if (sweep >= Math.PI * 2 - 1e-6) {
|
|
162
|
+
// Full ring: draw two halves to avoid a degenerate arc.
|
|
163
|
+
const mid = a0 + Math.PI;
|
|
164
|
+
return `${arcPath(cx, cy, rOuter, rInner, a0, mid)} ${arcPath(cx, cy, rOuter, rInner, mid, a0 + Math.PI * 2)}`;
|
|
165
|
+
}
|
|
166
|
+
const large = sweep > Math.PI ? 1 : 0;
|
|
167
|
+
const [x0, y0] = polar(cx, cy, rOuter, a0);
|
|
168
|
+
const [x1, y1] = polar(cx, cy, rOuter, a1);
|
|
169
|
+
if (rInner <= 0) {
|
|
170
|
+
return `M${cx},${cy} L${x0.toFixed(2)},${y0.toFixed(2)} A${rOuter},${rOuter} 0 ${large} 1 ${x1.toFixed(2)},${y1.toFixed(2)} Z`;
|
|
171
|
+
}
|
|
172
|
+
const [x2, y2] = polar(cx, cy, rInner, a1);
|
|
173
|
+
const [x3, y3] = polar(cx, cy, rInner, a0);
|
|
174
|
+
return [
|
|
175
|
+
`M${x0.toFixed(2)},${y0.toFixed(2)}`,
|
|
176
|
+
`A${rOuter},${rOuter} 0 ${large} 1 ${x1.toFixed(2)},${y1.toFixed(2)}`,
|
|
177
|
+
`L${x2.toFixed(2)},${y2.toFixed(2)}`,
|
|
178
|
+
`A${rInner},${rInner} 0 ${large} 0 ${x3.toFixed(2)},${y3.toFixed(2)}`,
|
|
179
|
+
'Z',
|
|
180
|
+
].join(' ');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Open arc stroke path (for gauges / rings). */
|
|
184
|
+
export function arcStroke(cx: number, cy: number, r: number, a0: number, a1: number): string {
|
|
185
|
+
const sweep = Math.min(a1 - a0, Math.PI * 2 - 1e-4);
|
|
186
|
+
const large = sweep > Math.PI ? 1 : 0;
|
|
187
|
+
const [x0, y0] = polar(cx, cy, r, a0);
|
|
188
|
+
const [x1, y1] = polar(cx, cy, r, a0 + sweep);
|
|
189
|
+
return `M${x0.toFixed(2)},${y0.toFixed(2)} A${r},${r} 0 ${large} 1 ${x1.toFixed(2)},${y1.toFixed(2)}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Sum helper. */
|
|
193
|
+
export function sum(values: number[]): number {
|
|
194
|
+
return values.reduce((a, b) => a + (Number.isFinite(b) ? b : 0), 0);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Quantile on a sorted array (linear interpolation). */
|
|
198
|
+
export function quantile(sorted: number[], q: number): number {
|
|
199
|
+
if (!sorted.length) return 0;
|
|
200
|
+
const pos = (sorted.length - 1) * q;
|
|
201
|
+
const lo = Math.floor(pos);
|
|
202
|
+
const hi = Math.ceil(pos);
|
|
203
|
+
return sorted[lo] + (sorted[hi] - sorted[lo]) * (pos - lo);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Unique, stable id for SVG gradients/clip paths so multiple charts can coexist. */
|
|
207
|
+
let uid = 0;
|
|
208
|
+
export function chartId(prefix = 'chart'): string {
|
|
209
|
+
uid += 1;
|
|
210
|
+
return `${prefix}-${uid}-${Math.random().toString(36).slice(2, 7)}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface Rect {
|
|
214
|
+
x: number;
|
|
215
|
+
y: number;
|
|
216
|
+
w: number;
|
|
217
|
+
h: number;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Squarified treemap layout (Bruls et al.) for a list of positive values. */
|
|
221
|
+
export function squarify(values: number[], box: Rect): Rect[] {
|
|
222
|
+
const total = sum(values);
|
|
223
|
+
if (!total) return values.map(() => ({ x: box.x, y: box.y, w: 0, h: 0 }));
|
|
224
|
+
const area = box.w * box.h;
|
|
225
|
+
const items = values.map((v, i) => ({ i, a: (v / total) * area })).sort((a, b) => b.a - a.a);
|
|
226
|
+
const out: Rect[] = new Array(values.length);
|
|
227
|
+
let rect = { ...box };
|
|
228
|
+
let row: typeof items = [];
|
|
229
|
+
|
|
230
|
+
const worst = (r: typeof items, side: number) => {
|
|
231
|
+
const s = r.reduce((acc, it) => acc + it.a, 0);
|
|
232
|
+
const max = Math.max(...r.map((it) => it.a));
|
|
233
|
+
const min = Math.min(...r.map((it) => it.a));
|
|
234
|
+
return Math.max((side * side * max) / (s * s), (s * s) / (side * side * min));
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const layoutRow = (r: typeof items) => {
|
|
238
|
+
const s = r.reduce((acc, it) => acc + it.a, 0);
|
|
239
|
+
if (rect.w >= rect.h) {
|
|
240
|
+
const w = s / rect.h;
|
|
241
|
+
let y = rect.y;
|
|
242
|
+
r.forEach((it) => {
|
|
243
|
+
const h = it.a / w;
|
|
244
|
+
out[it.i] = { x: rect.x, y, w, h };
|
|
245
|
+
y += h;
|
|
246
|
+
});
|
|
247
|
+
rect = { x: rect.x + w, y: rect.y, w: rect.w - w, h: rect.h };
|
|
248
|
+
} else {
|
|
249
|
+
const h = s / rect.w;
|
|
250
|
+
let x = rect.x;
|
|
251
|
+
r.forEach((it) => {
|
|
252
|
+
const w = it.a / h;
|
|
253
|
+
out[it.i] = { x, y: rect.y, w, h };
|
|
254
|
+
x += w;
|
|
255
|
+
});
|
|
256
|
+
rect = { x: rect.x, y: rect.y + h, w: rect.w, h: rect.h - h };
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
for (const it of items) {
|
|
261
|
+
const side = Math.min(rect.w, rect.h);
|
|
262
|
+
if (!row.length || worst([...row, it], side) <= worst(row, side)) {
|
|
263
|
+
row.push(it);
|
|
264
|
+
} else {
|
|
265
|
+
layoutRow(row);
|
|
266
|
+
row = [it];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (row.length) layoutRow(row);
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
@@ -4,3 +4,49 @@
|
|
|
4
4
|
export { default as AreaChart } from './AreaChart.astro';
|
|
5
5
|
export { default as BarChart } from './BarChart.astro';
|
|
6
6
|
export { default as DonutChart } from './DonutChart.astro';
|
|
7
|
+
export { default as RadarMetricChart } from './RadarMetricChart.astro';
|
|
8
|
+
export { default as LinearProgressGauge } from './LinearProgressGauge.astro';
|
|
9
|
+
|
|
10
|
+
// Zero-dependency SVG charts & statistics
|
|
11
|
+
export { default as ActivityRings } from './ActivityRings.astro';
|
|
12
|
+
export { default as BoxPlotChart } from './BoxPlotChart.astro';
|
|
13
|
+
export { default as BubbleChart } from './BubbleChart.astro';
|
|
14
|
+
export { default as BulletChart } from './BulletChart.astro';
|
|
15
|
+
export { default as BumpChart } from './BumpChart.astro';
|
|
16
|
+
export { default as CandlestickChart } from './CandlestickChart.astro';
|
|
17
|
+
export { default as ChartFrame } from './ChartFrame.astro';
|
|
18
|
+
export { default as DivergingBarChart } from './DivergingBarChart.astro';
|
|
19
|
+
export { default as DumbbellChart } from './DumbbellChart.astro';
|
|
20
|
+
export { default as ForecastLineChart } from './ForecastLineChart.astro';
|
|
21
|
+
export { default as FunnelChart } from './FunnelChart.astro';
|
|
22
|
+
export { default as GaugeChart } from './GaugeChart.astro';
|
|
23
|
+
export { default as GoalProgressChart } from './GoalProgressChart.astro';
|
|
24
|
+
export { default as GroupedBarChart } from './GroupedBarChart.astro';
|
|
25
|
+
export { default as HeatmapChart } from './HeatmapChart.astro';
|
|
26
|
+
export { default as HistogramChart } from './HistogramChart.astro';
|
|
27
|
+
export { default as HorizontalBarChart } from './HorizontalBarChart.astro';
|
|
28
|
+
export { default as KpiStatTile } from './KpiStatTile.astro';
|
|
29
|
+
export { default as KpiTrendGrid } from './KpiTrendGrid.astro';
|
|
30
|
+
export { default as LineChart } from './LineChart.astro';
|
|
31
|
+
export { default as LollipopChart } from './LollipopChart.astro';
|
|
32
|
+
export { default as MeterGauge } from './MeterGauge.astro';
|
|
33
|
+
export { default as MetricDeltaTable } from './MetricDeltaTable.astro';
|
|
34
|
+
export { default as PieChart } from './PieChart.astro';
|
|
35
|
+
export { default as PolarAreaChart } from './PolarAreaChart.astro';
|
|
36
|
+
export { default as PopulationPyramid } from './PopulationPyramid.astro';
|
|
37
|
+
export { default as PunchCardChart } from './PunchCardChart.astro';
|
|
38
|
+
export { default as RadarChart } from './RadarChart.astro';
|
|
39
|
+
export { default as RadialBarChart } from './RadialBarChart.astro';
|
|
40
|
+
export { default as RangeAreaChart } from './RangeAreaChart.astro';
|
|
41
|
+
export { default as RetentionCohortTable } from './RetentionCohortTable.astro';
|
|
42
|
+
export { default as SankeyChart } from './SankeyChart.astro';
|
|
43
|
+
export { default as ScatterPlot } from './ScatterPlot.astro';
|
|
44
|
+
export { default as SegmentedBarChart } from './SegmentedBarChart.astro';
|
|
45
|
+
export { default as SlopeChart } from './SlopeChart.astro';
|
|
46
|
+
export { default as SparklineChart } from './SparklineChart.astro';
|
|
47
|
+
export { default as StackedAreaChart } from './StackedAreaChart.astro';
|
|
48
|
+
export { default as StackedBarChart } from './StackedBarChart.astro';
|
|
49
|
+
export { default as SunburstChart } from './SunburstChart.astro';
|
|
50
|
+
export { default as TreemapChart } from './TreemapChart.astro';
|
|
51
|
+
export { default as WaffleChart } from './WaffleChart.astro';
|
|
52
|
+
export { default as WaterfallChart } from './WaterfallChart.astro';
|