astro-component-kit 0.3.1 → 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.
Files changed (115) hide show
  1. package/README.md +33 -14
  2. package/package.json +2 -2
  3. package/src/components/index.ts +16 -202
  4. package/src/components/lib/buttons/NeonBorderButton.astro +162 -0
  5. package/src/components/lib/buttons/RippleButton.astro +199 -0
  6. package/src/components/lib/buttons/SlideRevealButton.astro +159 -0
  7. package/src/components/lib/buttons/index.ts +4 -0
  8. package/src/components/lib/cards/GithubRepoCard.astro +176 -0
  9. package/src/components/lib/cards/PricingCompareMiniCard.astro +196 -0
  10. package/src/components/lib/cards/StatsTrendCard.astro +149 -0
  11. package/src/components/lib/cards/index.ts +3 -0
  12. package/src/components/lib/charts/ActivityRings.astro +105 -0
  13. package/src/components/lib/charts/BoxPlotChart.astro +109 -0
  14. package/src/components/lib/charts/BubbleChart.astro +101 -0
  15. package/src/components/lib/charts/BulletChart.astro +160 -0
  16. package/src/components/lib/charts/BumpChart.astro +92 -0
  17. package/src/components/lib/charts/CandlestickChart.astro +105 -0
  18. package/src/components/lib/charts/ChartFrame.astro +373 -0
  19. package/src/components/lib/charts/DivergingBarChart.astro +98 -0
  20. package/src/components/lib/charts/DumbbellChart.astro +92 -0
  21. package/src/components/lib/charts/ForecastLineChart.astro +125 -0
  22. package/src/components/lib/charts/FunnelChart.astro +122 -0
  23. package/src/components/lib/charts/GaugeChart.astro +111 -0
  24. package/src/components/lib/charts/GoalProgressChart.astro +97 -0
  25. package/src/components/lib/charts/GroupedBarChart.astro +125 -0
  26. package/src/components/lib/charts/HeatmapChart.astro +131 -0
  27. package/src/components/lib/charts/HistogramChart.astro +111 -0
  28. package/src/components/lib/charts/HorizontalBarChart.astro +155 -0
  29. package/src/components/lib/charts/KpiStatTile.astro +116 -0
  30. package/src/components/lib/charts/KpiTrendGrid.astro +60 -0
  31. package/src/components/lib/charts/LineChart.astro +160 -0
  32. package/src/components/lib/charts/LinearProgressGauge.astro +136 -0
  33. package/src/components/lib/charts/LollipopChart.astro +100 -0
  34. package/src/components/lib/charts/MeterGauge.astro +122 -0
  35. package/src/components/lib/charts/MetricDeltaTable.astro +125 -0
  36. package/src/components/lib/charts/PieChart.astro +135 -0
  37. package/src/components/lib/charts/PolarAreaChart.astro +85 -0
  38. package/src/components/lib/charts/PopulationPyramid.astro +122 -0
  39. package/src/components/lib/charts/PunchCardChart.astro +89 -0
  40. package/src/components/lib/charts/RadarChart.astro +94 -0
  41. package/src/components/lib/charts/RadarMetricChart.astro +168 -0
  42. package/src/components/lib/charts/RadialBarChart.astro +124 -0
  43. package/src/components/lib/charts/RangeAreaChart.astro +113 -0
  44. package/src/components/lib/charts/RetentionCohortTable.astro +126 -0
  45. package/src/components/lib/charts/SankeyChart.astro +128 -0
  46. package/src/components/lib/charts/ScatterPlot.astro +139 -0
  47. package/src/components/lib/charts/SegmentedBarChart.astro +119 -0
  48. package/src/components/lib/charts/SlopeChart.astro +105 -0
  49. package/src/components/lib/charts/SparklineChart.astro +87 -0
  50. package/src/components/lib/charts/StackedAreaChart.astro +130 -0
  51. package/src/components/lib/charts/StackedBarChart.astro +136 -0
  52. package/src/components/lib/charts/SunburstChart.astro +103 -0
  53. package/src/components/lib/charts/TreemapChart.astro +94 -0
  54. package/src/components/lib/charts/WaffleChart.astro +121 -0
  55. package/src/components/lib/charts/WaterfallChart.astro +118 -0
  56. package/src/components/lib/charts/chart-tooltip.ts +75 -0
  57. package/src/components/lib/charts/chart-utils.ts +271 -0
  58. package/src/components/lib/charts/index.ts +46 -0
  59. package/src/components/lib/data/ActivityHeatmapGrid.astro +157 -0
  60. package/src/components/lib/data/MetricComparisonRow.astro +109 -0
  61. package/src/components/lib/data/StatusTimelineFeed.astro +168 -0
  62. package/src/components/lib/data/index.ts +3 -0
  63. package/src/components/lib/feedback/ProgressRingCounter.astro +118 -0
  64. package/src/components/lib/feedback/ToastNotificationGroup.astro +173 -0
  65. package/src/components/lib/feedback/index.ts +2 -0
  66. package/src/components/lib/forms/GlassInlineToggleGroup.astro +161 -0
  67. package/src/components/lib/forms/PasswordStrengthInput.astro +242 -0
  68. package/src/components/lib/forms/PhoneCountryInput.astro +145 -0
  69. package/src/components/lib/forms/TagsAutocompleteInput.astro +249 -0
  70. package/src/components/lib/forms/index.ts +5 -0
  71. package/src/components/lib/layout/DiagonalSectionDivider.astro +69 -0
  72. package/src/components/lib/layout/HolyGrailGrid.astro +125 -0
  73. package/src/components/lib/layout/index.ts +3 -1
  74. package/src/components/lib/navigation/QuickJumpDock.astro +125 -0
  75. package/src/components/lib/navigation/StepBreadcrumbBar.astro +151 -0
  76. package/src/components/lib/navigation/SubNavbarPills.astro +102 -0
  77. package/src/components/lib/navigation/index.ts +3 -0
  78. package/src/components/lib/overlays/CookiePreferencesDrawer.astro +226 -0
  79. package/src/components/lib/overlays/FloatingFeedbackWidget.astro +203 -0
  80. package/src/components/lib/overlays/ImageCompareSlider.astro +172 -0
  81. package/src/components/lib/overlays/index.ts +3 -0
  82. package/src/components/lib/sections/BlogGridSection.astro +131 -0
  83. package/src/components/lib/sections/CareersOpenings.astro +139 -0
  84. package/src/components/lib/sections/ChangelogSection.astro +144 -0
  85. package/src/components/lib/sections/ContactSplitMap.astro +139 -0
  86. package/src/components/lib/sections/CtaNewsletterSection.astro +163 -0
  87. package/src/components/lib/sections/CtaSplitImage.astro +147 -0
  88. package/src/components/lib/sections/FaqTwoColumn.astro +119 -0
  89. package/src/components/lib/sections/FeatureAlternatingRows.astro +190 -0
  90. package/src/components/lib/sections/FeatureIconGrid.astro +155 -0
  91. package/src/components/lib/sections/FeatureTabsShowcase.astro +180 -0
  92. package/src/components/lib/sections/FooterMegaSection.astro +155 -0
  93. package/src/components/lib/sections/HeroDashboardPreview.astro +180 -0
  94. package/src/components/lib/sections/HeroMinimalCentered.astro +175 -0
  95. package/src/components/lib/sections/HeroShowcaseSplit.astro +304 -0
  96. package/src/components/lib/sections/HeroTerminal.astro +251 -0
  97. package/src/components/lib/sections/HeroWaitlist.astro +229 -0
  98. package/src/components/lib/sections/HowItWorksSteps.astro +178 -0
  99. package/src/components/lib/sections/IntegrationsGrid.astro +167 -0
  100. package/src/components/lib/sections/LogoCloudSection.astro +96 -0
  101. package/src/components/lib/sections/LogoWallGrid.astro +91 -0
  102. package/src/components/lib/sections/PricingSinglePlan.astro +157 -0
  103. package/src/components/lib/sections/PricingToggleCards.astro +171 -0
  104. package/src/components/lib/sections/StatsCounterBand.astro +122 -0
  105. package/src/components/lib/sections/StatsDashboardSection.astro +83 -0
  106. package/src/components/lib/sections/TestimonialCarousel.astro +165 -0
  107. package/src/components/lib/sections/TestimonialFeatured.astro +120 -0
  108. package/src/components/lib/sections/TestimonialGridSection.astro +188 -0
  109. package/src/components/lib/sections/index.ts +29 -0
  110. package/src/components/lib/typography/AnimatedCounterText.astro +102 -0
  111. package/src/components/lib/typography/CurvedPathHeading.astro +86 -0
  112. package/src/components/lib/typography/GradientTextMarquee.astro +105 -0
  113. package/src/components/lib/typography/HackerDecodeText.astro +85 -0
  114. package/src/components/lib/typography/index.ts +4 -0
  115. package/src/styles/global.css +18 -1
@@ -0,0 +1,113 @@
1
+ ---
2
+ /**
3
+ * RangeAreaChart — Min/max band with an average line. Ideal for temperatures,
4
+ * latency percentiles or price ranges.
5
+ *
6
+ * @param {string[]} categories - X-axis labels.
7
+ * @param {number[]} min - Lower bound per period.
8
+ * @param {number[]} max - Upper bound per period.
9
+ * @param {number[]} avg - Central value per period.
10
+ * @param {string} bandLabel - Legend label for the band.
11
+ * @param {string} lineLabel - Legend label for the line.
12
+ * @param {string} suffix - Value suffix.
13
+ * @param {string} color - Hue for band and line.
14
+ * @param {string} variant - Card style (glass|solid|ghost).
15
+ * @param {string} size - Density (sm|md|lg).
16
+ */
17
+ import ChartFrame from './ChartFrame.astro';
18
+ import { scaleLinear, niceTicks, bandPath, smoothPath, formatValue, type Point } from './chart-utils';
19
+
20
+ interface Props {
21
+ title?: string;
22
+ subtitle?: string;
23
+ categories?: string[];
24
+ min?: number[];
25
+ max?: number[];
26
+ avg?: number[];
27
+ bandLabel?: string;
28
+ lineLabel?: string;
29
+ prefix?: string;
30
+ suffix?: string;
31
+ height?: number;
32
+ color?: string;
33
+ variant?: 'glass' | 'solid' | 'ghost';
34
+ size?: 'sm' | 'md' | 'lg';
35
+ className?: string;
36
+ }
37
+
38
+ const {
39
+ title = 'API latency',
40
+ subtitle = 'p10–p90 band with median, last 10 days',
41
+ categories = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun', 'Mon', 'Tue', 'Wed'],
42
+ min = [42, 45, 40, 48, 52, 38, 36, 44, 47, 41],
43
+ max = [120, 135, 118, 160, 172, 98, 90, 126, 140, 115],
44
+ avg = [74, 80, 70, 92, 101, 62, 58, 78, 86, 72],
45
+ bandLabel = 'p10–p90',
46
+ lineLabel = 'Median',
47
+ prefix = '',
48
+ suffix = 'ms',
49
+ height = 260,
50
+ color = 'var(--chart-1, #3987e5)',
51
+ variant = 'glass',
52
+ size = 'md',
53
+ className = '',
54
+ } = Astro.props;
55
+
56
+ const W = 640;
57
+ const H = height;
58
+ const m = { t: 12, r: 16, b: 28, l: 48 };
59
+ const n = categories.length;
60
+ const ticks = niceTicks(Math.min(...min), Math.max(...max), 4);
61
+ const y = scaleLinear(ticks[0], ticks[ticks.length - 1], H - m.b, m.t);
62
+ const x = (i: number) => m.l + (n <= 1 ? 0 : (i / (n - 1)) * (W - m.l - m.r));
63
+ const step = n > 1 ? (W - m.l - m.r) / (n - 1) : W;
64
+ const upper: Point[] = max.map((v, i) => [x(i), y(v)]);
65
+ const lower: Point[] = min.map((v, i) => [x(i), y(v)]);
66
+ const mid: Point[] = avg.map((v, i) => [x(i), y(v)]);
67
+ const fmt = (v: number) => formatValue(v, prefix, suffix);
68
+ ---
69
+
70
+ <ChartFrame
71
+ title={title}
72
+ subtitle={subtitle}
73
+ variant={variant}
74
+ size={size}
75
+ className={className}
76
+ legend={[{ label: bandLabel, color: `color-mix(in srgb, ${color} 35%, transparent)` }, { label: lineLabel, color }]}
77
+ table={{
78
+ headers: ['Period', 'Min', lineLabel, 'Max'],
79
+ rows: categories.map((c, i) => [c, fmt(min[i]), fmt(avg[i]), fmt(max[i])]),
80
+ }}
81
+ >
82
+ <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: range band chart`}>
83
+ {ticks.map((t) => (
84
+ <g>
85
+ <line class="cf-grid" x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} />
86
+ <text class="cf-label" x={m.l - 8} y={y(t) + 4} text-anchor="end">{fmt(t)}</text>
87
+ </g>
88
+ ))}
89
+ {categories.map((c, i) => <text class="cf-label" x={x(i)} y={H - 8} text-anchor="middle">{c}</text>)}
90
+
91
+ <path d={bandPath(upper, lower)} fill={color} fill-opacity="0.18" aria-hidden="true" />
92
+ <path d={smoothPath(mid)} fill="none" stroke={color} stroke-width="2" aria-hidden="true" />
93
+
94
+ {categories.map((c, i) => (
95
+ <g class="cf-hit-group">
96
+ <line class="cf-cross" x1={x(i)} x2={x(i)} y1={y(max[i])} y2={y(min[i])} />
97
+ <circle class="cf-cross" cx={x(i)} cy={y(avg[i])} r="4.5" fill={color} stroke="#fff" stroke-width="1.5" />
98
+ <rect
99
+ class="cf-hit"
100
+ x={x(i) - step / 2}
101
+ y={m.t}
102
+ width={step}
103
+ height={H - m.t - m.b}
104
+ tabindex="0"
105
+ role="img"
106
+ aria-label={`${c}: min ${fmt(min[i])}, ${lineLabel} ${fmt(avg[i])}, max ${fmt(max[i])}`}
107
+ data-tip-title={c}
108
+ data-tip={`Max: ${fmt(max[i])}|${color}::${lineLabel}: ${fmt(avg[i])}|Min: ${fmt(min[i])}`}
109
+ />
110
+ </g>
111
+ ))}
112
+ </svg>
113
+ </ChartFrame>
@@ -0,0 +1,126 @@
1
+ ---
2
+ /**
3
+ * RetentionCohortTable — Triangular cohort retention matrix. Each row is a
4
+ * signup cohort, each column a period since signup, shaded on a single-hue ramp.
5
+ *
6
+ * @param {Array<{cohort: string, size: number, retention: number[]}>} cohorts - Retention % per period (period 0 = 100).
7
+ * @param {string} periodLabel - Column prefix (e.g. 'Week', 'Month').
8
+ * @param {string} color - Ramp hue.
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
+
14
+ interface Props {
15
+ title?: string;
16
+ subtitle?: string;
17
+ cohorts?: Array<{ cohort: string; size: number; retention: number[] }>;
18
+ periodLabel?: string;
19
+ color?: string;
20
+ variant?: 'glass' | 'solid' | 'ghost';
21
+ size?: 'sm' | 'md' | 'lg';
22
+ className?: string;
23
+ }
24
+
25
+ const {
26
+ title = 'User retention',
27
+ subtitle = 'Monthly cohorts, % still active',
28
+ cohorts = [
29
+ { cohort: 'May', size: 1240, retention: [100, 62, 48, 41, 37, 35] },
30
+ { cohort: 'Jun', size: 1410, retention: [100, 65, 51, 44, 40] },
31
+ { cohort: 'Jul', size: 1580, retention: [100, 68, 55, 47] },
32
+ { cohort: 'Aug', size: 1720, retention: [100, 71, 57] },
33
+ { cohort: 'Sep', size: 1890, retention: [100, 73] },
34
+ { cohort: 'Oct', size: 2050, retention: [100] },
35
+ ],
36
+ periodLabel = 'M',
37
+ color = 'var(--chart-3, #199e70)',
38
+ variant = 'glass',
39
+ size = 'md',
40
+ className = '',
41
+ } = Astro.props;
42
+
43
+ const periods = Math.max(...cohorts.map((c) => c.retention.length));
44
+ const cols = Array.from({ length: periods }, (_, i) => `${periodLabel}${i}`);
45
+ const shade = (v: number) => `color-mix(in srgb, ${color} ${Math.round(10 + (v / 100) * 85)}%, #141416)`;
46
+ const avg = cols.map((_, i) => {
47
+ const vals = cohorts.map((c) => c.retention[i]).filter((v) => v !== undefined);
48
+ return vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : 0;
49
+ });
50
+ ---
51
+
52
+ <ChartFrame
53
+ title={title}
54
+ subtitle={subtitle}
55
+ variant={variant}
56
+ size={size}
57
+ className={className}
58
+ >
59
+ <div class="rc">
60
+ <table class="rc__table">
61
+ <caption class="sr-only">{title}: retention percentage by cohort and period</caption>
62
+ <thead>
63
+ <tr>
64
+ <th scope="col">Cohort</th>
65
+ <th scope="col">Users</th>
66
+ {cols.map((c) => <th scope="col">{c}</th>)}
67
+ </tr>
68
+ </thead>
69
+ <tbody>
70
+ {cohorts.map((c) => (
71
+ <tr>
72
+ <th scope="row">{c.cohort}</th>
73
+ <td class="rc__size">{c.size.toLocaleString('en-US')}</td>
74
+ {cols.map((_, i) => {
75
+ const v = c.retention[i];
76
+ return v === undefined
77
+ ? <td class="rc__empty" aria-label="no data yet"></td>
78
+ : <td class="rc__cell" style={`background:${shade(v)}`} data-strong={v > 55 ? '' : undefined}>{v}%</td>;
79
+ })}
80
+ </tr>
81
+ ))}
82
+ </tbody>
83
+ <tfoot>
84
+ <tr>
85
+ <th scope="row">Average</th>
86
+ <td></td>
87
+ {avg.map((a) => <td class="rc__avg">{a.toFixed(0)}%</td>)}
88
+ </tr>
89
+ </tfoot>
90
+ </table>
91
+ </div>
92
+ </ChartFrame>
93
+
94
+ <style>
95
+ .rc { overflow-x: auto; }
96
+ .rc__table {
97
+ width: 100%;
98
+ border-collapse: separate;
99
+ border-spacing: 3px;
100
+ font-size: 0.75rem;
101
+ font-variant-numeric: tabular-nums;
102
+ }
103
+ .rc__table th {
104
+ padding: 0.4rem 0.5rem;
105
+ font-weight: 600;
106
+ color: var(--c-text-3, #8a8a8a);
107
+ text-align: center;
108
+ white-space: nowrap;
109
+ }
110
+ .rc__table tbody th, .rc__table tfoot th { text-align: left; color: var(--c-text-1, #fff); }
111
+ .rc__size { color: var(--c-text-2, #a3a3a3); text-align: right; padding-right: 0.6rem; }
112
+ .rc__cell {
113
+ min-width: 3rem;
114
+ padding: 0.55rem 0.4rem;
115
+ text-align: center;
116
+ border-radius: 4px;
117
+ color: rgba(255, 255, 255, 0.78);
118
+ }
119
+ .rc__cell[data-strong] { color: #fff; font-weight: 650; }
120
+ .rc__empty { background: rgba(255, 255, 255, 0.02); border-radius: 4px; }
121
+ .rc__avg { text-align: center; color: var(--c-text-1, #fff); font-weight: 600; padding-top: 0.5rem; }
122
+ .sr-only {
123
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
124
+ overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
125
+ }
126
+ </style>
@@ -0,0 +1,128 @@
1
+ ---
2
+ /**
3
+ * SankeyChart — Two-column flow diagram: sources on the left, targets on the
4
+ * right, ribbons whose thickness encodes the flow volume.
5
+ *
6
+ * @param {Array<{source: string, target: string, value: number}>} flows - Links between nodes.
7
+ * @param {string} suffix - Value suffix.
8
+ * @param {number} height - SVG height in viewBox units (width is 640).
9
+ * @param {string[]} colors - Optional per-source colors.
10
+ * @param {string} variant - Card style (glass|solid|ghost).
11
+ * @param {string} size - Density (sm|md|lg).
12
+ */
13
+ import ChartFrame from './ChartFrame.astro';
14
+ import { colorAt, formatValue, sum } from './chart-utils';
15
+
16
+ interface Props {
17
+ title?: string;
18
+ subtitle?: string;
19
+ flows?: Array<{ source: string; target: string; value: number }>;
20
+ prefix?: string;
21
+ suffix?: string;
22
+ height?: number;
23
+ colors?: string[];
24
+ variant?: 'glass' | 'solid' | 'ghost';
25
+ size?: 'sm' | 'md' | 'lg';
26
+ className?: string;
27
+ }
28
+
29
+ const {
30
+ title = 'Where signups come from',
31
+ subtitle = 'Acquisition channel → plan chosen',
32
+ flows = [
33
+ { source: 'Organic', target: 'Free', value: 820 },
34
+ { source: 'Organic', target: 'Pro', value: 310 },
35
+ { source: 'Organic', target: 'Team', value: 90 },
36
+ { source: 'Paid ads', target: 'Free', value: 410 },
37
+ { source: 'Paid ads', target: 'Pro', value: 160 },
38
+ { source: 'Referral', target: 'Pro', value: 220 },
39
+ { source: 'Referral', target: 'Team', value: 140 },
40
+ { source: 'Partners', target: 'Team', value: 120 },
41
+ { source: 'Partners', target: 'Enterprise', value: 60 },
42
+ ],
43
+ prefix = '',
44
+ suffix = '',
45
+ height = 320,
46
+ colors,
47
+ variant = 'glass',
48
+ size = 'md',
49
+ className = '',
50
+ } = Astro.props;
51
+
52
+ const W = 640;
53
+ const H = height;
54
+ const nodeW = 12;
55
+ const gap = 14;
56
+ const xL = 110;
57
+ const xR = W - 110 - nodeW;
58
+ const sources = [...new Set(flows.map((f) => f.source))];
59
+ const targets = [...new Set(flows.map((f) => f.target))];
60
+ const total = sum(flows.map((f) => f.value)) || 1;
61
+ const scaleFor = (count: number) => (H - gap * (count - 1)) / total;
62
+ const k = Math.min(scaleFor(sources.length), scaleFor(targets.length));
63
+
64
+ const layout = (names: string[], key: 'source' | 'target') => {
65
+ const used = names.reduce((a, nm) => a + sum(flows.filter((f) => f[key] === nm).map((f) => f.value)) * k, 0) + gap * (names.length - 1);
66
+ let y = (H - used) / 2;
67
+ return Object.fromEntries(names.map((nm) => {
68
+ const v = sum(flows.filter((f) => f[key] === nm).map((f) => f.value));
69
+ const node = { y, h: v * k, v, offset: 0 };
70
+ y += v * k + gap;
71
+ return [nm, node];
72
+ }));
73
+ };
74
+ const L = layout(sources, 'source');
75
+ const R = layout(targets, 'target');
76
+ const srcColor = Object.fromEntries(sources.map((s, i) => [s, colorAt(i, colors)]));
77
+
78
+ const links = flows.map((f) => {
79
+ const s = L[f.source];
80
+ const t = R[f.target];
81
+ const h = f.value * k;
82
+ const y0 = s.y + s.offset + h / 2;
83
+ const y1 = t.y + t.offset + h / 2;
84
+ s.offset += h;
85
+ t.offset += h;
86
+ const x0 = xL + nodeW;
87
+ const x1 = xR;
88
+ const mx = (x0 + x1) / 2;
89
+ return { ...f, h, d: `M${x0},${y0} C${mx},${y0} ${mx},${y1} ${x1},${y1}`, color: srcColor[f.source] };
90
+ });
91
+ const fmt = (v: number) => formatValue(v, prefix, suffix);
92
+ ---
93
+
94
+ <ChartFrame
95
+ title={title}
96
+ subtitle={subtitle}
97
+ variant={variant}
98
+ size={size}
99
+ className={className}
100
+ table={{ headers: ['From', 'To', 'Value', 'Share'], rows: flows.map((f) => [f.source, f.target, fmt(f.value), `${((f.value / total) * 100).toFixed(1)}%`]) }}
101
+ >
102
+ <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: flow from ${sources.length} sources to ${targets.length} targets`}>
103
+ {links.map((l) => (
104
+ <path class="cf-mark cf-dim sk__link" d={l.d} fill="none" stroke={l.color} stroke-opacity="0.38" stroke-width={Math.max(1, l.h)}
105
+ tabindex="0" role="img" aria-label={`${l.source} to ${l.target}: ${fmt(l.value)}`}
106
+ data-tip-title={`${l.source} → ${l.target}`} data-tip={`${l.color}::${fmt(l.value)} · ${((l.value / total) * 100).toFixed(1)}%`} />
107
+ ))}
108
+ {sources.map((s) => (
109
+ <g>
110
+ <rect x={xL} y={L[s].y} width={nodeW} height={L[s].h} rx="3" fill={srcColor[s]} />
111
+ <text class="cf-label cf-label--strong" x={xL - 8} y={L[s].y + L[s].h / 2} text-anchor="end">{s}</text>
112
+ <text class="cf-label" x={xL - 8} y={L[s].y + L[s].h / 2 + 14} text-anchor="end">{fmt(L[s].v)}</text>
113
+ </g>
114
+ ))}
115
+ {targets.map((t) => (
116
+ <g>
117
+ <rect x={xR} y={R[t].y} width={nodeW} height={R[t].h} rx="3" fill="var(--c-text-2, #a3a3a3)" />
118
+ <text class="cf-label cf-label--strong" x={xR + nodeW + 8} y={R[t].y + R[t].h / 2}>{t}</text>
119
+ <text class="cf-label" x={xR + nodeW + 8} y={R[t].y + R[t].h / 2 + 14}>{fmt(R[t].v)}</text>
120
+ </g>
121
+ ))}
122
+ </svg>
123
+ </ChartFrame>
124
+
125
+ <style>
126
+ .sk__link { transition: stroke-opacity 0.2s ease; }
127
+ .sk__link:hover, .sk__link:focus-visible { stroke-opacity: 0.7; }
128
+ </style>
@@ -0,0 +1,139 @@
1
+ ---
2
+ /**
3
+ * ScatterPlot — X/Y relationship plot for up to 3 groups, with an optional
4
+ * least-squares trend line and per-point tooltips.
5
+ *
6
+ * @param {Array<{name: string, points: Array<{x: number, y: number, label?: string}>}>} groups - Point groups (max 3).
7
+ * @param {string} xLabel - X-axis title.
8
+ * @param {string} yLabel - Y-axis title.
9
+ * @param {boolean} trend - Draw a linear regression line across all points.
10
+ * @param {string[]} colors - Optional per-group colors.
11
+ * @param {string} variant - Card style (glass|solid|ghost).
12
+ * @param {string} size - Density (sm|md|lg).
13
+ */
14
+ import ChartFrame from './ChartFrame.astro';
15
+ import { scaleLinear, niceTicks, colorAt, formatCompact } from './chart-utils';
16
+
17
+ interface Pt { x: number; y: number; label?: string }
18
+ interface Props {
19
+ title?: string;
20
+ subtitle?: string;
21
+ groups?: Array<{ name: string; points: Pt[] }>;
22
+ xLabel?: string;
23
+ yLabel?: string;
24
+ trend?: boolean;
25
+ height?: number;
26
+ colors?: string[];
27
+ variant?: 'glass' | 'solid' | 'ghost';
28
+ size?: 'sm' | 'md' | 'lg';
29
+ className?: string;
30
+ }
31
+
32
+ const gen = (seed: number, n: number, slope: number, base: number, noise: number): Pt[] =>
33
+ Array.from({ length: n }, (_, i) => {
34
+ const r = Math.sin((i + 1) * seed) * 10000;
35
+ const j = r - Math.floor(r);
36
+ const xv = 5 + ((i * 37) % 95);
37
+ return { x: xv, y: Math.round(base + slope * xv + (j - 0.5) * noise) };
38
+ });
39
+
40
+ const {
41
+ title = 'Ad spend vs signups',
42
+ subtitle = 'Per campaign',
43
+ groups = [
44
+ { name: 'Search', points: gen(3.1, 16, 2.1, 20, 60) },
45
+ { name: 'Social', points: gen(7.7, 14, 1.3, 10, 70) },
46
+ { name: 'Video', points: gen(5.3, 10, 0.8, 30, 50) },
47
+ ],
48
+ xLabel = 'Spend ($k)',
49
+ yLabel = 'Signups',
50
+ trend = true,
51
+ height = 300,
52
+ colors,
53
+ variant = 'glass',
54
+ size = 'md',
55
+ className = '',
56
+ } = Astro.props;
57
+
58
+ const W = 640;
59
+ const H = height;
60
+ const m = { t: 12, r: 16, b: 40, l: 52 };
61
+ const all = groups.flatMap((g) => g.points);
62
+ const xt = niceTicks(Math.min(0, ...all.map((p) => p.x)), Math.max(...all.map((p) => p.x)), 5);
63
+ const yt = niceTicks(Math.min(0, ...all.map((p) => p.y)), Math.max(...all.map((p) => p.y)), 4);
64
+ const x = scaleLinear(xt[0], xt[xt.length - 1], m.l, W - m.r);
65
+ const y = scaleLinear(yt[0], yt[yt.length - 1], H - m.b, m.t);
66
+
67
+ // Ordinary least squares over all points.
68
+ const n = all.length;
69
+ const mx = all.reduce((a, p) => a + p.x, 0) / n;
70
+ const my = all.reduce((a, p) => a + p.y, 0) / n;
71
+ const slope = all.reduce((a, p) => a + (p.x - mx) * (p.y - my), 0) / (all.reduce((a, p) => a + (p.x - mx) ** 2, 0) || 1);
72
+ const icpt = my - slope * mx;
73
+ const r2 = (() => {
74
+ const ssTot = all.reduce((a, p) => a + (p.y - my) ** 2, 0) || 1;
75
+ const ssRes = all.reduce((a, p) => a + (p.y - (icpt + slope * p.x)) ** 2, 0);
76
+ return 1 - ssRes / ssTot;
77
+ })();
78
+ const shapes = ['circle', 'square', 'diamond'];
79
+ ---
80
+
81
+ <ChartFrame
82
+ title={title}
83
+ subtitle={subtitle}
84
+ variant={variant}
85
+ size={size}
86
+ className={className}
87
+ legend={groups.slice(0, 3).map((g, i) => ({ label: g.name, color: colorAt(i, colors) }))}
88
+ table={{ headers: ['Group', xLabel, yLabel], rows: groups.flatMap((g) => g.points.map((p) => [p.label ? `${g.name} · ${p.label}` : g.name, p.x, p.y])) }}
89
+ >
90
+ <svg viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`${title}: scatter plot of ${n} points${trend ? `, trend R² ${r2.toFixed(2)}` : ''}`}>
91
+ {yt.map((t) => (
92
+ <g>
93
+ <line class="cf-grid" x1={m.l} x2={W - m.r} y1={y(t)} y2={y(t)} />
94
+ <text class="cf-label" x={m.l - 8} y={y(t) + 4} text-anchor="end">{formatCompact(t)}</text>
95
+ </g>
96
+ ))}
97
+ {xt.map((t) => (
98
+ <g>
99
+ <line class="cf-grid" x1={x(t)} x2={x(t)} y1={m.t} y2={H - m.b} />
100
+ <text class="cf-label" x={x(t)} y={H - m.b + 16} text-anchor="middle">{formatCompact(t)}</text>
101
+ </g>
102
+ ))}
103
+ <text class="cf-label" x={(m.l + W - m.r) / 2} y={H - 4} text-anchor="middle">{xLabel}</text>
104
+ <text class="cf-label" transform={`translate(12 ${(m.t + H - m.b) / 2}) rotate(-90)`} text-anchor="middle">{yLabel}</text>
105
+
106
+ {trend && (
107
+ <g aria-hidden="true">
108
+ <line x1={x(xt[0])} y1={y(icpt + slope * xt[0])} x2={x(xt[xt.length - 1])} y2={y(icpt + slope * xt[xt.length - 1])}
109
+ stroke="var(--c-text-1, #fff)" stroke-opacity="0.45" stroke-width="1.5" />
110
+ <text class="cf-label" x={W - m.r} y={y(icpt + slope * xt[xt.length - 1]) - 8} text-anchor="end">R² {r2.toFixed(2)}</text>
111
+ </g>
112
+ )}
113
+
114
+ {groups.slice(0, 3).map((g, gi) => {
115
+ const col = colorAt(gi, colors);
116
+ return g.points.map((p) => {
117
+ const px = x(p.x);
118
+ const py = y(p.y);
119
+ const common = {
120
+ class: 'cf-mark cf-dim',
121
+ fill: col,
122
+ 'fill-opacity': 0.85,
123
+ stroke: 'var(--chart-surface, #0d0d0f)',
124
+ 'stroke-width': 1.5,
125
+ tabindex: 0,
126
+ role: 'img' as const,
127
+ 'aria-label': `${g.name}${p.label ? ` ${p.label}` : ''}: ${xLabel} ${p.x}, ${yLabel} ${p.y}`,
128
+ 'data-tip-title': p.label ?? g.name,
129
+ 'data-tip': `${col}::${g.name}|${xLabel}: ${p.x}|${yLabel}: ${p.y}`,
130
+ };
131
+ return shapes[gi] === 'square'
132
+ ? <rect {...common} x={px - 5} y={py - 5} width="10" height="10" rx="2" />
133
+ : shapes[gi] === 'diamond'
134
+ ? <path {...common} d={`M${px},${py - 6.5} L${px + 6.5},${py} L${px},${py + 6.5} L${px - 6.5},${py} Z`} />
135
+ : <circle {...common} cx={px} cy={py} r="5.5" />;
136
+ });
137
+ })}
138
+ </svg>
139
+ </ChartFrame>
@@ -0,0 +1,119 @@
1
+ ---
2
+ /**
3
+ * SegmentedBarChart — A single tall part-to-whole bar with labelled segments
4
+ * and a breakdown list. A clearer alternative to a pie for ≤ 6 parts.
5
+ *
6
+ * @param {Array<{label: string, value: number}>} segments - Parts of the whole.
7
+ * @param {string} total - Optional formatted total for the header.
8
+ * @param {string} prefix - Value prefix.
9
+ * @param {string} suffix - Value suffix.
10
+ * @param {string[]} colors - Optional per-segment colors.
11
+ * @param {string} variant - Card style (glass|solid|ghost).
12
+ * @param {string} size - Density (sm|md|lg).
13
+ */
14
+ import ChartFrame from './ChartFrame.astro';
15
+ import { colorAt, formatValue, sum } from './chart-utils';
16
+
17
+ interface Props {
18
+ title?: string;
19
+ subtitle?: string;
20
+ segments?: Array<{ label: string; value: number }>;
21
+ total?: string;
22
+ prefix?: string;
23
+ suffix?: string;
24
+ colors?: string[];
25
+ variant?: 'glass' | 'solid' | 'ghost';
26
+ size?: 'sm' | 'md' | 'lg';
27
+ className?: string;
28
+ }
29
+
30
+ const {
31
+ title = 'Storage usage',
32
+ subtitle = '512 GB plan',
33
+ segments = [
34
+ { label: 'Media', value: 182 },
35
+ { label: 'Backups', value: 96 },
36
+ { label: 'Databases', value: 64 },
37
+ { label: 'Logs', value: 38 },
38
+ { label: 'Other', value: 22 },
39
+ ],
40
+ total,
41
+ prefix = '',
42
+ suffix = ' GB',
43
+ colors,
44
+ variant = 'glass',
45
+ size = 'md',
46
+ className = '',
47
+ } = Astro.props;
48
+
49
+ const tot = sum(segments.map((s) => s.value)) || 1;
50
+ const fmt = (v: number) => formatValue(v, prefix, suffix, false);
51
+ const segs = segments.slice(0, 8).map((s, i) => ({ ...s, pct: (s.value / tot) * 100, color: colorAt(i, colors) }));
52
+ ---
53
+
54
+ <ChartFrame
55
+ title={title}
56
+ subtitle={subtitle}
57
+ value={total ?? fmt(tot)}
58
+ variant={variant}
59
+ size={size}
60
+ className={className}
61
+ table={{ headers: ['Segment', 'Value', 'Share'], rows: segs.map((s) => [s.label, fmt(s.value), `${s.pct.toFixed(1)}%`]) }}
62
+ >
63
+ <div class="sb__bar" role="list" aria-label={`${title} breakdown`}>
64
+ {segs.map((s) => (
65
+ <span class="sb__seg cf-mark" role="listitem" tabindex="0" style={`flex-grow:${s.value}; background:${s.color}`}
66
+ aria-label={`${s.label}: ${fmt(s.value)} (${s.pct.toFixed(1)}%)`}
67
+ data-tip-title={s.label} data-tip={`${s.color}::${fmt(s.value)} · ${s.pct.toFixed(1)}%`}>
68
+ {s.pct >= 9 && <span class="sb__pct">{s.pct.toFixed(0)}%</span>}
69
+ </span>
70
+ ))}
71
+ </div>
72
+ <ul class="sb__list">
73
+ {segs.map((s) => (
74
+ <li>
75
+ <span class="sb__dot" style={`background:${s.color}`} aria-hidden="true"></span>
76
+ <span class="sb__name">{s.label}</span>
77
+ <span class="sb__val">{fmt(s.value)}</span>
78
+ </li>
79
+ ))}
80
+ </ul>
81
+ </ChartFrame>
82
+
83
+ <style>
84
+ .sb__bar {
85
+ display: flex;
86
+ gap: 2px;
87
+ height: 44px;
88
+ border-radius: 8px;
89
+ overflow: hidden;
90
+ }
91
+ .sb__seg {
92
+ flex-basis: 0;
93
+ min-width: 4px;
94
+ display: flex;
95
+ align-items: center;
96
+ padding-left: 0.6rem;
97
+ transition: filter 0.2s ease;
98
+ }
99
+ .sb__seg:hover { filter: brightness(1.15); }
100
+ .sb__seg:focus-visible { outline: 2px solid var(--c-text-1, #fff); outline-offset: -2px; }
101
+ .sb__pct {
102
+ font-size: 0.75rem;
103
+ font-weight: 700;
104
+ color: #fff;
105
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
106
+ }
107
+ .sb__list {
108
+ list-style: none;
109
+ margin: 1rem 0 0;
110
+ padding: 0;
111
+ display: grid;
112
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
113
+ gap: 0.6rem 1.25rem;
114
+ }
115
+ .sb__list li { display: flex; align-items: center; gap: 0.5rem; font-size: 0.8125rem; }
116
+ .sb__dot { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; }
117
+ .sb__name { color: var(--c-text-2, #a3a3a3); }
118
+ .sb__val { margin-left: auto; color: var(--c-text-1, #fff); font-weight: 600; font-variant-numeric: tabular-nums; }
119
+ </style>