@flesler/cursor-canvas-web 1.4.0 → 1.5.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/dist/cursor-canvas-web.css +1 -0
- package/dist/cursor-canvas.cjs +1 -1
- package/dist/cursor-canvas.js +530 -565
- package/dist/pie-donut-DNbA0GeH.cjs +1 -0
- package/dist/pie-donut-qRBFP0tT.js +63 -0
- package/package.json +6 -2
- package/src/runtime/index.tsx +64 -0
- package/src/shim/__tests__/barChart.test.ts +61 -0
- package/src/shim/__tests__/canvas.smoke.test.tsx +209 -0
- package/src/shim/__tests__/category-theme.test.tsx +58 -0
- package/src/shim/__tests__/dag.test.ts +49 -0
- package/src/shim/__tests__/host-tokens.test.ts +106 -0
- package/src/shim/__tests__/lineChart.test.ts +28 -0
- package/src/shim/__tests__/useCanvasState.test.tsx +35 -0
- package/src/shim/__tests__/useHostTheme.test.tsx +43 -0
- package/src/shim/__tests__/valueAxis.test.ts +39 -0
- package/src/shim/bar-chart.tsx +77 -0
- package/src/shim/canvas-hooks.tsx +57 -0
- package/src/shim/canvas-mantine-root.tsx +129 -0
- package/src/shim/chart-shared.tsx +252 -0
- package/src/shim/cursor-canvas.tsx +136 -0
- package/src/shim/dag.ts +179 -0
- package/src/shim/diff.tsx +161 -0
- package/src/shim/host-tokens.ts +614 -0
- package/src/shim/line-chart.tsx +62 -0
- package/src/shim/pie-chart.tsx +127 -0
- package/src/shim/pie-donut.tsx +79 -0
- package/src/shim/theme.ts +77 -0
- package/src/shim/todo.tsx +184 -0
- package/src/shim/ui/actions.tsx +58 -0
- package/src/shim/ui/card-context.tsx +11 -0
- package/src/shim/ui/card.tsx +79 -0
- package/src/shim/ui/chevron.tsx +23 -0
- package/src/shim/ui/display.tsx +83 -0
- package/src/shim/ui/form.tsx +102 -0
- package/src/shim/ui/layout.tsx +67 -0
- package/src/shim/ui/rich.tsx +136 -0
- package/src/shim/ui/table.tsx +71 -0
- package/src/shim/ui/typography.tsx +129 -0
- package/src/shim/use-tokens.ts +13 -0
- package/src/superset/chart-extra.d.ts +11 -11
- package/src/superset/index.d.ts +4 -4
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { act, renderHook } from '@testing-library/react'
|
|
3
|
+
import { useCanvasState } from 'cursor/canvas'
|
|
4
|
+
|
|
5
|
+
describe('useCanvasState', () => {
|
|
6
|
+
it('returns the initial value when storage is empty', () => {
|
|
7
|
+
const { result } = renderHook(() => useCanvasState('count', 1))
|
|
8
|
+
expect(result.current[0]).toBe(1)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('persists updates to localStorage under the canvas namespace', () => {
|
|
12
|
+
const { result } = renderHook(() => useCanvasState('count', 1))
|
|
13
|
+
|
|
14
|
+
act(() => result.current[1](42))
|
|
15
|
+
|
|
16
|
+
expect(result.current[0]).toBe(42)
|
|
17
|
+
expect(localStorage.getItem('canvas:count')).toBe('42')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('supports functional updates', () => {
|
|
21
|
+
const { result } = renderHook(() => useCanvasState('count', 1))
|
|
22
|
+
|
|
23
|
+
act(() => result.current[1](prev => prev + 9))
|
|
24
|
+
|
|
25
|
+
expect(result.current[0]).toBe(10)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('rehydrates a previously stored value on mount', () => {
|
|
29
|
+
localStorage.setItem('canvas:flag', JSON.stringify(true))
|
|
30
|
+
|
|
31
|
+
const { result } = renderHook(() => useCanvasState('flag', false))
|
|
32
|
+
|
|
33
|
+
expect(result.current[0]).toBe(true)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react'
|
|
2
|
+
import { useHostTheme } from 'cursor/canvas'
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
import { CanvasMantineRoot } from '../canvas-mantine-root'
|
|
6
|
+
|
|
7
|
+
function wrapper({ children }: { children: ReactNode }) {
|
|
8
|
+
return (
|
|
9
|
+
<CanvasMantineRoot defaultColorScheme="dark">{children}</CanvasMantineRoot>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('useHostTheme', () => {
|
|
14
|
+
it('exposes category, chart palette, fill, and kind (dark)', () => {
|
|
15
|
+
const { result } = renderHook(() => useHostTheme(), { wrapper })
|
|
16
|
+
const theme = result.current
|
|
17
|
+
expect(theme.kind).toBe('dark')
|
|
18
|
+
expect(theme.brand).toBe('cursor')
|
|
19
|
+
expect(theme.category.orange).toMatch(/^#/)
|
|
20
|
+
expect(theme.category.gray).toBe(theme.text.tertiary)
|
|
21
|
+
expect(theme.tokens.category.orange).toBe(theme.category.orange)
|
|
22
|
+
expect(theme.chart.palette.lightGreen).toBe('#52B896E0')
|
|
23
|
+
expect(theme.chart.sequence.length).toBeGreaterThan(0)
|
|
24
|
+
expect(theme.fill.secondary).toMatch(/^#/)
|
|
25
|
+
expect(theme.stroke.focused).toMatch(/^#/)
|
|
26
|
+
expect(theme.radius.md).toBe(6)
|
|
27
|
+
expect(theme.typography.body.fontSize).toBe('14px')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('switches category table for light scheme', () => {
|
|
31
|
+
const lightWrap = ({ children }: { children: ReactNode }) => (
|
|
32
|
+
<CanvasMantineRoot defaultColorScheme="light">{children}</CanvasMantineRoot>
|
|
33
|
+
)
|
|
34
|
+
const dark = renderHook(() => useHostTheme(), { wrapper }).result.current
|
|
35
|
+
const light = renderHook(() => useHostTheme(), {
|
|
36
|
+
wrapper: lightWrap,
|
|
37
|
+
}).result.current
|
|
38
|
+
expect(light.kind).toBe('light')
|
|
39
|
+
expect(light.category.orange).toBe(light.tokens.category.orange)
|
|
40
|
+
expect(light.category.orange).not.toBe(dark.category.orange)
|
|
41
|
+
expect(light.category.gray).not.toBe(dark.category.gray)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { valueAxisChartProps } from '../chart-shared'
|
|
3
|
+
|
|
4
|
+
describe('valueAxisChartProps', () => {
|
|
5
|
+
it('keeps the SDK zero-based domain without extra', () => {
|
|
6
|
+
expect(valueAxisChartProps({})).toEqual({
|
|
7
|
+
yAxisProps: { domain: [0, 'auto'] },
|
|
8
|
+
referenceLines: undefined,
|
|
9
|
+
})
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('merges extra.valueAxis over the SDK domain', () => {
|
|
13
|
+
const axis = valueAxisChartProps({ valueAxis: { tickCount: 3, domain: [5, 50] } })
|
|
14
|
+
expect(axis.yAxisProps).toEqual({ tickCount: 3, domain: [5, 50] })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('defaults non-linear scales to a [1, dataMax] domain', () => {
|
|
18
|
+
const axis = valueAxisChartProps({ valueAxis: { scale: 'log' } })
|
|
19
|
+
expect(axis.yAxisProps).toEqual({
|
|
20
|
+
scale: 'log',
|
|
21
|
+
type: 'number',
|
|
22
|
+
allowDataOverflow: true,
|
|
23
|
+
allowDecimals: true,
|
|
24
|
+
domain: [1, 'dataMax'],
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('targets the X axis on horizontal bars', () => {
|
|
29
|
+
const axis = valueAxisChartProps({ horizontal: true, valueAxis: { unit: '%' } })
|
|
30
|
+
expect(axis.xAxisProps).toEqual({ domain: [0, 'auto'], unit: '%' })
|
|
31
|
+
expect(axis.yAxisProps).toBeUndefined()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('applies extra on stacked charts, which have no SDK domain', () => {
|
|
35
|
+
expect(valueAxisChartProps({ stacked: true })).toEqual({ referenceLines: undefined })
|
|
36
|
+
const axis = valueAxisChartProps({ stacked: true, valueAxis: { tickCount: 4 } })
|
|
37
|
+
expect(axis.yAxisProps).toEqual({ tickCount: 4 })
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { BarChart as MantineBarChart } from '@mantine/charts'
|
|
2
|
+
import '@mantine/charts/styles.css'
|
|
3
|
+
import type { BarChartProps } from '../superset/index'
|
|
4
|
+
import { barValueLabelProps, CHART_LEGEND_PROPS, CHART_LEGEND_STYLES, defaultShowBarValues, horizontalBarProps, horizontalBarValueLabelProps, reshapeLineChartData, toMantineSeries, useChartColorProps, useSeriesColor, valueAxisChartProps, valueFormatter, verticalBarLabelProps, withPerCategoryBarColors } from './chart-shared'
|
|
5
|
+
|
|
6
|
+
export function BarChart({
|
|
7
|
+
categories,
|
|
8
|
+
series,
|
|
9
|
+
height = 240,
|
|
10
|
+
stacked,
|
|
11
|
+
horizontal,
|
|
12
|
+
normalized,
|
|
13
|
+
valueSuffix,
|
|
14
|
+
valuePrefix,
|
|
15
|
+
showValues,
|
|
16
|
+
beginAtZero,
|
|
17
|
+
yMin,
|
|
18
|
+
yMax,
|
|
19
|
+
referenceLines,
|
|
20
|
+
style,
|
|
21
|
+
extra,
|
|
22
|
+
}: BarChartProps) {
|
|
23
|
+
const seriesColor = useSeriesColor()
|
|
24
|
+
const chartColors = useChartColorProps()
|
|
25
|
+
const data = withPerCategoryBarColors(
|
|
26
|
+
reshapeLineChartData(categories, series),
|
|
27
|
+
series,
|
|
28
|
+
seriesColor,
|
|
29
|
+
)
|
|
30
|
+
const type = normalized ? 'percent' : stacked ? 'stacked' : 'default'
|
|
31
|
+
const axis = valueAxisChartProps({
|
|
32
|
+
beginAtZero,
|
|
33
|
+
yMin,
|
|
34
|
+
yMax,
|
|
35
|
+
referenceLines,
|
|
36
|
+
horizontal,
|
|
37
|
+
stacked,
|
|
38
|
+
normalized,
|
|
39
|
+
valueAxis: extra?.valueAxis,
|
|
40
|
+
})
|
|
41
|
+
const withBarValueLabel =
|
|
42
|
+
!stacked &&
|
|
43
|
+
!normalized &&
|
|
44
|
+
defaultShowBarValues(showValues, series, categories)
|
|
45
|
+
return (
|
|
46
|
+
<MantineBarChart
|
|
47
|
+
h={height}
|
|
48
|
+
data={data}
|
|
49
|
+
dataKey="category"
|
|
50
|
+
series={toMantineSeries(series, seriesColor)}
|
|
51
|
+
type={type}
|
|
52
|
+
orientation={horizontal ? 'vertical' : 'horizontal'}
|
|
53
|
+
withLegend={series.length > 1}
|
|
54
|
+
legendProps={CHART_LEGEND_PROPS}
|
|
55
|
+
valueFormatter={valueFormatter(valueSuffix, valuePrefix)}
|
|
56
|
+
withBarValueLabel={withBarValueLabel}
|
|
57
|
+
{...(withBarValueLabel
|
|
58
|
+
? {
|
|
59
|
+
valueLabelProps: horizontal
|
|
60
|
+
? horizontalBarValueLabelProps(chartColors.barLabelColor)
|
|
61
|
+
: barValueLabelProps(chartColors.barLabelColor),
|
|
62
|
+
}
|
|
63
|
+
: {})}
|
|
64
|
+
{...(horizontal
|
|
65
|
+
? horizontalBarProps(extra?.categoryAxis?.width, withBarValueLabel)
|
|
66
|
+
: withBarValueLabel
|
|
67
|
+
? verticalBarLabelProps()
|
|
68
|
+
: {})}
|
|
69
|
+
styles={{
|
|
70
|
+
...CHART_LEGEND_STYLES,
|
|
71
|
+
legendItemName: { color: chartColors.textColor },
|
|
72
|
+
}}
|
|
73
|
+
{...axis}
|
|
74
|
+
style={style}
|
|
75
|
+
/>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useCallback, useState, type CSSProperties } from 'react'
|
|
2
|
+
import type { CanvasAction, SetCanvasState } from '../vendor/canvas-sdk/hooks.js'
|
|
3
|
+
|
|
4
|
+
export function useCanvasState<T>(
|
|
5
|
+
key: string,
|
|
6
|
+
defaultValue: T,
|
|
7
|
+
): [T, SetCanvasState<T>] {
|
|
8
|
+
const storageKey = `canvas:${key}`
|
|
9
|
+
|
|
10
|
+
const [state, setState] = useState<T>(() => {
|
|
11
|
+
if (typeof localStorage === 'undefined') return defaultValue
|
|
12
|
+
try {
|
|
13
|
+
const raw = localStorage.getItem(storageKey)
|
|
14
|
+
return raw === null ? defaultValue : (JSON.parse(raw) as T)
|
|
15
|
+
} catch {
|
|
16
|
+
return defaultValue
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const set = useCallback<SetCanvasState<T>>(
|
|
21
|
+
(action) => {
|
|
22
|
+
setState((prev) => {
|
|
23
|
+
const next =
|
|
24
|
+
typeof action === 'function'
|
|
25
|
+
? (action as (prev: T) => T)(prev)
|
|
26
|
+
: action
|
|
27
|
+
try {
|
|
28
|
+
localStorage?.setItem(storageKey, JSON.stringify(next))
|
|
29
|
+
} catch {
|
|
30
|
+
// Ignore quota / unavailable storage; state still updates in memory.
|
|
31
|
+
}
|
|
32
|
+
return next
|
|
33
|
+
})
|
|
34
|
+
},
|
|
35
|
+
[storageKey],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return [state, set]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function useCanvasAction(): (action: CanvasAction) => void {
|
|
42
|
+
return useCallback((action: CanvasAction) => {
|
|
43
|
+
if (import.meta.env?.DEV) {
|
|
44
|
+
console.info(
|
|
45
|
+
'[cursor/canvas shim] useCanvasAction (no-op on web):',
|
|
46
|
+
action,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}, [])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function mergeStyle(
|
|
53
|
+
base: CSSProperties,
|
|
54
|
+
override?: CSSProperties,
|
|
55
|
+
): CSSProperties {
|
|
56
|
+
return { ...base, ...override }
|
|
57
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { MantineProvider, createTheme, type MantineColorScheme, type MantineColorsTuple, type MantineThemeOverride } from '@mantine/core'
|
|
2
|
+
import { useMemo, useSyncExternalStore, type ReactNode } from 'react'
|
|
3
|
+
import { resolveThemeForColorScheme, type ShimHostTheme } from './host-tokens'
|
|
4
|
+
|
|
5
|
+
export interface CanvasMantineRootProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
defaultColorScheme?: MantineColorScheme
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function solidScale(hex: string): MantineColorsTuple {
|
|
11
|
+
return [hex, hex, hex, hex, hex, hex, hex, hex, hex, hex]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Maps `useHostTheme()` / brand onto Mantine `theme` (primary, radii, headings). */
|
|
15
|
+
export function createMantineThemeFromHost(host: ShimHostTheme): MantineThemeOverride {
|
|
16
|
+
const { typography: type, radius } = host
|
|
17
|
+
return createTheme({
|
|
18
|
+
primaryColor: 'accent',
|
|
19
|
+
colors: {
|
|
20
|
+
accent: solidScale(host.palette.accent),
|
|
21
|
+
},
|
|
22
|
+
fontFamily: type.fontFamily,
|
|
23
|
+
defaultRadius: `${radius.md}`,
|
|
24
|
+
radius: {
|
|
25
|
+
xs: `${radius.xs}px`,
|
|
26
|
+
sm: `${radius.sm}px`,
|
|
27
|
+
md: `${radius.md}px`,
|
|
28
|
+
lg: `${radius.lg}px`,
|
|
29
|
+
xl: `${radius.xl}px`,
|
|
30
|
+
},
|
|
31
|
+
headings: {
|
|
32
|
+
fontFamily: type.fontFamily,
|
|
33
|
+
fontWeight: String(type.h1.fontWeight),
|
|
34
|
+
sizes: {
|
|
35
|
+
h1: {
|
|
36
|
+
fontSize: type.h1.fontSize,
|
|
37
|
+
lineHeight: type.h1.lineHeight,
|
|
38
|
+
fontWeight: String(type.h1.fontWeight),
|
|
39
|
+
},
|
|
40
|
+
h2: {
|
|
41
|
+
fontSize: type.h2.fontSize,
|
|
42
|
+
lineHeight: type.h2.lineHeight,
|
|
43
|
+
fontWeight: String(type.h2.fontWeight),
|
|
44
|
+
},
|
|
45
|
+
h3: {
|
|
46
|
+
fontSize: type.h3.fontSize,
|
|
47
|
+
lineHeight: type.h3.lineHeight,
|
|
48
|
+
fontWeight: String(type.h3.fontWeight),
|
|
49
|
+
},
|
|
50
|
+
h4: {
|
|
51
|
+
fontSize: type.body.fontSize,
|
|
52
|
+
lineHeight: type.body.lineHeight,
|
|
53
|
+
fontWeight: String(type.body.fontWeight),
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function subscribeSystemScheme(onChange: () => void): () => void {
|
|
61
|
+
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
|
62
|
+
mq.addEventListener('change', onChange)
|
|
63
|
+
return () => mq.removeEventListener('change', onChange)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getSystemScheme(): 'light' | 'dark' {
|
|
67
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
68
|
+
? 'dark'
|
|
69
|
+
: 'light'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function useResolvedPolarity(
|
|
73
|
+
defaultColorScheme: MantineColorScheme,
|
|
74
|
+
): 'light' | 'dark' {
|
|
75
|
+
const system = useSyncExternalStore<'light' | 'dark'>(
|
|
76
|
+
subscribeSystemScheme,
|
|
77
|
+
getSystemScheme,
|
|
78
|
+
() => 'light',
|
|
79
|
+
)
|
|
80
|
+
if (defaultColorScheme === 'dark') {
|
|
81
|
+
return 'dark'
|
|
82
|
+
}
|
|
83
|
+
if (defaultColorScheme === 'light') {
|
|
84
|
+
return 'light'
|
|
85
|
+
}
|
|
86
|
+
return system
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Mantine + canvas host tokens (brand, Grok radii/typography, accent primary).
|
|
91
|
+
* Not part of the `cursor/canvas` barrel.
|
|
92
|
+
*/
|
|
93
|
+
export function CanvasMantineRoot({
|
|
94
|
+
children,
|
|
95
|
+
defaultColorScheme = 'auto',
|
|
96
|
+
}: CanvasMantineRootProps) {
|
|
97
|
+
const polarity = useResolvedPolarity(defaultColorScheme)
|
|
98
|
+
const host = useMemo(
|
|
99
|
+
() => resolveThemeForColorScheme(polarity),
|
|
100
|
+
[polarity],
|
|
101
|
+
)
|
|
102
|
+
const mantineTheme = useMemo(() => createMantineThemeFromHost(host), [host])
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<MantineProvider
|
|
106
|
+
theme={mantineTheme}
|
|
107
|
+
defaultColorScheme={defaultColorScheme}
|
|
108
|
+
forceColorScheme={polarity}
|
|
109
|
+
>
|
|
110
|
+
<div
|
|
111
|
+
data-cursor-canvas-host
|
|
112
|
+
style={{
|
|
113
|
+
minHeight: 'inherit',
|
|
114
|
+
background: host.bg.editor,
|
|
115
|
+
color: host.text.primary,
|
|
116
|
+
fontFamily: host.typography.fontFamily,
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
{/* Canvas-authored responsive SVGs (e.g. DAG): IDE scales to width; do not clamp max-height. */}
|
|
120
|
+
<style>{`
|
|
121
|
+
[data-cursor-canvas-host] svg[viewBox][width='100%'] {
|
|
122
|
+
max-height: unset !important;
|
|
123
|
+
}
|
|
124
|
+
`}</style>
|
|
125
|
+
{children}
|
|
126
|
+
</div>
|
|
127
|
+
</MantineProvider>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared chart data shaping and axis helpers (no @mantine/charts component imports).
|
|
3
|
+
*/
|
|
4
|
+
import type { ChartReferenceLineProps } from '@mantine/charts'
|
|
5
|
+
import type { XAxisProps, YAxisProps } from 'recharts'
|
|
6
|
+
import type { CanvasChartValueAxisExtra } from '../superset/chart-extra'
|
|
7
|
+
import type { BarChartProps } from '../superset/index'
|
|
8
|
+
import { toneHex } from './theme'
|
|
9
|
+
import { useHostTheme } from './use-tokens'
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
CanvasBarChartExtra,
|
|
13
|
+
CanvasChartExtra,
|
|
14
|
+
CanvasChartScale,
|
|
15
|
+
CanvasChartValueAxisExtra,
|
|
16
|
+
CanvasLineChartExtra,
|
|
17
|
+
} from '../superset/chart-extra'
|
|
18
|
+
export type { BarChartProps, LineChartProps } from '../superset/index'
|
|
19
|
+
export type {
|
|
20
|
+
ChartDataPoint,
|
|
21
|
+
ChartReferenceLine,
|
|
22
|
+
ChartSeries,
|
|
23
|
+
ChartTone,
|
|
24
|
+
PieChartProps,
|
|
25
|
+
} from '../vendor/canvas-sdk/chart-primitives.js'
|
|
26
|
+
|
|
27
|
+
import type { ChartReferenceLine, ChartSeries, ChartTone } from '../vendor/canvas-sdk/chart-primitives.js'
|
|
28
|
+
|
|
29
|
+
export const CHART_LEGEND_PROPS = {
|
|
30
|
+
verticalAlign: 'bottom' as const,
|
|
31
|
+
align: 'center' as const,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const CHART_LEGEND_STYLES = {
|
|
35
|
+
legend: { justifyContent: 'center' },
|
|
36
|
+
} as const
|
|
37
|
+
|
|
38
|
+
export function useSeriesColor(): (
|
|
39
|
+
tone: ChartTone | undefined,
|
|
40
|
+
index: number,
|
|
41
|
+
) => string {
|
|
42
|
+
const theme = useHostTheme()
|
|
43
|
+
const sequence = theme.chart.sequence
|
|
44
|
+
return (tone, index) =>
|
|
45
|
+
tone ? toneHex[tone] : sequence[index % sequence.length]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function useChartColorProps(): { textColor: string; barLabelColor: string } {
|
|
49
|
+
const theme = useHostTheme()
|
|
50
|
+
const text = theme.text.primary
|
|
51
|
+
return { textColor: text, barLabelColor: text }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function reshapeLineChartData(
|
|
55
|
+
categories: string[],
|
|
56
|
+
series: ChartSeries[],
|
|
57
|
+
): Record<string, string | number>[] {
|
|
58
|
+
return categories.map((category, index) => {
|
|
59
|
+
const row: Record<string, string | number> = { category }
|
|
60
|
+
for (const s of series) {
|
|
61
|
+
row[s.name] = s.data[index] ?? 0
|
|
62
|
+
}
|
|
63
|
+
return row
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function toMantineSeries(
|
|
68
|
+
series: ChartSeries[],
|
|
69
|
+
seriesColor: (tone: ChartTone | undefined, index: number) => string,
|
|
70
|
+
) {
|
|
71
|
+
return series.map((s, index) => ({
|
|
72
|
+
name: s.name,
|
|
73
|
+
color: seriesColor(s.tone, index),
|
|
74
|
+
}))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function valueFormatter(valueSuffix?: string, valuePrefix?: string) {
|
|
78
|
+
return (value: number) => `${valuePrefix ?? ''}${value}${valueSuffix ?? ''}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function mantineReferenceLines(
|
|
82
|
+
lines: ChartReferenceLine[] | undefined,
|
|
83
|
+
horizontal: boolean,
|
|
84
|
+
): ChartReferenceLineProps[] | undefined {
|
|
85
|
+
if (!lines?.length) {
|
|
86
|
+
return undefined
|
|
87
|
+
}
|
|
88
|
+
return lines.map(line => ({
|
|
89
|
+
...(horizontal ? { x: line.value } : { y: line.value }),
|
|
90
|
+
label: line.label,
|
|
91
|
+
color: line.tone ? toneHex[line.tone] : toneHex.neutral,
|
|
92
|
+
}))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type ValueAxisProps = Pick<
|
|
96
|
+
BarChartProps,
|
|
97
|
+
'beginAtZero' | 'yMin' | 'yMax' | 'referenceLines'
|
|
98
|
+
>
|
|
99
|
+
|
|
100
|
+
type AxisChartProps = {
|
|
101
|
+
yAxisProps?: Omit<YAxisProps, 'ref'>;
|
|
102
|
+
xAxisProps?: Omit<XAxisProps, 'ref'>;
|
|
103
|
+
referenceLines?: ChartReferenceLineProps[]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const NON_LINEAR_SCALES = new Set(['log', 'symlog', 'pow', 'sqrt'])
|
|
107
|
+
|
|
108
|
+
function normalizeValueAxisExtra(
|
|
109
|
+
valueAxis: CanvasChartValueAxisExtra,
|
|
110
|
+
): CanvasChartValueAxisExtra {
|
|
111
|
+
const { scale } = valueAxis
|
|
112
|
+
if (typeof scale !== 'string' || !NON_LINEAR_SCALES.has(scale)) {
|
|
113
|
+
return valueAxis
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
...valueAxis,
|
|
117
|
+
type: valueAxis.type ?? 'number',
|
|
118
|
+
allowDataOverflow: valueAxis.allowDataOverflow ?? true,
|
|
119
|
+
allowDecimals: valueAxis.allowDecimals ?? true,
|
|
120
|
+
domain: valueAxis.domain ?? [1, 'dataMax'],
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function valueAxisChartProps(
|
|
125
|
+
opts: ValueAxisProps & {
|
|
126
|
+
horizontal?: boolean;
|
|
127
|
+
stacked?: boolean;
|
|
128
|
+
normalized?: boolean;
|
|
129
|
+
valueAxis?: CanvasChartValueAxisExtra
|
|
130
|
+
},
|
|
131
|
+
): AxisChartProps {
|
|
132
|
+
const ref = mantineReferenceLines(opts.referenceLines, !!opts.horizontal)
|
|
133
|
+
let domain: [number | string, number | string] | undefined
|
|
134
|
+
if (!opts.stacked && !opts.normalized) {
|
|
135
|
+
const beginAtZero = opts.beginAtZero !== false
|
|
136
|
+
domain = [opts.yMin ?? (beginAtZero ? 0 : 'auto'), opts.yMax ?? 'auto']
|
|
137
|
+
}
|
|
138
|
+
const extra = opts.valueAxis ? normalizeValueAxisExtra(opts.valueAxis) : undefined
|
|
139
|
+
if (!domain && !extra) {
|
|
140
|
+
return { referenceLines: ref }
|
|
141
|
+
}
|
|
142
|
+
const axisProps = { ...(domain && { domain }), ...extra }
|
|
143
|
+
return opts.horizontal
|
|
144
|
+
? { xAxisProps: axisProps as Omit<XAxisProps, 'ref'>, referenceLines: ref }
|
|
145
|
+
: { yAxisProps: axisProps, referenceLines: ref }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function defaultShowBarValues(
|
|
149
|
+
showValues: boolean | undefined,
|
|
150
|
+
series: ChartSeries[],
|
|
151
|
+
categories: string[],
|
|
152
|
+
): boolean {
|
|
153
|
+
if (showValues !== undefined) {
|
|
154
|
+
return showValues
|
|
155
|
+
}
|
|
156
|
+
return series.length === 1 && categories.length <= 8
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function withPerCategoryBarColors(
|
|
160
|
+
data: Record<string, string | number>[],
|
|
161
|
+
series: ChartSeries[],
|
|
162
|
+
seriesColor: (tone: ChartTone | undefined, index: number) => string,
|
|
163
|
+
): Record<string, string | number>[] {
|
|
164
|
+
if (series.length !== 1 || series[0].tone) {
|
|
165
|
+
return data
|
|
166
|
+
}
|
|
167
|
+
return data.map((row, index) => ({
|
|
168
|
+
...row,
|
|
169
|
+
color: seriesColor(undefined, index),
|
|
170
|
+
}))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const HORIZONTAL_CATEGORY_AXIS_WIDTH = 96
|
|
174
|
+
const CATEGORY_LABEL_CHAR_WIDTH = 6.5
|
|
175
|
+
|
|
176
|
+
function truncateCategoryAxisLabel(text: string, max: number): string {
|
|
177
|
+
if (text.length <= max) {
|
|
178
|
+
return text
|
|
179
|
+
}
|
|
180
|
+
return `${text.slice(0, Math.max(0, max - 1))}…`
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function horizontalCategoryAxisTick(
|
|
184
|
+
labelMax: number,
|
|
185
|
+
props: {
|
|
186
|
+
x?: string | number;
|
|
187
|
+
y?: string | number;
|
|
188
|
+
payload?: { value?: string | number }
|
|
189
|
+
},
|
|
190
|
+
) {
|
|
191
|
+
const x = typeof props.x === 'number' ? props.x : Number(props.x) || 0
|
|
192
|
+
const y = typeof props.y === 'number' ? props.y : Number(props.y) || 0
|
|
193
|
+
const full = String(props.payload?.value ?? '')
|
|
194
|
+
const display = truncateCategoryAxisLabel(full, labelMax)
|
|
195
|
+
return (
|
|
196
|
+
<g transform={`translate(${x},${y})`}>
|
|
197
|
+
<text
|
|
198
|
+
x={-10}
|
|
199
|
+
y={0}
|
|
200
|
+
dy={4}
|
|
201
|
+
textAnchor="end"
|
|
202
|
+
fill="currentColor"
|
|
203
|
+
fontSize={12}
|
|
204
|
+
>
|
|
205
|
+
<title>{full}</title>
|
|
206
|
+
{display}
|
|
207
|
+
</text>
|
|
208
|
+
</g>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function barValueLabelProps(barLabelColor: string) {
|
|
213
|
+
return {
|
|
214
|
+
fontWeight: 700,
|
|
215
|
+
fill: barLabelColor,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function horizontalBarValueLabelProps(barLabelColor: string) {
|
|
220
|
+
return {
|
|
221
|
+
...barValueLabelProps(barLabelColor),
|
|
222
|
+
position: 'right' as const,
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function horizontalBarProps(
|
|
227
|
+
width = HORIZONTAL_CATEGORY_AXIS_WIDTH,
|
|
228
|
+
withValueLabel = false,
|
|
229
|
+
) {
|
|
230
|
+
const labelMax = Math.floor(width / CATEGORY_LABEL_CHAR_WIDTH)
|
|
231
|
+
return {
|
|
232
|
+
yAxisProps: {
|
|
233
|
+
tick: (props: Parameters<typeof horizontalCategoryAxisTick>[1]) =>
|
|
234
|
+
horizontalCategoryAxisTick(labelMax, props),
|
|
235
|
+
width,
|
|
236
|
+
},
|
|
237
|
+
barChartProps: {
|
|
238
|
+
margin: { right: withValueLabel ? 48 : 32 },
|
|
239
|
+
},
|
|
240
|
+
gridAxis: 'y' as const,
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function verticalBarLabelProps() {
|
|
245
|
+
return {
|
|
246
|
+
barChartProps: {
|
|
247
|
+
margin: { top: 22 },
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export { barValueLabelProps, horizontalBarValueLabelProps, verticalBarLabelProps }
|