@a4ui/core 0.37.0 → 0.38.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 +8 -7
- package/dist/Collapse-Cr5cDqAl.js +375 -0
- package/dist/charts/AreaChart.d.ts +38 -0
- package/dist/charts/BarChart.d.ts +17 -1
- package/dist/charts/PieChart.d.ts +29 -0
- package/dist/charts/RadialBarChart.d.ts +33 -0
- package/dist/charts/index.d.ts +5 -0
- package/dist/charts/shared.d.ts +111 -0
- package/dist/charts.js +1493 -780
- package/dist/commerce.js +625 -634
- package/dist/elements.css +1076 -8
- package/dist/elements.iife.js +1 -6
- package/dist/elements.js +7774 -9133
- package/dist/full.css +1076 -8
- package/dist/index.d.ts +30 -3
- package/dist/index.js +17502 -15098
- package/dist/layout/Aurora.d.ts +16 -0
- package/dist/layout/EffectsToggle.d.ts +1 -1
- package/dist/layout/SpaceBackground.d.ts +1 -1
- package/dist/lib/anchored.d.ts +16 -0
- package/dist/motion-CTIBwMCA.js +104 -0
- package/dist/styles.css +110 -3
- package/dist/ui/AspectRatio.d.ts +19 -0
- package/dist/ui/Attachment.d.ts +70 -0
- package/dist/ui/ButtonGroup.d.ts +37 -0
- package/dist/ui/ChatMarker.d.ts +20 -0
- package/dist/ui/ChatThread.d.ts +32 -8
- package/dist/ui/ContactSection.d.ts +49 -0
- package/dist/ui/CtaBanner.d.ts +28 -0
- package/dist/ui/DateField.d.ts +2 -2
- package/dist/ui/Drawer.d.ts +5 -2
- package/dist/ui/Faq.d.ts +29 -0
- package/dist/ui/FeatureGrid.d.ts +35 -0
- package/dist/ui/Field.d.ts +148 -0
- package/dist/ui/Hero.d.ts +48 -0
- package/dist/ui/InputGroup.d.ts +93 -0
- package/dist/ui/Item.d.ts +63 -0
- package/dist/ui/Label.d.ts +23 -0
- package/dist/ui/Menubar.d.ts +83 -0
- package/dist/ui/Message.d.ts +51 -5
- package/dist/ui/Modal.d.ts +8 -2
- package/dist/ui/NativeSelect.d.ts +26 -0
- package/dist/ui/NavigationMenu.d.ts +85 -0
- package/dist/ui/PromptComposer.d.ts +5 -0
- package/dist/ui/Prose.d.ts +21 -0
- package/dist/ui/Questionnaire.d.ts +71 -0
- package/dist/ui/ScrollArea.d.ts +30 -0
- package/dist/ui/Sidebar.d.ts +229 -0
- package/dist/ui/SiteFooter.d.ts +39 -0
- package/dist/ui/SiteNav.d.ts +35 -0
- package/dist/ui/TeamGrid.d.ts +33 -0
- package/dist/ui/Testimonials.d.ts +37 -0
- package/dist/ui/internal/sectionKit.d.ts +22 -0
- package/dist/ui/internal/socialIcons.d.ts +9 -0
- package/package.json +38 -26
- package/preset.d.ts +23 -0
- package/src/charts/AreaChart.tsx +206 -0
- package/src/charts/BarChart.tsx +241 -5
- package/src/charts/DonutChart.tsx +8 -11
- package/src/charts/LineChart.tsx +2 -3
- package/src/charts/PieChart.tsx +145 -0
- package/src/charts/RadialBarChart.tsx +141 -0
- package/src/charts/index.ts +14 -0
- package/src/charts/shared.tsx +304 -0
- package/src/index.ts +192 -3
- package/src/layout/Aurora.tsx +36 -7
- package/src/layout/sceneEffects.ts +22 -3
- package/src/lib/anchored.ts +57 -0
- package/src/ui/AspectRatio.tsx +35 -0
- package/src/ui/Attachment.tsx +189 -0
- package/src/ui/ButtonGroup.tsx +89 -0
- package/src/ui/ChatMarker.tsx +58 -0
- package/src/ui/ChatThread.tsx +115 -23
- package/src/ui/ContactSection.tsx +199 -0
- package/src/ui/CtaBanner.tsx +62 -0
- package/src/ui/DateField.tsx +17 -7
- package/src/ui/DateRangePicker.tsx +15 -5
- package/src/ui/Drawer.tsx +37 -4
- package/src/ui/Faq.tsx +57 -0
- package/src/ui/FeatureGrid.tsx +77 -0
- package/src/ui/Field.tsx +374 -0
- package/src/ui/Hero.tsx +146 -0
- package/src/ui/InputGroup.tsx +219 -0
- package/src/ui/Item.tsx +185 -0
- package/src/ui/Label.tsx +50 -0
- package/src/ui/Menubar.tsx +212 -0
- package/src/ui/Message.tsx +139 -13
- package/src/ui/Modal.tsx +41 -5
- package/src/ui/MultiSelect.tsx +15 -5
- package/src/ui/NativeSelect.tsx +66 -0
- package/src/ui/NavigationMenu.tsx +262 -0
- package/src/ui/PromptComposer.tsx +11 -0
- package/src/ui/Prose.tsx +63 -0
- package/src/ui/Questionnaire.tsx +331 -0
- package/src/ui/ScrollArea.tsx +70 -0
- package/src/ui/Sidebar.tsx +753 -0
- package/src/ui/SiteFooter.tsx +112 -0
- package/src/ui/SiteNav.tsx +146 -0
- package/src/ui/TeamGrid.tsx +81 -0
- package/src/ui/Testimonials.tsx +103 -0
- package/src/ui/TimeField.tsx +15 -5
- package/src/ui/internal/sectionKit.tsx +72 -0
- package/src/ui/internal/socialIcons.tsx +41 -0
- package/dist/NumberInput-Bnh2u7pp.js +0 -381
- package/dist/motion-jYMWmZqB.js +0 -119
package/preset.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Types for `@a4ui/core/preset` — the Spatial Glass Tailwind preset.
|
|
2
|
+
// Requires Tailwind CSS v3 (`tailwindcss@^3.4.0`): v4 removed `presets:`.
|
|
3
|
+
import type { Config } from 'tailwindcss'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tailwind v3 preset: semantic color names bound to the A4ui CSS variables, the
|
|
7
|
+
* glass surface classes (`.card`, `.bg-glass`, `.tile-glass`, `.glow-edge`) and
|
|
8
|
+
* the `light:` variant.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import type { Config } from 'tailwindcss'
|
|
13
|
+
* import a4ui from '@a4ui/core/preset'
|
|
14
|
+
*
|
|
15
|
+
* export default {
|
|
16
|
+
* presets: [a4ui],
|
|
17
|
+
* content: ['./src/**\/*.{ts,tsx}', './node_modules/@a4ui/core/dist/**\/*.js'],
|
|
18
|
+
* } satisfies Config
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
declare const preset: Partial<Config>
|
|
22
|
+
|
|
23
|
+
export default preset
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// AreaChart — multi-series SVG area chart (no charting library). Series either
|
|
2
|
+
// overlap or stack; each area is filled with an SVG linearGradient built from
|
|
3
|
+
// the series' theme token, with a solid line on top. Hover or arrow keys show
|
|
4
|
+
// a tooltip with every series' value at that x.
|
|
5
|
+
import { createMemo, createUniqueId, For, Show, type JSX } from 'solid-js'
|
|
6
|
+
|
|
7
|
+
import { cn } from '../lib/cn'
|
|
8
|
+
import {
|
|
9
|
+
ChartLegend,
|
|
10
|
+
ChartTooltip,
|
|
11
|
+
FOCUS_CLASS,
|
|
12
|
+
createChartNav,
|
|
13
|
+
createHiddenSet,
|
|
14
|
+
seriesColor,
|
|
15
|
+
seriesValue,
|
|
16
|
+
type ChartSeries,
|
|
17
|
+
ChartXLabels,
|
|
18
|
+
} from './shared'
|
|
19
|
+
|
|
20
|
+
export interface AreaDatum {
|
|
21
|
+
label: string
|
|
22
|
+
/** Per-series values keyed by `ChartSeries.key`. */
|
|
23
|
+
values: Record<string, number>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AreaChartProps {
|
|
27
|
+
data: AreaDatum[]
|
|
28
|
+
series: ChartSeries[]
|
|
29
|
+
/** `overlap` (default) draws each series from the baseline; `stacked` accumulates them. */
|
|
30
|
+
mode?: 'overlap' | 'stacked'
|
|
31
|
+
/** Height of the plot area in px. Default 200. */
|
|
32
|
+
height?: number
|
|
33
|
+
/** Hover / keyboard tooltip. Default true. */
|
|
34
|
+
showTooltip?: boolean
|
|
35
|
+
/** Toggleable legend. Default true. */
|
|
36
|
+
showLegend?: boolean
|
|
37
|
+
class?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const VIEW_W = 400
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Multi-series area chart with gradient fills. Each series is tinted by a
|
|
44
|
+
* theme token (`color`), overlapping or stacked. The chart is focusable:
|
|
45
|
+
* Left / Right arrows (Home / End) move between points and show the tooltip.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* <AreaChart
|
|
50
|
+
* mode="stacked"
|
|
51
|
+
* series={[{ key: 'web', label: 'Web' }, { key: 'app', label: 'App', color: 'accent' }]}
|
|
52
|
+
* data={[
|
|
53
|
+
* { label: 'Jan', values: { web: 4, app: 2 } },
|
|
54
|
+
* { label: 'Feb', values: { web: 6, app: 3 } },
|
|
55
|
+
* ]}
|
|
56
|
+
* />
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function AreaChart(props: AreaChartProps): JSX.Element {
|
|
60
|
+
const uid = createUniqueId()
|
|
61
|
+
const height = () => props.height ?? 200
|
|
62
|
+
const stacked = () => props.mode === 'stacked'
|
|
63
|
+
const { hidden, toggle } = createHiddenSet()
|
|
64
|
+
const nav = createChartNav(() => props.data.length)
|
|
65
|
+
const val = (d: AreaDatum, key: string) => seriesValue(d.values, key, d.label)
|
|
66
|
+
const colorOf = (key: string) => {
|
|
67
|
+
const i = props.series.findIndex((s) => s.key === key)
|
|
68
|
+
return seriesColor(props.series[i].color, i)
|
|
69
|
+
}
|
|
70
|
+
const visible = () => props.series.filter((s) => !hidden().has(s.key))
|
|
71
|
+
const xFor = (i: number) => (props.data.length <= 1 ? VIEW_W / 2 : (i / (props.data.length - 1)) * VIEW_W)
|
|
72
|
+
|
|
73
|
+
const max = createMemo(() => {
|
|
74
|
+
const vis = visible()
|
|
75
|
+
const tops = props.data.map((d) =>
|
|
76
|
+
stacked() ? vis.reduce((s, x) => s + val(d, x.key), 0) : Math.max(0, ...vis.map((x) => val(d, x.key))),
|
|
77
|
+
)
|
|
78
|
+
return Math.max(...tops, 0) || 1
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const layers = createMemo(() => {
|
|
82
|
+
const acc = props.data.map(() => 0)
|
|
83
|
+
return visible().map((s) => {
|
|
84
|
+
const base = stacked() ? [...acc] : acc.map(() => 0)
|
|
85
|
+
const top = props.data.map((d, i) => base[i] + val(d, s.key))
|
|
86
|
+
if (stacked()) top.forEach((t, i) => (acc[i] = t))
|
|
87
|
+
const y = (v: number) => height() - (v / max()) * height()
|
|
88
|
+
const upper = top.map((t, i) => `${xFor(i)},${y(t)}`)
|
|
89
|
+
const lower = base.map((b, i) => `${xFor(i)},${y(b)}`).reverse()
|
|
90
|
+
return {
|
|
91
|
+
key: s.key,
|
|
92
|
+
color: colorOf(s.key),
|
|
93
|
+
line: `M ${upper.join(' L ')}`,
|
|
94
|
+
area: `M ${upper.join(' L ')} L ${lower.join(' L ')} Z`,
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const summary = () =>
|
|
100
|
+
`${stacked() ? 'Stacked area' : 'Area'} chart. ` +
|
|
101
|
+
props.data
|
|
102
|
+
.map((d) => `${d.label}: ${props.series.map((s) => `${s.label} ${val(d, s.key)}`).join(', ')}`)
|
|
103
|
+
.join('; ')
|
|
104
|
+
|
|
105
|
+
let svgRef: SVGSVGElement | undefined
|
|
106
|
+
const onPointerMove = (e: PointerEvent) => {
|
|
107
|
+
if (!svgRef || props.data.length === 0) return
|
|
108
|
+
const rect = svgRef.getBoundingClientRect()
|
|
109
|
+
const f = rect.width === 0 ? 0 : (e.clientX - rect.left) / rect.width
|
|
110
|
+
nav.setActive(
|
|
111
|
+
props.data.length <= 1 ? 0 : Math.round(Math.min(1, Math.max(0, f)) * (props.data.length - 1)),
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
const frac = () => {
|
|
115
|
+
const i = nav.active() ?? 0
|
|
116
|
+
return props.data.length <= 1 ? 0.5 : i / (props.data.length - 1)
|
|
117
|
+
}
|
|
118
|
+
const activeDatum = () => {
|
|
119
|
+
const i = nav.active()
|
|
120
|
+
return i === null ? undefined : props.data[i]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div class={cn('w-full', props.class)}>
|
|
125
|
+
<Show when={props.showLegend ?? true}>
|
|
126
|
+
<ChartLegend
|
|
127
|
+
class="mb-2"
|
|
128
|
+
hidden={hidden()}
|
|
129
|
+
onToggle={toggle}
|
|
130
|
+
items={props.series.map((s, i) => ({ key: s.key, label: s.label, color: seriesColor(s.color, i) }))}
|
|
131
|
+
/>
|
|
132
|
+
</Show>
|
|
133
|
+
<div
|
|
134
|
+
class={cn('relative w-full', FOCUS_CLASS)}
|
|
135
|
+
tabindex="0"
|
|
136
|
+
role="img"
|
|
137
|
+
aria-label={summary()}
|
|
138
|
+
data-chart="area"
|
|
139
|
+
data-mode={props.mode ?? 'overlap'}
|
|
140
|
+
onKeyDown={nav.onKeyDown}
|
|
141
|
+
onFocus={nav.onFocus}
|
|
142
|
+
onBlur={nav.onBlur}
|
|
143
|
+
>
|
|
144
|
+
<svg
|
|
145
|
+
ref={svgRef}
|
|
146
|
+
class="block w-full overflow-visible"
|
|
147
|
+
viewBox={`0 0 ${VIEW_W} ${height()}`}
|
|
148
|
+
preserveAspectRatio="none"
|
|
149
|
+
height={height()}
|
|
150
|
+
aria-hidden="true"
|
|
151
|
+
onPointerMove={onPointerMove}
|
|
152
|
+
onPointerLeave={() => nav.setActive(null)}
|
|
153
|
+
>
|
|
154
|
+
<defs>
|
|
155
|
+
<For each={props.series}>
|
|
156
|
+
{(s, i) => (
|
|
157
|
+
<linearGradient id={`${uid}-${s.key}`} x1="0" y1="0" x2="0" y2="1">
|
|
158
|
+
<stop offset="0%" stop-color={seriesColor(s.color, i())} stop-opacity="0.55" />
|
|
159
|
+
<stop offset="100%" stop-color={seriesColor(s.color, i())} stop-opacity="0.05" />
|
|
160
|
+
</linearGradient>
|
|
161
|
+
)}
|
|
162
|
+
</For>
|
|
163
|
+
</defs>
|
|
164
|
+
<For each={layers()}>
|
|
165
|
+
{(l) => (
|
|
166
|
+
<g data-series={l.key}>
|
|
167
|
+
<path d={l.area} fill={`url(#${uid}-${l.key})`} stroke="none" data-area />
|
|
168
|
+
<path
|
|
169
|
+
d={l.line}
|
|
170
|
+
fill="none"
|
|
171
|
+
stroke={l.color}
|
|
172
|
+
stroke-width={2}
|
|
173
|
+
stroke-linejoin="round"
|
|
174
|
+
vector-effect="non-scaling-stroke"
|
|
175
|
+
/>
|
|
176
|
+
</g>
|
|
177
|
+
)}
|
|
178
|
+
</For>
|
|
179
|
+
<Show when={nav.active() !== null}>
|
|
180
|
+
<line
|
|
181
|
+
data-cursor
|
|
182
|
+
x1={xFor(nav.active() ?? 0)}
|
|
183
|
+
x2={xFor(nav.active() ?? 0)}
|
|
184
|
+
y1={0}
|
|
185
|
+
y2={height()}
|
|
186
|
+
stroke="hsl(var(--border))"
|
|
187
|
+
stroke-width={1}
|
|
188
|
+
vector-effect="non-scaling-stroke"
|
|
189
|
+
/>
|
|
190
|
+
</Show>
|
|
191
|
+
</svg>
|
|
192
|
+
<Show when={(props.showTooltip ?? true) && activeDatum()}>
|
|
193
|
+
{(d) => (
|
|
194
|
+
<ChartTooltip
|
|
195
|
+
title={d().label}
|
|
196
|
+
left={`${frac() * 100}%`}
|
|
197
|
+
anchor={frac()}
|
|
198
|
+
rows={visible().map((s) => ({ label: s.label, value: val(d(), s.key), color: colorOf(s.key) }))}
|
|
199
|
+
/>
|
|
200
|
+
)}
|
|
201
|
+
</Show>
|
|
202
|
+
</div>
|
|
203
|
+
<ChartXLabels labels={props.data.map((d) => d.label)} />
|
|
204
|
+
</div>
|
|
205
|
+
)
|
|
206
|
+
}
|
package/src/charts/BarChart.tsx
CHANGED
|
@@ -2,24 +2,52 @@
|
|
|
2
2
|
// SVG, no charting library). Bar heights are percentages of the tallest
|
|
3
3
|
// value so the whole chart reflows with its container, and height changes
|
|
4
4
|
// animate via `transition-all`. Colors come from theme tokens only.
|
|
5
|
-
import { For, type JSX } from 'solid-js'
|
|
5
|
+
import { createMemo, For, Show, type JSX } from 'solid-js'
|
|
6
6
|
|
|
7
7
|
import { cn } from '../lib/cn'
|
|
8
|
+
import {
|
|
9
|
+
ChartLegend,
|
|
10
|
+
ChartTooltip,
|
|
11
|
+
FOCUS_CLASS,
|
|
12
|
+
createChartNav,
|
|
13
|
+
createHiddenSet,
|
|
14
|
+
seriesColor,
|
|
15
|
+
seriesValue,
|
|
16
|
+
type ChartSeries,
|
|
17
|
+
} from './shared'
|
|
8
18
|
|
|
9
19
|
export interface BarDatum {
|
|
10
20
|
label: string
|
|
11
|
-
|
|
21
|
+
/** Value for `mode="single"` (required there). */
|
|
22
|
+
value?: number
|
|
23
|
+
/** Per-series values keyed by `ChartSeries.key`, for `stacked` / `grouped`. */
|
|
24
|
+
values?: Record<string, number>
|
|
12
25
|
}
|
|
13
26
|
|
|
27
|
+
export type BarChartMode = 'single' | 'stacked' | 'grouped'
|
|
28
|
+
|
|
14
29
|
export interface BarChartProps {
|
|
15
30
|
data: BarDatum[]
|
|
16
31
|
/** Height of the plot area in px. Default 160. */
|
|
17
32
|
height?: number
|
|
18
33
|
/** Fill color token. Default 'primary'. */
|
|
19
34
|
tone?: 'primary' | 'accent'
|
|
35
|
+
/**
|
|
36
|
+
* `single` (default) draws one bar per datum from `value`; `stacked` and
|
|
37
|
+
* `grouped` draw one segment / bar per entry of `series` from `values`.
|
|
38
|
+
*/
|
|
39
|
+
mode?: BarChartMode
|
|
40
|
+
/** Series definitions, required for `stacked` / `grouped`. */
|
|
41
|
+
series?: ChartSeries[]
|
|
42
|
+
/** Hover / keyboard tooltip in the multi-series modes. Default true. */
|
|
43
|
+
showTooltip?: boolean
|
|
44
|
+
/** Show a toggleable legend in the multi-series modes. Default true. */
|
|
45
|
+
showLegend?: boolean
|
|
20
46
|
class?: string
|
|
21
47
|
}
|
|
22
48
|
|
|
49
|
+
const VIEW_W = 400
|
|
50
|
+
|
|
23
51
|
/**
|
|
24
52
|
* Vertical bar chart rendered with flexbox: each bar's height is `value /
|
|
25
53
|
* max` of the plot area, filled with a theme token, with its label truncated
|
|
@@ -31,9 +59,22 @@ export interface BarChartProps {
|
|
|
31
59
|
* ```
|
|
32
60
|
*/
|
|
33
61
|
export function BarChart(props: BarChartProps): JSX.Element {
|
|
62
|
+
return (
|
|
63
|
+
<Show when={(props.mode ?? 'single') !== 'single'} fallback={<SingleBars {...props} />}>
|
|
64
|
+
<MultiBars {...props} />
|
|
65
|
+
</Show>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function SingleBars(props: BarChartProps): JSX.Element {
|
|
70
|
+
const value = (d: BarDatum): number => {
|
|
71
|
+
if (d.value === undefined)
|
|
72
|
+
throw new Error(`BarChart datum "${d.label}" needs a numeric "value" in single mode`)
|
|
73
|
+
return d.value
|
|
74
|
+
}
|
|
34
75
|
const height = () => props.height ?? 160
|
|
35
76
|
const tone = () => props.tone ?? 'primary'
|
|
36
|
-
const max = () => Math.max(...props.data.map(
|
|
77
|
+
const max = () => Math.max(...props.data.map(value), 0) || 1
|
|
37
78
|
const barClass = () => (tone() === 'accent' ? 'bg-accent' : 'bg-primary')
|
|
38
79
|
|
|
39
80
|
return (
|
|
@@ -42,9 +83,9 @@ export function BarChart(props: BarChartProps): JSX.Element {
|
|
|
42
83
|
{(datum) => (
|
|
43
84
|
<div class="flex h-full min-w-0 flex-1 flex-col items-center justify-end gap-1">
|
|
44
85
|
<div
|
|
45
|
-
title={`${datum.label}: ${datum
|
|
86
|
+
title={`${datum.label}: ${value(datum)}`}
|
|
46
87
|
class={cn('w-full rounded-t transition-all', barClass())}
|
|
47
|
-
style={{ height: `${Math.max((datum
|
|
88
|
+
style={{ height: `${Math.max((value(datum) / max()) * 100, 0)}%` }}
|
|
48
89
|
/>
|
|
49
90
|
<span class="w-full truncate text-center text-xs text-muted-foreground">{datum.label}</span>
|
|
50
91
|
</div>
|
|
@@ -53,3 +94,198 @@ export function BarChart(props: BarChartProps): JSX.Element {
|
|
|
53
94
|
</div>
|
|
54
95
|
)
|
|
55
96
|
}
|
|
97
|
+
|
|
98
|
+
function MultiBars(props: BarChartProps): JSX.Element {
|
|
99
|
+
const height = () => props.height ?? 160
|
|
100
|
+
const stacked = () => props.mode === 'stacked'
|
|
101
|
+
const series = () => {
|
|
102
|
+
if (!props.series || props.series.length === 0)
|
|
103
|
+
throw new Error(`BarChart mode="${props.mode}" requires a non-empty "series" prop`)
|
|
104
|
+
return props.series
|
|
105
|
+
}
|
|
106
|
+
const { hidden, toggle } = createHiddenSet()
|
|
107
|
+
const nav = createChartNav(() => props.data.length)
|
|
108
|
+
const visible = () => series().filter((s) => !hidden().has(s.key))
|
|
109
|
+
const val = (d: BarDatum, key: string) => seriesValue(d.values, key, d.label)
|
|
110
|
+
|
|
111
|
+
const colorOf = (key: string) => {
|
|
112
|
+
const i = series().findIndex((s) => s.key === key)
|
|
113
|
+
return seriesColor(series()[i].color, i)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const max = createMemo(() => {
|
|
117
|
+
const vis = visible()
|
|
118
|
+
const perCat = props.data.map((d) =>
|
|
119
|
+
stacked()
|
|
120
|
+
? vis.reduce((sum, s) => sum + val(d, s.key), 0)
|
|
121
|
+
: Math.max(0, ...vis.map((s) => val(d, s.key))),
|
|
122
|
+
)
|
|
123
|
+
return Math.max(...perCat, 0) || 1
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const bars = createMemo(() => {
|
|
127
|
+
const n = props.data.length
|
|
128
|
+
const band = VIEW_W / Math.max(n, 1)
|
|
129
|
+
const vis = visible()
|
|
130
|
+
const m = vis.length
|
|
131
|
+
const out: {
|
|
132
|
+
cat: number
|
|
133
|
+
key: string
|
|
134
|
+
label: string
|
|
135
|
+
value: number
|
|
136
|
+
x: number
|
|
137
|
+
y: number
|
|
138
|
+
w: number
|
|
139
|
+
h: number
|
|
140
|
+
color: string
|
|
141
|
+
}[] = []
|
|
142
|
+
props.data.forEach((d, cat) => {
|
|
143
|
+
if (stacked()) {
|
|
144
|
+
const w = band * 0.6
|
|
145
|
+
const x = cat * band + (band - w) / 2
|
|
146
|
+
let acc = 0
|
|
147
|
+
vis.forEach((s) => {
|
|
148
|
+
const v = val(d, s.key)
|
|
149
|
+
const h = (v / max()) * height()
|
|
150
|
+
acc += h
|
|
151
|
+
out.push({
|
|
152
|
+
cat,
|
|
153
|
+
key: s.key,
|
|
154
|
+
label: s.label,
|
|
155
|
+
value: v,
|
|
156
|
+
x,
|
|
157
|
+
y: height() - acc,
|
|
158
|
+
w,
|
|
159
|
+
h,
|
|
160
|
+
color: colorOf(s.key),
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
} else {
|
|
164
|
+
const gap = 2
|
|
165
|
+
const groupW = band * 0.8
|
|
166
|
+
const w = m > 0 ? (groupW - gap * (m - 1)) / m : 0
|
|
167
|
+
vis.forEach((s, k) => {
|
|
168
|
+
const v = val(d, s.key)
|
|
169
|
+
const h = (v / max()) * height()
|
|
170
|
+
out.push({
|
|
171
|
+
cat,
|
|
172
|
+
key: s.key,
|
|
173
|
+
label: s.label,
|
|
174
|
+
value: v,
|
|
175
|
+
x: cat * band + band * 0.1 + k * (w + gap),
|
|
176
|
+
y: height() - h,
|
|
177
|
+
w,
|
|
178
|
+
h,
|
|
179
|
+
color: colorOf(s.key),
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
return out
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const summary = () =>
|
|
188
|
+
`${props.mode === 'stacked' ? 'Stacked' : 'Grouped'} bar chart. ` +
|
|
189
|
+
props.data
|
|
190
|
+
.map(
|
|
191
|
+
(d) =>
|
|
192
|
+
`${d.label}: ${series()
|
|
193
|
+
.map((s) => `${s.label} ${val(d, s.key)}`)
|
|
194
|
+
.join(', ')}`,
|
|
195
|
+
)
|
|
196
|
+
.join('; ')
|
|
197
|
+
|
|
198
|
+
let svgRef: SVGSVGElement | undefined
|
|
199
|
+
const onPointerMove = (e: PointerEvent) => {
|
|
200
|
+
if (!svgRef || props.data.length === 0) return
|
|
201
|
+
const rect = svgRef.getBoundingClientRect()
|
|
202
|
+
const f = rect.width === 0 ? 0 : (e.clientX - rect.left) / rect.width
|
|
203
|
+
nav.setActive(Math.min(props.data.length - 1, Math.max(0, Math.floor(f * props.data.length))))
|
|
204
|
+
}
|
|
205
|
+
const activeDatum = () => {
|
|
206
|
+
const i = nav.active()
|
|
207
|
+
return i === null ? undefined : props.data[i]
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<div class={cn('w-full', props.class)}>
|
|
212
|
+
<Show when={props.showLegend ?? true}>
|
|
213
|
+
<ChartLegend
|
|
214
|
+
class="mb-2"
|
|
215
|
+
hidden={hidden()}
|
|
216
|
+
onToggle={toggle}
|
|
217
|
+
items={series().map((s, i) => ({ key: s.key, label: s.label, color: seriesColor(s.color, i) }))}
|
|
218
|
+
/>
|
|
219
|
+
</Show>
|
|
220
|
+
<div
|
|
221
|
+
class={cn('relative w-full', FOCUS_CLASS)}
|
|
222
|
+
tabindex="0"
|
|
223
|
+
role="img"
|
|
224
|
+
aria-label={summary()}
|
|
225
|
+
data-chart="bar"
|
|
226
|
+
data-mode={props.mode}
|
|
227
|
+
onKeyDown={nav.onKeyDown}
|
|
228
|
+
onFocus={nav.onFocus}
|
|
229
|
+
onBlur={nav.onBlur}
|
|
230
|
+
>
|
|
231
|
+
<svg
|
|
232
|
+
ref={svgRef}
|
|
233
|
+
class="block w-full overflow-visible"
|
|
234
|
+
viewBox={`0 0 ${VIEW_W} ${height()}`}
|
|
235
|
+
preserveAspectRatio="none"
|
|
236
|
+
height={height()}
|
|
237
|
+
aria-hidden="true"
|
|
238
|
+
onPointerMove={onPointerMove}
|
|
239
|
+
onPointerLeave={() => nav.setActive(null)}
|
|
240
|
+
>
|
|
241
|
+
<Show when={nav.active() !== null}>
|
|
242
|
+
<rect
|
|
243
|
+
x={(nav.active() ?? 0) * (VIEW_W / props.data.length)}
|
|
244
|
+
y={0}
|
|
245
|
+
width={VIEW_W / props.data.length}
|
|
246
|
+
height={height()}
|
|
247
|
+
fill="hsl(var(--muted) / 0.4)"
|
|
248
|
+
/>
|
|
249
|
+
</Show>
|
|
250
|
+
<For each={bars()}>
|
|
251
|
+
{(b) => (
|
|
252
|
+
<rect
|
|
253
|
+
data-bar
|
|
254
|
+
data-category={props.data[b.cat].label}
|
|
255
|
+
data-series={b.key}
|
|
256
|
+
data-value={b.value}
|
|
257
|
+
x={b.x}
|
|
258
|
+
y={b.y}
|
|
259
|
+
width={b.w}
|
|
260
|
+
height={b.h}
|
|
261
|
+
rx={1}
|
|
262
|
+
fill={b.color}
|
|
263
|
+
/>
|
|
264
|
+
)}
|
|
265
|
+
</For>
|
|
266
|
+
</svg>
|
|
267
|
+
<Show when={props.showTooltip ?? true}>
|
|
268
|
+
<Show when={activeDatum()}>
|
|
269
|
+
{(d) => (
|
|
270
|
+
<ChartTooltip
|
|
271
|
+
title={d().label}
|
|
272
|
+
left={`${(((nav.active() ?? 0) + 0.5) / props.data.length) * 100}%`}
|
|
273
|
+
anchor={((nav.active() ?? 0) + 0.5) / props.data.length}
|
|
274
|
+
rows={visible().map((s) => ({
|
|
275
|
+
label: s.label,
|
|
276
|
+
value: val(d(), s.key),
|
|
277
|
+
color: colorOf(s.key),
|
|
278
|
+
}))}
|
|
279
|
+
/>
|
|
280
|
+
)}
|
|
281
|
+
</Show>
|
|
282
|
+
</Show>
|
|
283
|
+
</div>
|
|
284
|
+
<div class="mt-1 flex w-full text-xs text-muted-foreground">
|
|
285
|
+
<For each={props.data}>
|
|
286
|
+
{(d) => <span class="min-w-0 flex-1 truncate text-center">{d.label}</span>}
|
|
287
|
+
</For>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { For, Show, type JSX } from 'solid-js'
|
|
8
8
|
|
|
9
9
|
import { cn } from '../lib/cn'
|
|
10
|
+
import { sliceAngles } from './shared'
|
|
10
11
|
|
|
11
12
|
export type DonutTone = 'primary' | 'accent' | 'destructive' | 'emit' | 'received' | 'net'
|
|
12
13
|
|
|
@@ -71,17 +72,13 @@ export function DonutChart(props: DonutChartProps): JSX.Element {
|
|
|
71
72
|
const circumference = () => 2 * Math.PI * radius()
|
|
72
73
|
const total = () => props.data.reduce((sum, d) => sum + d.value, 0)
|
|
73
74
|
|
|
74
|
-
const arcs = () =>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
cumulative += arcLength
|
|
82
|
-
return { segment, index, arcLength, offset }
|
|
83
|
-
})
|
|
84
|
-
}
|
|
75
|
+
const arcs = () =>
|
|
76
|
+
sliceAngles(props.data.map((d) => d.value)).map((slice) => ({
|
|
77
|
+
segment: props.data[slice.index],
|
|
78
|
+
index: slice.index,
|
|
79
|
+
arcLength: (slice.angle / 360) * circumference(),
|
|
80
|
+
offset: (slice.start / 360) * circumference(),
|
|
81
|
+
}))
|
|
85
82
|
|
|
86
83
|
return (
|
|
87
84
|
<div
|
package/src/charts/LineChart.tsx
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { createSignal, For, Show, type JSX } from 'solid-js'
|
|
7
7
|
|
|
8
8
|
import { cn } from '../lib/cn'
|
|
9
|
+
import { ChartXLabels } from './shared'
|
|
9
10
|
|
|
10
11
|
export interface LineSeries {
|
|
11
12
|
name?: string
|
|
@@ -255,9 +256,7 @@ export function LineChart(props: LineChartProps): JSX.Element {
|
|
|
255
256
|
</div>
|
|
256
257
|
|
|
257
258
|
<Show when={props.labels && props.labels.length > 0}>
|
|
258
|
-
<
|
|
259
|
-
<For each={props.labels}>{(label) => <span class="flex-1 truncate text-center">{label}</span>}</For>
|
|
260
|
-
</div>
|
|
259
|
+
<ChartXLabels labels={props.labels ?? []} />
|
|
261
260
|
</Show>
|
|
262
261
|
</div>
|
|
263
262
|
)
|