@kolkrabbi/kol-component 0.6.0 → 0.7.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/package.json +4 -2
- package/src/chess/ChessPiece.jsx +191 -0
- package/src/chess/apparatus/AlternativeControlsMock.jsx +460 -0
- package/src/chess/apparatus/ChessAnalysisLayout.jsx +24 -0
- package/src/chess/apparatus/ChessBoard.jsx +210 -0
- package/src/chess/apparatus/ChessBoardFullscreen.jsx +49 -0
- package/src/chess/apparatus/ChessBoardWithControls.jsx +41 -0
- package/src/chess/apparatus/ChessBoardWithSidebar.jsx +122 -0
- package/src/chess/apparatus/ChessSidebar.jsx +131 -0
- package/src/chess/apparatus/GameArchiveTable.jsx +493 -0
- package/src/chess/apparatus/GameSelector.jsx +47 -0
- package/src/chess/apparatus/NotationPanel.jsx +70 -0
- package/src/chess/apparatus/PlaybackControls.jsx +52 -0
- package/src/chess/apparatus/VariationTree.jsx +137 -0
- package/src/chess/assets/chess-extra-set/set-1/bishop-1.svg +6 -0
- package/src/chess/assets/chess-extra-set/set-1/bishop.svg +6 -0
- package/src/chess/assets/chess-extra-set/set-1/king-1.svg +9 -0
- package/src/chess/assets/chess-extra-set/set-1/king.svg +7 -0
- package/src/chess/assets/chess-extra-set/set-1/knight-1.svg +8 -0
- package/src/chess/assets/chess-extra-set/set-1/knight.svg +7 -0
- package/src/chess/assets/chess-extra-set/set-1/pawn-1.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-1/pawn.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-1/queen-1.svg +11 -0
- package/src/chess/assets/chess-extra-set/set-1/queen.svg +6 -0
- package/src/chess/assets/chess-extra-set/set-1/rook-1.svg +7 -0
- package/src/chess/assets/chess-extra-set/set-1/rook.svg +11 -0
- package/src/chess/assets/chess-extra-set/set-2/bishop-1.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/bishop.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/king-1.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/king.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/knight-1.svg +5 -0
- package/src/chess/assets/chess-extra-set/set-2/knight.svg +5 -0
- package/src/chess/assets/chess-extra-set/set-2/pawn-1.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/pawn.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/queen-1.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/queen.svg +4 -0
- package/src/chess/assets/chess-extra-set/set-2/rook-1.svg +5 -0
- package/src/chess/assets/chess-extra-set/set-2/rook.svg +5 -0
- package/src/chess/assets/chess-extra-set/set-3/bishop-1.svg +10 -0
- package/src/chess/assets/chess-extra-set/set-3/bishop.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-3/king-1.svg +12 -0
- package/src/chess/assets/chess-extra-set/set-3/king.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-3/knight-1.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-3/knight.svg +10 -0
- package/src/chess/assets/chess-extra-set/set-3/pawn-1.svg +10 -0
- package/src/chess/assets/chess-extra-set/set-3/pawn.svg +10 -0
- package/src/chess/assets/chess-extra-set/set-3/queen-1.svg +10 -0
- package/src/chess/assets/chess-extra-set/set-3/queen.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-3/rook-1.svg +3 -0
- package/src/chess/assets/chess-extra-set/set-3/rook.svg +11 -0
- package/src/chess/assets/chess-vector-set/ABoard.svg +164 -0
- package/src/chess/assets/chess-vector-set/BBiskup.svg +12 -0
- package/src/chess/assets/chess-vector-set/BKing.svg +15 -0
- package/src/chess/assets/chess-vector-set/BKnight.svg +13 -0
- package/src/chess/assets/chess-vector-set/BPawn.svg +7 -0
- package/src/chess/assets/chess-vector-set/BQueen.svg +6 -0
- package/src/chess/assets/chess-vector-set/BRook.svg +12 -0
- package/src/chess/assets/chess-vector-set/WBiskup.svg +14 -0
- package/src/chess/assets/chess-vector-set/WKing.svg +13 -0
- package/src/chess/assets/chess-vector-set/WKnight.svg +10 -0
- package/src/chess/assets/chess-vector-set/WPawn.svg +8 -0
- package/src/chess/assets/chess-vector-set/WQueen.svg +17 -0
- package/src/chess/assets/chess-vector-set/WRook.svg +13 -0
- package/src/chess/context/ChessControlsContext.jsx +379 -0
- package/src/chess/dashboards/ChessHero.jsx +303 -0
- package/src/chess/index.js +32 -0
- package/src/chess/utils/parsePgnTree.js +97 -0
- package/src/dashboards/cards/DashAlertCard.jsx +44 -0
- package/src/dashboards/cards/DashChartCard.jsx +44 -0
- package/src/dashboards/cards/DashFeaturedCard.jsx +42 -0
- package/src/dashboards/cards/DashListCard.jsx +87 -0
- package/src/dashboards/cards/DashMetricCard.jsx +59 -0
- package/src/dashboards/cards/DashSlotCard.jsx +45 -0
- package/src/dashboards/cards/DashStackedBarCard.jsx +70 -0
- package/src/dashboards/cards/DashTableCard.jsx +31 -0
- package/src/dashboards/cards/_shared/CardHeader.jsx +15 -0
- package/src/dashboards/cards/_shared/LegendRow.jsx +25 -0
- package/src/dashboards/charts/Candlestick.jsx +79 -0
- package/src/dashboards/charts/DonutChart.jsx +87 -0
- package/src/dashboards/charts/Heatmap.jsx +79 -0
- package/src/dashboards/charts/Histogram.jsx +56 -0
- package/src/dashboards/charts/LineChart.jsx +157 -0
- package/src/dashboards/charts/ScatterPlot.jsx +93 -0
- package/src/dashboards/charts/Sparkline.jsx +63 -0
- package/src/dashboards/index.js +28 -0
- package/src/dashboards/layout/DashboardGrid.jsx +30 -0
- package/src/dashboards/layout/GridCard.jsx +44 -0
- package/src/dashboards/shared/DashTooltip.jsx +6 -0
- package/src/dashboards/shared/formatMetric.js +26 -0
- package/src/dashboards/shared/useChartTooltip.js +25 -0
- package/src/dashboards/shared/useCountUp.js +107 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import useChartTooltip from '../shared/useChartTooltip'
|
|
3
|
+
import DashTooltip from '../shared/DashTooltip'
|
|
4
|
+
|
|
5
|
+
const DonutChart = ({
|
|
6
|
+
segments = [],
|
|
7
|
+
size = 120,
|
|
8
|
+
thickness = 20,
|
|
9
|
+
centerLabel,
|
|
10
|
+
showLegend = false
|
|
11
|
+
}) => {
|
|
12
|
+
const total = useMemo(() => segments.reduce((sum, s) => sum + s.value, 0), [segments])
|
|
13
|
+
const { activeIndex, handlers, containerHandlers, containerRef } = useChartTooltip()
|
|
14
|
+
|
|
15
|
+
const radius = (size - thickness) / 2
|
|
16
|
+
const circumference = 2 * Math.PI * radius
|
|
17
|
+
const center = size / 2
|
|
18
|
+
|
|
19
|
+
const arcs = useMemo(() => {
|
|
20
|
+
let offset = 0
|
|
21
|
+
return segments.map((seg) => {
|
|
22
|
+
const pct = total > 0 ? seg.value / total : 0
|
|
23
|
+
const dash = pct * circumference
|
|
24
|
+
const gap = circumference - dash
|
|
25
|
+
const rotation = (offset / total) * 360
|
|
26
|
+
offset += seg.value
|
|
27
|
+
return { ...seg, dash, gap, rotation, pct }
|
|
28
|
+
})
|
|
29
|
+
}, [segments, total, circumference])
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="inline-flex flex-col items-center gap-3">
|
|
33
|
+
<div
|
|
34
|
+
className="relative"
|
|
35
|
+
style={{ width: size, height: size }}
|
|
36
|
+
ref={containerRef}
|
|
37
|
+
{...containerHandlers}
|
|
38
|
+
>
|
|
39
|
+
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
|
|
40
|
+
{arcs.map((arc, i) => (
|
|
41
|
+
<circle
|
|
42
|
+
key={i}
|
|
43
|
+
className={`dash-donut-segment ${activeIndex === i ? 'dash-chart-element--active' : ''}`}
|
|
44
|
+
cx={center}
|
|
45
|
+
cy={center}
|
|
46
|
+
r={radius}
|
|
47
|
+
fill="none"
|
|
48
|
+
stroke={arc.color}
|
|
49
|
+
strokeWidth={thickness}
|
|
50
|
+
strokeDasharray={`${arc.dash} ${arc.gap}`}
|
|
51
|
+
transform={`rotate(${arc.rotation - 90} ${center} ${center})`}
|
|
52
|
+
style={{ cursor: 'default' }}
|
|
53
|
+
{...handlers(i)}
|
|
54
|
+
/>
|
|
55
|
+
))}
|
|
56
|
+
</svg>
|
|
57
|
+
|
|
58
|
+
{centerLabel && (
|
|
59
|
+
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
|
60
|
+
<span className="dash-body text-fg-88">{centerLabel}</span>
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
<DashTooltip visible={activeIndex !== null}>
|
|
65
|
+
{activeIndex !== null && arcs[activeIndex] && (
|
|
66
|
+
<span className="dash-caption">
|
|
67
|
+
{arcs[activeIndex].label ? `${arcs[activeIndex].label}: ` : ''}{arcs[activeIndex].value} ({(arcs[activeIndex].pct * 100).toFixed(1)}%)
|
|
68
|
+
</span>
|
|
69
|
+
)}
|
|
70
|
+
</DashTooltip>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{showLegend && (
|
|
74
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 justify-center">
|
|
75
|
+
{segments.map((seg, i) => (
|
|
76
|
+
<div key={i} className="flex items-center gap-1.5">
|
|
77
|
+
<span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: seg.color }} />
|
|
78
|
+
<span className="dash-caption text-fg-64">{seg.label}</span>
|
|
79
|
+
</div>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default DonutChart
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import useChartTooltip from '../shared/useChartTooltip'
|
|
2
|
+
import DashTooltip from '../shared/DashTooltip'
|
|
3
|
+
|
|
4
|
+
const Heatmap = ({
|
|
5
|
+
data = [],
|
|
6
|
+
rows = [],
|
|
7
|
+
cols = [],
|
|
8
|
+
colorScale = ['var(--kol-palette-blue)', 'var(--kol-palette-yellow)'],
|
|
9
|
+
cellSize = 32,
|
|
10
|
+
fill = false
|
|
11
|
+
}) => {
|
|
12
|
+
const { activeIndex, handlers, containerHandlers, containerRef } = useChartTooltip()
|
|
13
|
+
|
|
14
|
+
const cells = Array.isArray(data[0])
|
|
15
|
+
? data.flatMap((row, r) => row.map((value, c) => ({ row: r, col: c, value })))
|
|
16
|
+
: data
|
|
17
|
+
|
|
18
|
+
const maxValue = Math.max(...cells.map(c => c.value), 1)
|
|
19
|
+
const colCount = cols.length || (Array.isArray(data[0]) ? data[0].length : Math.max(...cells.map(c => c.col)) + 1)
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className={`flex gap-2 min-w-0${fill ? ' h-full' : ''}`} ref={containerRef} {...containerHandlers}>
|
|
23
|
+
{rows.length > 0 && (
|
|
24
|
+
<div
|
|
25
|
+
className="grid shrink-0 items-center"
|
|
26
|
+
style={{ gridTemplateRows: `repeat(${rows.length}, ${fill ? '1fr' : `${cellSize}px`})`, gap: '4px' }}
|
|
27
|
+
>
|
|
28
|
+
{rows.map((label, i) => (
|
|
29
|
+
<span key={i} className="dash-caption text-fg-64 text-right">{label}</span>
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
)}
|
|
33
|
+
|
|
34
|
+
<div className="flex-1 flex flex-col gap-2 min-w-0">
|
|
35
|
+
<div
|
|
36
|
+
className={`grid gap-1${fill ? ' flex-1' : ''}`}
|
|
37
|
+
style={{
|
|
38
|
+
gridTemplateColumns: `repeat(${colCount}, 1fr)`,
|
|
39
|
+
gridAutoRows: fill ? '1fr' : `${cellSize}px`
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
{cells.map((cell, idx) => {
|
|
43
|
+
const pct = Math.round((cell.value / maxValue) * 100)
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
key={idx}
|
|
47
|
+
className={`dash-heatmap-cell rounded-sm ${activeIndex === idx ? 'dash-chart-element--active' : ''}`}
|
|
48
|
+
style={{
|
|
49
|
+
backgroundColor: `color-mix(in srgb, ${colorScale[1]} ${pct}%, ${colorScale[0]})`
|
|
50
|
+
}}
|
|
51
|
+
{...handlers(idx)}
|
|
52
|
+
/>
|
|
53
|
+
)
|
|
54
|
+
})}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
{cols.length > 0 && (
|
|
58
|
+
<div className="flex justify-around">
|
|
59
|
+
{cols.map((label, i) => (
|
|
60
|
+
<span key={i} className="dash-caption text-fg-64 text-center">{label}</span>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
<DashTooltip visible={activeIndex !== null}>
|
|
66
|
+
{activeIndex !== null && cells[activeIndex] && (
|
|
67
|
+
<span className="dash-caption">
|
|
68
|
+
{rows[cells[activeIndex].row] ?? `R${cells[activeIndex].row}`},{' '}
|
|
69
|
+
{cols[cells[activeIndex].col] ?? `C${cells[activeIndex].col}`}:{' '}
|
|
70
|
+
{cells[activeIndex].value}
|
|
71
|
+
</span>
|
|
72
|
+
)}
|
|
73
|
+
</DashTooltip>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default Heatmap
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import useChartTooltip from '../shared/useChartTooltip'
|
|
2
|
+
import DashTooltip from '../shared/DashTooltip'
|
|
3
|
+
|
|
4
|
+
const Histogram = ({
|
|
5
|
+
data = [],
|
|
6
|
+
barColor = 'var(--kol-accent-primary)'
|
|
7
|
+
}) => {
|
|
8
|
+
const maxCount = Math.max(...data.map(d => d.count), 1)
|
|
9
|
+
const { activeIndex, handlers, containerHandlers, containerRef } = useChartTooltip()
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="flex min-w-0 flex-1 min-h-0">
|
|
13
|
+
<div className="flex flex-col justify-between pr-2 shrink-0">
|
|
14
|
+
<span className="dash-caption text-fg-64 text-right">{maxCount}</span>
|
|
15
|
+
<span className="dash-caption text-fg-64 text-right">{Math.round(maxCount / 2)}</span>
|
|
16
|
+
<span className="dash-caption text-fg-64 text-right">0</span>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div className="flex-1 flex flex-col gap-2 min-w-0" ref={containerRef} {...containerHandlers}>
|
|
20
|
+
<div className="relative flex items-end gap-1 flex-1">
|
|
21
|
+
{data.map((bucket, idx) => {
|
|
22
|
+
const pct = Math.round((bucket.count / maxCount) * 100)
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
key={idx}
|
|
26
|
+
className={`flex-1 flex items-end min-w-0 h-full ${activeIndex === idx ? 'dash-chart-element--active' : ''}`}
|
|
27
|
+
{...handlers(idx)}
|
|
28
|
+
>
|
|
29
|
+
<div
|
|
30
|
+
className="w-full rounded-t-sm"
|
|
31
|
+
style={{ height: `${pct}%`, backgroundColor: barColor }}
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
})}
|
|
36
|
+
|
|
37
|
+
<DashTooltip visible={activeIndex !== null}>
|
|
38
|
+
{activeIndex !== null && data[activeIndex] && (
|
|
39
|
+
<span className="dash-caption">{data[activeIndex].range}: {data[activeIndex].count}</span>
|
|
40
|
+
)}
|
|
41
|
+
</DashTooltip>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div className="flex items-start gap-1">
|
|
45
|
+
{data.map((bucket, idx) => (
|
|
46
|
+
<div key={idx} className="flex-1 flex flex-col items-center min-w-0 overflow-hidden">
|
|
47
|
+
<span className="dash-caption text-fg-64 truncate w-full text-center">{bucket.range}</span>
|
|
48
|
+
</div>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default Histogram
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import useChartTooltip from '../shared/useChartTooltip'
|
|
3
|
+
import DashTooltip from '../shared/DashTooltip'
|
|
4
|
+
|
|
5
|
+
const SVG_HEIGHT = 100
|
|
6
|
+
|
|
7
|
+
const normalizeAll = (seriesArr) => {
|
|
8
|
+
const allValues = seriesArr.flatMap(s => s.data.map(d => d.y))
|
|
9
|
+
const min = Math.min(...allValues)
|
|
10
|
+
const max = Math.max(...allValues)
|
|
11
|
+
const range = max - min || 1
|
|
12
|
+
|
|
13
|
+
return seriesArr.map(s => ({
|
|
14
|
+
...s,
|
|
15
|
+
points: s.data.map((d, i) => ({
|
|
16
|
+
x: (i / Math.max(s.data.length - 1, 1)) * 100,
|
|
17
|
+
y: SVG_HEIGHT - ((d.y - min) / range) * (SVG_HEIGHT * 0.8) - SVG_HEIGHT * 0.1,
|
|
18
|
+
raw: d
|
|
19
|
+
}))
|
|
20
|
+
}))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const buildPath = (points) =>
|
|
24
|
+
points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(' ')
|
|
25
|
+
|
|
26
|
+
const buildAreaPath = (linePath) =>
|
|
27
|
+
`${linePath} L 100 ${SVG_HEIGHT} L 0 ${SVG_HEIGHT} Z`
|
|
28
|
+
|
|
29
|
+
const LineChart = ({
|
|
30
|
+
data,
|
|
31
|
+
series,
|
|
32
|
+
height = 240,
|
|
33
|
+
fill = false,
|
|
34
|
+
xLabels,
|
|
35
|
+
yLabels,
|
|
36
|
+
showArea = false,
|
|
37
|
+
showDots = false
|
|
38
|
+
}) => {
|
|
39
|
+
const resolvedSeries = useMemo(() => {
|
|
40
|
+
if (series) return series
|
|
41
|
+
if (data) return [{ data, color: 'var(--kol-accent-primary)' }]
|
|
42
|
+
return []
|
|
43
|
+
}, [data, series])
|
|
44
|
+
|
|
45
|
+
const normalizedSeries = useMemo(() => normalizeAll(resolvedSeries), [resolvedSeries])
|
|
46
|
+
|
|
47
|
+
const { activeIndex, handlers, containerHandlers, containerRef } = useChartTooltip()
|
|
48
|
+
|
|
49
|
+
const gradientIds = normalizedSeries.map((_, i) => `line-area-grad-${i}`)
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className={`flex min-w-0${fill ? ' h-full' : ''}`}>
|
|
53
|
+
{yLabels && (
|
|
54
|
+
<div className="flex flex-col justify-between pr-2 shrink-0" style={fill ? undefined : { height }}>
|
|
55
|
+
{[...yLabels].reverse().map((label, i) => (
|
|
56
|
+
<span key={i} className="dash-caption text-fg-64 text-right">{label}</span>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
<div className="flex-1 flex flex-col gap-2 min-w-0" ref={containerRef} {...containerHandlers}>
|
|
62
|
+
<div className={`relative${fill ? ' flex-1' : ''}`} style={fill ? undefined : { height }}>
|
|
63
|
+
<svg
|
|
64
|
+
className="absolute inset-0 w-full h-full"
|
|
65
|
+
viewBox={`0 0 100 ${SVG_HEIGHT}`}
|
|
66
|
+
preserveAspectRatio="none"
|
|
67
|
+
>
|
|
68
|
+
<defs>
|
|
69
|
+
{normalizedSeries.map((s, i) => (showArea || s.fill) && (
|
|
70
|
+
<linearGradient key={i} id={gradientIds[i]} x1="0%" y1="0%" x2="0%" y2="100%">
|
|
71
|
+
<stop offset="0%" stopColor={s.color} stopOpacity="0.3" />
|
|
72
|
+
<stop offset="100%" stopColor={s.color} stopOpacity="0" />
|
|
73
|
+
</linearGradient>
|
|
74
|
+
))}
|
|
75
|
+
</defs>
|
|
76
|
+
|
|
77
|
+
{normalizedSeries.map((s, i) => {
|
|
78
|
+
const path = buildPath(s.points)
|
|
79
|
+
return (
|
|
80
|
+
<g key={i}>
|
|
81
|
+
{(showArea || s.fill) && (
|
|
82
|
+
<path
|
|
83
|
+
className="dash-line-area"
|
|
84
|
+
d={buildAreaPath(path)}
|
|
85
|
+
fill={`url(#${gradientIds[i]})`}
|
|
86
|
+
/>
|
|
87
|
+
)}
|
|
88
|
+
<path
|
|
89
|
+
className="dash-line-path"
|
|
90
|
+
d={path}
|
|
91
|
+
fill="none"
|
|
92
|
+
stroke={s.color}
|
|
93
|
+
strokeWidth="2"
|
|
94
|
+
strokeLinejoin="round"
|
|
95
|
+
strokeLinecap="round"
|
|
96
|
+
vectorEffect="non-scaling-stroke"
|
|
97
|
+
/>
|
|
98
|
+
</g>
|
|
99
|
+
)
|
|
100
|
+
})}
|
|
101
|
+
</svg>
|
|
102
|
+
|
|
103
|
+
{showDots && normalizedSeries.map((s, si) =>
|
|
104
|
+
s.points.map((p, pi) => (
|
|
105
|
+
<span
|
|
106
|
+
key={`${si}-${pi}`}
|
|
107
|
+
className={`absolute block w-2 h-2 rounded-full ${activeIndex === pi ? 'dash-chart-element--active' : ''}`}
|
|
108
|
+
style={{
|
|
109
|
+
backgroundColor: s.color,
|
|
110
|
+
left: `${p.x}%`,
|
|
111
|
+
top: `${p.y}%`,
|
|
112
|
+
transform: 'translate(-50%, -50%)'
|
|
113
|
+
}}
|
|
114
|
+
{...handlers(pi)}
|
|
115
|
+
/>
|
|
116
|
+
))
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
{!showDots && normalizedSeries[0]?.points.map((p, pi) => (
|
|
120
|
+
<span
|
|
121
|
+
key={pi}
|
|
122
|
+
className="absolute top-0 bottom-0"
|
|
123
|
+
style={{ left: `${p.x}%`, width: `${100 / Math.max(normalizedSeries[0].points.length - 1, 1)}%`, transform: 'translateX(-50%)' }}
|
|
124
|
+
{...handlers(pi)}
|
|
125
|
+
/>
|
|
126
|
+
))}
|
|
127
|
+
|
|
128
|
+
<DashTooltip visible={activeIndex !== null}>
|
|
129
|
+
{activeIndex !== null && (
|
|
130
|
+
<div className="flex flex-col gap-0.5">
|
|
131
|
+
{normalizedSeries.map((s, si) => {
|
|
132
|
+
const pt = s.points[activeIndex]
|
|
133
|
+
if (!pt) return null
|
|
134
|
+
return (
|
|
135
|
+
<span key={si} className="dash-caption">
|
|
136
|
+
{pt.raw.x != null ? `${pt.raw.x}: ` : ''}{pt.raw.y}
|
|
137
|
+
</span>
|
|
138
|
+
)
|
|
139
|
+
})}
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
</DashTooltip>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{xLabels && (
|
|
146
|
+
<div className="flex justify-between">
|
|
147
|
+
{xLabels.map((label, i) => (
|
|
148
|
+
<span key={i} className="dash-caption text-fg-64">{label}</span>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default LineChart
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import useChartTooltip from '../shared/useChartTooltip'
|
|
2
|
+
import DashTooltip from '../shared/DashTooltip'
|
|
3
|
+
|
|
4
|
+
const ScatterPlot = ({
|
|
5
|
+
data = [],
|
|
6
|
+
maxX = 200,
|
|
7
|
+
maxY = 3000,
|
|
8
|
+
xLabels = [],
|
|
9
|
+
yLabels = [],
|
|
10
|
+
pointColor = 'var(--kol-palette-blue)',
|
|
11
|
+
height = 320
|
|
12
|
+
}) => {
|
|
13
|
+
const formatY = (v) => v === 0 ? '0' : v >= 1000 ? `${(v / 1000).toFixed(1).replace('.0', '')}K` : v
|
|
14
|
+
const { activeIndex, handlers, containerHandlers, containerRef } = useChartTooltip()
|
|
15
|
+
|
|
16
|
+
const minY = yLabels.length > 0 ? Math.min(...yLabels) : 0
|
|
17
|
+
const rangeY = maxY - minY || 1
|
|
18
|
+
const scaleY = (v) => ((v - minY) / rangeY) * 90 + 5
|
|
19
|
+
const scaleX = (v) => (v / maxX) * 100
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex">
|
|
23
|
+
<div className="relative pr-2 shrink-0 w-10" style={{ height }}>
|
|
24
|
+
{yLabels.map((yValue) => (
|
|
25
|
+
<span
|
|
26
|
+
key={yValue}
|
|
27
|
+
className="absolute right-2 dash-caption text-fg-64"
|
|
28
|
+
style={{ bottom: `${scaleY(yValue)}%`, transform: 'translateY(50%)' }}
|
|
29
|
+
>
|
|
30
|
+
{formatY(yValue)}
|
|
31
|
+
</span>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
36
|
+
<div className="relative rounded" style={{ height }} ref={containerRef} {...containerHandlers}>
|
|
37
|
+
{yLabels.map((yValue) => (
|
|
38
|
+
<div
|
|
39
|
+
key={yValue}
|
|
40
|
+
className="absolute left-0 right-0 border-t border-fg-08"
|
|
41
|
+
style={{ bottom: `${scaleY(yValue)}%` }}
|
|
42
|
+
/>
|
|
43
|
+
))}
|
|
44
|
+
|
|
45
|
+
{xLabels.map((xValue) => (
|
|
46
|
+
<div
|
|
47
|
+
key={xValue}
|
|
48
|
+
className="absolute top-0 bottom-0 border-l border-fg-08"
|
|
49
|
+
style={{ left: `${scaleX(xValue)}%` }}
|
|
50
|
+
/>
|
|
51
|
+
))}
|
|
52
|
+
|
|
53
|
+
{data.map(point => (
|
|
54
|
+
<span
|
|
55
|
+
key={point.id}
|
|
56
|
+
className={`absolute block dash-scatter-point ${activeIndex === point.id ? 'dash-chart-element--active' : ''}`}
|
|
57
|
+
style={{
|
|
58
|
+
background: pointColor,
|
|
59
|
+
left: `${scaleX(point.x)}%`,
|
|
60
|
+
bottom: `${scaleY(point.y)}%`,
|
|
61
|
+
transform: 'translate(-50%, 50%)'
|
|
62
|
+
}}
|
|
63
|
+
{...handlers(point.id)}
|
|
64
|
+
/>
|
|
65
|
+
))}
|
|
66
|
+
|
|
67
|
+
<DashTooltip visible={activeIndex !== null}>
|
|
68
|
+
{activeIndex !== null && (() => {
|
|
69
|
+
const point = data.find(p => p.id === activeIndex)
|
|
70
|
+
if (!point) return null
|
|
71
|
+
return (
|
|
72
|
+
<div className="flex flex-col gap-0.5">
|
|
73
|
+
{point.id != null && <span className="dash-caption text-fg-64">{point.id}</span>}
|
|
74
|
+
<span className="dash-caption">x: {point.x}</span>
|
|
75
|
+
<span className="dash-caption">y: {point.y}</span>
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
})()}
|
|
79
|
+
</DashTooltip>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div className="flex justify-between pt-2">
|
|
83
|
+
<span className="dash-caption text-fg-64">0</span>
|
|
84
|
+
{xLabels.map((xValue) => (
|
|
85
|
+
<span key={xValue} className="dash-caption text-fg-64">{xValue}</span>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default ScatterPlot
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
const Sparkline = ({
|
|
4
|
+
data = [],
|
|
5
|
+
color = 'var(--kol-surface-on-primary)',
|
|
6
|
+
height = 24,
|
|
7
|
+
fill = false
|
|
8
|
+
}) => {
|
|
9
|
+
const path = useMemo(() => {
|
|
10
|
+
if (data.length < 2) return ''
|
|
11
|
+
const min = Math.min(...data)
|
|
12
|
+
const max = Math.max(...data)
|
|
13
|
+
const range = max - min || 1
|
|
14
|
+
|
|
15
|
+
return data
|
|
16
|
+
.map((v, i) => {
|
|
17
|
+
const x = (i / (data.length - 1)) * 100
|
|
18
|
+
const y = 100 - ((v - min) / range) * 80 - 10
|
|
19
|
+
return `${i === 0 ? 'M' : 'L'} ${x.toFixed(2)} ${y.toFixed(2)}`
|
|
20
|
+
})
|
|
21
|
+
.join(' ')
|
|
22
|
+
}, [data])
|
|
23
|
+
|
|
24
|
+
if (!path) return null
|
|
25
|
+
|
|
26
|
+
const id = useMemo(() => `spark-${Math.random().toString(36).slice(2, 8)}`, [])
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<svg
|
|
30
|
+
width="100%"
|
|
31
|
+
height={height}
|
|
32
|
+
viewBox="0 0 100 100"
|
|
33
|
+
preserveAspectRatio="none"
|
|
34
|
+
style={{ display: 'block' }}
|
|
35
|
+
>
|
|
36
|
+
{fill && (
|
|
37
|
+
<>
|
|
38
|
+
<defs>
|
|
39
|
+
<linearGradient id={id} x1="0%" y1="0%" x2="0%" y2="100%">
|
|
40
|
+
<stop offset="0%" stopColor={color} stopOpacity="0.25" />
|
|
41
|
+
<stop offset="100%" stopColor={color} stopOpacity="0" />
|
|
42
|
+
</linearGradient>
|
|
43
|
+
</defs>
|
|
44
|
+
<path
|
|
45
|
+
d={`${path} L 100 100 L 0 100 Z`}
|
|
46
|
+
fill={`url(#${id})`}
|
|
47
|
+
/>
|
|
48
|
+
</>
|
|
49
|
+
)}
|
|
50
|
+
<path
|
|
51
|
+
d={path}
|
|
52
|
+
fill="none"
|
|
53
|
+
stroke={color}
|
|
54
|
+
strokeWidth="2"
|
|
55
|
+
strokeLinejoin="round"
|
|
56
|
+
strokeLinecap="round"
|
|
57
|
+
vectorEffect="non-scaling-stroke"
|
|
58
|
+
/>
|
|
59
|
+
</svg>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default Sparkline
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Cards
|
|
2
|
+
export { default as DashMetricCard } from './cards/DashMetricCard'
|
|
3
|
+
export { default as DashStackedBarCard } from './cards/DashStackedBarCard'
|
|
4
|
+
export { default as DashChartCard } from './cards/DashChartCard'
|
|
5
|
+
export { default as DashListCard } from './cards/DashListCard'
|
|
6
|
+
export { default as DashFeaturedCard } from './cards/DashFeaturedCard'
|
|
7
|
+
export { default as DashAlertCard } from './cards/DashAlertCard'
|
|
8
|
+
export { default as DashSlotCard } from './cards/DashSlotCard'
|
|
9
|
+
export { default as DashTableCard } from './cards/DashTableCard'
|
|
10
|
+
|
|
11
|
+
// Chart visualizations
|
|
12
|
+
export { default as Histogram } from './charts/Histogram'
|
|
13
|
+
export { default as Candlestick } from './charts/Candlestick'
|
|
14
|
+
export { default as ScatterPlot } from './charts/ScatterPlot'
|
|
15
|
+
export { default as LineChart } from './charts/LineChart'
|
|
16
|
+
export { default as DonutChart } from './charts/DonutChart'
|
|
17
|
+
export { default as Sparkline } from './charts/Sparkline'
|
|
18
|
+
export { default as Heatmap } from './charts/Heatmap'
|
|
19
|
+
|
|
20
|
+
// Layout
|
|
21
|
+
export { default as DashboardGrid } from './layout/DashboardGrid'
|
|
22
|
+
export { default as GridCard } from './layout/GridCard'
|
|
23
|
+
|
|
24
|
+
// Shared
|
|
25
|
+
export { default as DashTooltip } from './shared/DashTooltip'
|
|
26
|
+
export { default as useChartTooltip } from './shared/useChartTooltip'
|
|
27
|
+
export { default as useCountUp } from './shared/useCountUp'
|
|
28
|
+
export { default as formatMetric, formatCompact, formatPercent, formatDelta } from './shared/formatMetric'
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DashboardGrid - Flexible grid system for chess dashboard layouts
|
|
5
|
+
*
|
|
6
|
+
* Layouts:
|
|
7
|
+
* - '4-col': 4 columns × 200px rows (default)
|
|
8
|
+
* - '6-col': 6 columns × 180px rows (dense)
|
|
9
|
+
* - 'board-focus': 3fr/2fr columns × 240px rows (board emphasis)
|
|
10
|
+
* - '3-col': 3 columns × 200px rows (simple)
|
|
11
|
+
*
|
|
12
|
+
* @param {string} layout - Grid configuration preset
|
|
13
|
+
* @param {string} className - Additional CSS classes
|
|
14
|
+
* @param {React.ReactNode} children - GridCard components
|
|
15
|
+
*/
|
|
16
|
+
const DashboardGrid = ({
|
|
17
|
+
layout = '4-col',
|
|
18
|
+
className = '',
|
|
19
|
+
children
|
|
20
|
+
}) => {
|
|
21
|
+
return (
|
|
22
|
+
<div style={{ containerType: 'inline-size' }}>
|
|
23
|
+
<div className={`dash-grid ${className}`.trim()}>
|
|
24
|
+
{children}
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default DashboardGrid
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GridCard - Wrapper for components in the dashboard grid
|
|
5
|
+
*
|
|
6
|
+
* Span configurations:
|
|
7
|
+
* - '1x1': Single cell (default)
|
|
8
|
+
* - '2x1': 2 columns × 1 row
|
|
9
|
+
* - '2x2': 2 columns × 2 rows
|
|
10
|
+
* - '3x2': 3 columns × 2 rows
|
|
11
|
+
* - '4x2': 4 columns × 2 rows (full width, double height)
|
|
12
|
+
* - '1x2': 1 column × 2 rows (tall card)
|
|
13
|
+
* - '3x1': 3 columns × 1 row
|
|
14
|
+
* - '4x1': 4 columns × 1 row (full width strip)
|
|
15
|
+
*
|
|
16
|
+
* @param {string} span - Grid span configuration
|
|
17
|
+
* @param {string} className - Additional CSS classes
|
|
18
|
+
* @param {React.ReactNode} children - Component to display
|
|
19
|
+
* @param {boolean} asCard - Wrap in card styling (for non-component content)
|
|
20
|
+
*/
|
|
21
|
+
const GridCard = ({
|
|
22
|
+
span = '1x1',
|
|
23
|
+
className = '',
|
|
24
|
+
children,
|
|
25
|
+
asCard = false
|
|
26
|
+
}) => {
|
|
27
|
+
const [cols, rows] = (span || '1x1').split('x').map(Number)
|
|
28
|
+
|
|
29
|
+
const style = {
|
|
30
|
+
gridColumn: `span ${cols || 1}`,
|
|
31
|
+
gridRow: `span ${rows || 1}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// If asCard, add card styling; otherwise just grid positioning
|
|
35
|
+
const cardClass = asCard ? 'dash-card' : ''
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div data-cols={cols || 1} style={style} className={`h-full ${cardClass} ${className}`.trim()}>
|
|
39
|
+
{children}
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default GridCard
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const formatCompact = (n) => {
|
|
2
|
+
if (n >= 1000000) {
|
|
3
|
+
return `${(n / 1000000).toFixed(1).replace(/\.0$/, '')}M`
|
|
4
|
+
}
|
|
5
|
+
if (n >= 1000) {
|
|
6
|
+
const formatted = (n / 1000).toFixed(n >= 10000 ? 0 : 1)
|
|
7
|
+
return `${formatted.replace(/\.0$/, '')}K`
|
|
8
|
+
}
|
|
9
|
+
return n.toLocaleString()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const formatPercent = (n) => {
|
|
13
|
+
if (!Number.isFinite(n)) return '0'
|
|
14
|
+
const formatted = n >= 10 ? n.toFixed(0) : n.toFixed(1)
|
|
15
|
+
return `${formatted.replace(/\.0$/, '')}%`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const formatDelta = (n) => {
|
|
19
|
+
if (!Number.isFinite(n)) return '0%'
|
|
20
|
+
const sign = n > 0 ? '+' : ''
|
|
21
|
+
const formatted = Math.abs(n) >= 10 ? n.toFixed(0) : n.toFixed(1)
|
|
22
|
+
return `${sign}${formatted.replace(/\.0$/, '')}%`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const formatMetric = { formatCompact, formatPercent, formatDelta }
|
|
26
|
+
export default formatMetric
|