@titan-design/react-ui 0.2.6 → 0.3.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/bodymap-BhG55xHM.d.mts +318 -0
- package/dist/bodymap-BhG55xHM.d.ts +318 -0
- package/dist/bodymap.d.mts +3 -0
- package/dist/bodymap.d.ts +3 -0
- package/dist/bodymap.js +2670 -0
- package/dist/bodymap.js.map +1 -0
- package/dist/bodymap.mjs +1101 -0
- package/dist/bodymap.mjs.map +1 -0
- package/dist/{chunk-UXVRPOME.mjs → chunk-7XPB4NAO.mjs} +125 -785
- package/dist/chunk-7XPB4NAO.mjs.map +1 -0
- package/dist/chunk-P7TSQUN6.mjs +846 -0
- package/dist/chunk-P7TSQUN6.mjs.map +1 -0
- package/dist/chunk-TOD2WQBG.mjs +1048 -0
- package/dist/chunk-TOD2WQBG.mjs.map +1 -0
- package/dist/index.d.mts +1131 -4
- package/dist/index.d.ts +1131 -4
- package/dist/index.js +6396 -225
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5572 -641
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +317 -1
- package/dist/theme/index.d.ts +317 -1
- package/dist/theme/index.js +185 -2
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +2 -1
- package/dist/theme/tokens-css.d.mts +20 -0
- package/dist/theme/tokens-css.d.ts +20 -0
- package/dist/theme/tokens-css.js +598 -0
- package/dist/theme/tokens-css.js.map +1 -0
- package/dist/theme/tokens-css.mjs +24 -0
- package/dist/theme/tokens-css.mjs.map +1 -0
- package/package.json +25 -5
- package/src/bodymap.ts +37 -0
- package/src/components/custom/DateTime/DateTime.stories.tsx +1 -1
- package/src/components/custom/Gauge/Gauge.stories.tsx +45 -0
- package/src/components/custom/Gauge/Gauge.test.tsx +71 -0
- package/src/components/custom/Gauge/Gauge.tsx +167 -0
- package/src/components/custom/Gauge/index.ts +2 -0
- package/src/components/custom/Scatter/Scatter.stories.tsx +85 -0
- package/src/components/custom/Scatter/Scatter.test.tsx +96 -0
- package/src/components/custom/Scatter/Scatter.tsx +260 -0
- package/src/components/custom/Scatter/index.ts +2 -0
- package/src/components/custom/Sidebar/Sidebar.tsx +1 -1
- package/src/components/custom/Table/Table.test.tsx +27 -0
- package/src/components/custom/Table/Table.tsx +33 -17
- package/src/components/custom/Treemap/Treemap.stories.tsx +60 -0
- package/src/components/custom/Treemap/Treemap.test.tsx +67 -0
- package/src/components/custom/Treemap/Treemap.tsx +192 -0
- package/src/components/custom/Treemap/index.ts +2 -0
- package/src/components/custom/Workout/ActiveWorkoutPage.stories.tsx +118 -0
- package/src/components/custom/Workout/ActiveWorkoutPage.test.tsx +171 -0
- package/src/components/custom/Workout/ActiveWorkoutPage.tsx +447 -0
- package/src/components/custom/Workout/BaseBadge.stories.tsx +75 -0
- package/src/components/custom/Workout/BaseBadge.test.tsx +102 -0
- package/src/components/custom/Workout/BaseBadge.tsx +113 -0
- package/src/components/custom/Workout/BodyMap.stories.tsx +121 -0
- package/src/components/custom/Workout/BodyMap.test.tsx +124 -0
- package/src/components/custom/Workout/BodyMap.tsx +347 -0
- package/src/components/custom/Workout/BodyMapDetailPanel.stories.tsx +171 -0
- package/src/components/custom/Workout/BodyMapDetailPanel.test.tsx +225 -0
- package/src/components/custom/Workout/BodyMapDetailPanel.tsx +477 -0
- package/src/components/custom/Workout/CapacityBandChart.stories.tsx +125 -0
- package/src/components/custom/Workout/CapacityBandChart.test.tsx +193 -0
- package/src/components/custom/Workout/CapacityBandChart.tsx +493 -0
- package/src/components/custom/Workout/DeviationBar.stories.tsx +58 -0
- package/src/components/custom/Workout/DeviationBar.test.tsx +86 -0
- package/src/components/custom/Workout/DeviationBar.tsx +78 -0
- package/src/components/custom/Workout/ExerciseCard.stories.tsx +186 -0
- package/src/components/custom/Workout/ExerciseCard.test.tsx +343 -0
- package/src/components/custom/Workout/ExerciseCard.tsx +442 -0
- package/src/components/custom/Workout/ExerciseDetailPage.stories.tsx +225 -0
- package/src/components/custom/Workout/ExerciseDetailPage.test.tsx +128 -0
- package/src/components/custom/Workout/ExerciseDetailPage.tsx +630 -0
- package/src/components/custom/Workout/InputBar.stories.tsx +75 -0
- package/src/components/custom/Workout/InputBar.test.tsx +116 -0
- package/src/components/custom/Workout/InputBar.tsx +162 -0
- package/src/components/custom/Workout/IntensityBar.stories.tsx +60 -0
- package/src/components/custom/Workout/IntensityBar.test.tsx +113 -0
- package/src/components/custom/Workout/IntensityBar.tsx +166 -0
- package/src/components/custom/Workout/MesoCard.stories.tsx +152 -0
- package/src/components/custom/Workout/MesoCard.test.tsx +170 -0
- package/src/components/custom/Workout/MesoCard.tsx +237 -0
- package/src/components/custom/Workout/MesoProgressBar.stories.tsx +85 -0
- package/src/components/custom/Workout/MesoProgressBar.test.tsx +153 -0
- package/src/components/custom/Workout/MesoProgressBar.tsx +165 -0
- package/src/components/custom/Workout/MesoStatusCard.stories.tsx +126 -0
- package/src/components/custom/Workout/MesoStatusCard.test.tsx +209 -0
- package/src/components/custom/Workout/MesoStatusCard.tsx +456 -0
- package/src/components/custom/Workout/MuscleGroupChip.stories.tsx +118 -0
- package/src/components/custom/Workout/MuscleGroupChip.test.tsx +91 -0
- package/src/components/custom/Workout/MuscleGroupChip.tsx +92 -0
- package/src/components/custom/Workout/PlaceholderStrip.stories.tsx +94 -0
- package/src/components/custom/Workout/PlaceholderStrip.test.tsx +83 -0
- package/src/components/custom/Workout/PlaceholderStrip.tsx +90 -0
- package/src/components/custom/Workout/PrBadge.stories.tsx +105 -0
- package/src/components/custom/Workout/PrBadge.test.tsx +111 -0
- package/src/components/custom/Workout/PrBadge.tsx +103 -0
- package/src/components/custom/Workout/PrHistoryModal.stories.tsx +116 -0
- package/src/components/custom/Workout/PrHistoryModal.test.tsx +161 -0
- package/src/components/custom/Workout/PrHistoryModal.tsx +244 -0
- package/src/components/custom/Workout/ProgramPlanningPage.stories.tsx +257 -0
- package/src/components/custom/Workout/ProgramPlanningPage.test.tsx +142 -0
- package/src/components/custom/Workout/ProgramPlanningPage.tsx +422 -0
- package/src/components/custom/Workout/ReadinessCheck.stories.tsx +126 -0
- package/src/components/custom/Workout/ReadinessCheck.test.tsx +181 -0
- package/src/components/custom/Workout/ReadinessCheck.tsx +265 -0
- package/src/components/custom/Workout/RestTimer.test.tsx +10 -0
- package/src/components/custom/Workout/RestTimer.tsx +15 -11
- package/src/components/custom/Workout/SetRow.stories.tsx +165 -0
- package/src/components/custom/Workout/SetRow.test.tsx +222 -0
- package/src/components/custom/Workout/SetRow.tsx +247 -0
- package/src/components/custom/Workout/Sparkline.stories.tsx +125 -0
- package/src/components/custom/Workout/Sparkline.test.tsx +113 -0
- package/src/components/custom/Workout/Sparkline.tsx +188 -0
- package/src/components/custom/Workout/StatusDot.stories.tsx +150 -0
- package/src/components/custom/Workout/StatusDot.test.tsx +165 -0
- package/src/components/custom/Workout/StatusDot.tsx +160 -0
- package/src/components/custom/Workout/StrengthTrendChart.stories.tsx +130 -0
- package/src/components/custom/Workout/StrengthTrendChart.test.tsx +217 -0
- package/src/components/custom/Workout/StrengthTrendChart.tsx +628 -0
- package/src/components/custom/Workout/SupersetWrapper.tsx +1 -2
- package/src/components/custom/Workout/TempoDisplay.stories.tsx +66 -0
- package/src/components/custom/Workout/TempoDisplay.test.tsx +90 -0
- package/src/components/custom/Workout/TempoDisplay.tsx +209 -0
- package/src/components/custom/Workout/TrainingStatusPage.stories.tsx +212 -0
- package/src/components/custom/Workout/TrainingStatusPage.test.tsx +131 -0
- package/src/components/custom/Workout/TrainingStatusPage.tsx +321 -0
- package/src/components/custom/Workout/VelocityStrip.stories.tsx +125 -0
- package/src/components/custom/Workout/VelocityStrip.test.tsx +244 -0
- package/src/components/custom/Workout/VelocityStrip.tsx +288 -0
- package/src/components/custom/Workout/WeekRow.stories.tsx +121 -0
- package/src/components/custom/Workout/WeekRow.test.tsx +117 -0
- package/src/components/custom/Workout/WeekRow.tsx +140 -0
- package/src/components/custom/Workout/WeightBadge.stories.tsx +87 -0
- package/src/components/custom/Workout/WeightBadge.test.tsx +164 -0
- package/src/components/custom/Workout/WeightBadge.tsx +113 -0
- package/src/components/custom/Workout/WorkoutCard.stories.tsx +135 -0
- package/src/components/custom/Workout/WorkoutCard.test.tsx +172 -0
- package/src/components/custom/Workout/WorkoutCard.tsx +228 -0
- package/src/components/custom/Workout/WorkoutPill.stories.tsx +63 -0
- package/src/components/custom/Workout/WorkoutPill.test.tsx +153 -0
- package/src/components/custom/Workout/WorkoutPill.tsx +177 -0
- package/src/components/custom/Workout/index.ts +152 -0
- package/src/components/custom/Workout/muscleTaxonomy.ts +252 -0
- package/src/components/custom/index.ts +3 -0
- package/src/components/custom/stepper/Stepper.stories.tsx +1 -1
- package/src/components/custom/stepper/Stepper.tsx +2 -2
- package/src/components/ui/autocomplete/Autocomplete.stories.tsx +1 -1
- package/src/components/ui/autocomplete/Autocomplete.tsx +1 -1
- package/src/components/ui/card/Card.tsx +1 -1
- package/src/components/ui/checkbox/Checkbox.tsx +1 -1
- package/src/components/ui/collapse/Collapse.tsx +1 -1
- package/src/components/ui/data-row/DataRow.stories.tsx +1 -1
- package/src/components/ui/drawer/Drawer.tsx +3 -3
- package/src/components/ui/form-field/FormField.tsx +1 -1
- package/src/components/ui/help-tip/HelpTip.tsx +1 -1
- package/src/components/ui/menu/Menu.tsx +2 -2
- package/src/components/ui/pill/Pill.tsx +1 -1
- package/src/components/ui/popover/Popover.stories.tsx +2 -2
- package/src/components/ui/popover/Popover.tsx +1 -1
- package/src/components/ui/radio/Radio.stories.tsx +1 -1
- package/src/components/ui/section/Section.stories.tsx +4 -4
- package/src/components/ui/select/Select.tsx +1 -1
- package/src/components/ui/stack/Stack.stories.tsx +4 -4
- package/src/components/ui/tabs/Tabs.tsx +2 -2
- package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +1 -1
- package/src/components/ui/toolbar-button/ToolbarButton.tsx +1 -1
- package/src/examples/react-hook-form/ControlledFields.tsx +171 -0
- package/src/examples/react-hook-form/ReactHookForm.stories.tsx +51 -0
- package/src/examples/react-hook-form/ReactHookForm.test.tsx +61 -0
- package/src/examples/react-hook-form/RhfContactForm.tsx +99 -0
- package/src/stories/PresetShowcase.stories.tsx +15 -15
- package/src/theme/config.completeness.test.ts +77 -0
- package/src/theme/config.ts +151 -0
- package/src/theme/index.ts +2 -1
- package/src/theme/manifest/css-property-manifest.example.json +79 -0
- package/src/theme/manifest/css-property-manifest.schema.json +93 -0
- package/src/theme/manifest/css-property-manifest.test.ts +81 -0
- package/src/theme/manifest/css-property-manifest.types.ts +49 -0
- package/src/theme/manifest/css-property-manifest.validate.ts +84 -0
- package/src/theme/manifest/design-freeze.test.ts +155 -0
- package/src/theme/manifest/extract-css-properties.fixture.html +29 -0
- package/src/theme/manifest/extract-css-properties.test.ts +112 -0
- package/src/theme/manifest/index.ts +11 -0
- package/src/theme/tokens-css.test.ts +28 -0
- package/src/theme/tokens-css.ts +36 -0
- package/src/theme/workout-tokens.ts +59 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/workout-format.test.ts +81 -0
- package/src/utils/workout-format.ts +83 -0
- package/dist/chunk-UXVRPOME.mjs.map +0 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { Scatter, type ScatterDatum } from './Scatter'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Scatter> = {
|
|
5
|
+
title: 'Custom/Scatter',
|
|
6
|
+
component: Scatter,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
argTypes: {
|
|
9
|
+
width: { control: { type: 'range', min: 240, max: 800, step: 20 } },
|
|
10
|
+
height: { control: { type: 'range', min: 180, max: 600, step: 20 } },
|
|
11
|
+
diagonal: { control: 'boolean' },
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
export default meta
|
|
15
|
+
type Story = StoryObj<typeof Scatter>
|
|
16
|
+
|
|
17
|
+
/** Modules on the instability × abstractness "main sequence". */
|
|
18
|
+
const mainSequence: ScatterDatum[] = [
|
|
19
|
+
{ id: 'core', x: 0.08, y: 0.85, r: 10, color: '#14B8A6', label: 'core' },
|
|
20
|
+
{ id: 'domain', x: 0.22, y: 0.7, r: 8, color: '#14B8A6', label: 'domain' },
|
|
21
|
+
{ id: 'services', x: 0.45, y: 0.4, r: 12, color: '#5B9BD5', label: 'services' },
|
|
22
|
+
{ id: 'api', x: 0.6, y: 0.35, r: 7, color: '#5B9BD5', label: 'api' },
|
|
23
|
+
{ id: 'cli', x: 0.9, y: 0.08, r: 9, color: '#F4A736', label: 'cli' },
|
|
24
|
+
{ id: 'utils', x: 0.85, y: 0.75, r: 6, color: '#F83030', label: 'utils (pain?)' },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
export const Default: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
data: mainSequence,
|
|
30
|
+
width: 480,
|
|
31
|
+
height: 340,
|
|
32
|
+
diagonal: true,
|
|
33
|
+
axis: { xLabel: 'Instability (I)', yLabel: 'Abstractness (A)', xMin: 0, xMax: 1, yMin: 0, yMax: 1 },
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const NoDiagonal: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
data: mainSequence,
|
|
40
|
+
width: 480,
|
|
41
|
+
height: 340,
|
|
42
|
+
axis: { xLabel: 'Instability (I)', yLabel: 'Abstractness (A)', xMin: 0, xMax: 1, yMin: 0, yMax: 1 },
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const Selected: Story = {
|
|
47
|
+
args: {
|
|
48
|
+
data: mainSequence,
|
|
49
|
+
width: 480,
|
|
50
|
+
height: 340,
|
|
51
|
+
diagonal: true,
|
|
52
|
+
selectedId: 'utils',
|
|
53
|
+
axis: { xLabel: 'Instability (I)', yLabel: 'Abstractness (A)', xMin: 0, xMax: 1, yMin: 0, yMax: 1 },
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Auto-domain from a single point — the frame still renders cleanly. */
|
|
58
|
+
export const SinglePoint: Story = {
|
|
59
|
+
args: {
|
|
60
|
+
data: [{ id: 'only', x: 0.5, y: 0.5, r: 10, label: 'only module' }],
|
|
61
|
+
width: 400,
|
|
62
|
+
height: 300,
|
|
63
|
+
axis: { xLabel: 'I', yLabel: 'A' },
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** An outlier far outside the main cluster, kept legible by an explicit domain. */
|
|
68
|
+
export const Outlier: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
data: [...mainSequence, { id: 'legacy', x: 0.98, y: 0.98, r: 16, color: '#F83030', label: 'legacy.ts' }],
|
|
71
|
+
width: 480,
|
|
72
|
+
height: 340,
|
|
73
|
+
diagonal: true,
|
|
74
|
+
axis: { xLabel: 'Instability (I)', yLabel: 'Abstractness (A)', xMin: 0, xMax: 1, yMin: 0, yMax: 1 },
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const Empty: Story = {
|
|
79
|
+
args: {
|
|
80
|
+
data: [],
|
|
81
|
+
width: 400,
|
|
82
|
+
height: 300,
|
|
83
|
+
axis: { xLabel: 'Instability (I)', yLabel: 'Abstractness (A)', xMin: 0, xMax: 1, yMin: 0, yMax: 1 },
|
|
84
|
+
},
|
|
85
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { axe } from 'jest-axe'
|
|
4
|
+
import { Scatter, type ScatterDatum } from './Scatter'
|
|
5
|
+
|
|
6
|
+
const data: ScatterDatum[] = [
|
|
7
|
+
{ id: 'a', x: 0.1, y: 0.9, label: 'core' },
|
|
8
|
+
{ id: 'b', x: 0.5, y: 0.5, label: 'mid' },
|
|
9
|
+
{ id: 'c', x: 0.9, y: 0.1, label: 'leaf' },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
const base = { data, width: 320, height: 240 }
|
|
13
|
+
|
|
14
|
+
describe('Scatter', () => {
|
|
15
|
+
it('renders one point per datum', () => {
|
|
16
|
+
render(<Scatter {...base} />)
|
|
17
|
+
expect(screen.getByTestId('scatter-point-a')).toBeInTheDocument()
|
|
18
|
+
expect(screen.getByTestId('scatter-point-b')).toBeInTheDocument()
|
|
19
|
+
expect(screen.getByTestId('scatter-point-c')).toBeInTheDocument()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('renders gridlines on both axes', () => {
|
|
23
|
+
render(<Scatter {...base} />)
|
|
24
|
+
expect(screen.getAllByTestId('scatter-gridline-x').length).toBeGreaterThanOrEqual(2)
|
|
25
|
+
expect(screen.getAllByTestId('scatter-gridline-y').length).toBeGreaterThanOrEqual(2)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('fires onPress with the point id', () => {
|
|
29
|
+
const onPress = vi.fn()
|
|
30
|
+
render(<Scatter {...base} onPress={onPress} />)
|
|
31
|
+
fireEvent.click(screen.getByTestId('scatter-point-b'))
|
|
32
|
+
expect(onPress).toHaveBeenCalledWith('b')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('draws the main-sequence diagonal only when requested', () => {
|
|
36
|
+
const { rerender } = render(<Scatter {...base} />)
|
|
37
|
+
expect(screen.queryByTestId('scatter-diagonal')).not.toBeInTheDocument()
|
|
38
|
+
rerender(<Scatter {...base} diagonal />)
|
|
39
|
+
expect(screen.getByTestId('scatter-diagonal')).toBeInTheDocument()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('renders axis labels when provided', () => {
|
|
43
|
+
render(<Scatter {...base} axis={{ xLabel: 'Instability', yLabel: 'Abstractness' }} />)
|
|
44
|
+
expect(screen.getByTestId('scatter-x-label')).toHaveTextContent('Instability')
|
|
45
|
+
expect(screen.getByTestId('scatter-y-label')).toHaveTextContent('Abstractness')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('labels a point by its label, falling back to id', () => {
|
|
49
|
+
render(<Scatter data={[{ id: 'solo', x: 0.2, y: 0.4 }]} width={200} height={200} />)
|
|
50
|
+
expect(screen.getByLabelText('solo')).toBeInTheDocument()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('renders a single point without crashing', () => {
|
|
54
|
+
render(<Scatter data={[{ id: 'one', x: 5, y: 5 }]} width={200} height={200} />)
|
|
55
|
+
expect(screen.getByTestId('scatter-point-one')).toBeInTheDocument()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('renders an empty plot frame with no points', () => {
|
|
59
|
+
render(<Scatter data={[]} width={200} height={200} />)
|
|
60
|
+
expect(screen.getByTestId('scatter-canvas')).toBeInTheDocument()
|
|
61
|
+
expect(screen.queryByTestId('scatter-points')?.childNodes.length ?? 0).toBe(0)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('places an outlier inside the plot when domains are overridden', () => {
|
|
65
|
+
render(
|
|
66
|
+
<Scatter
|
|
67
|
+
data={[{ id: 'big', x: 999, y: 999 }]}
|
|
68
|
+
width={200}
|
|
69
|
+
height={200}
|
|
70
|
+
axis={{ xMin: 0, xMax: 1000, yMin: 0, yMax: 1000 }}
|
|
71
|
+
/>,
|
|
72
|
+
)
|
|
73
|
+
expect(screen.getByTestId('scatter-point-big')).toBeInTheDocument()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('accessibility', () => {
|
|
77
|
+
it('labels the canvas as an image', () => {
|
|
78
|
+
render(<Scatter {...base} axis={{ xLabel: 'I', yLabel: 'A' }} />)
|
|
79
|
+
const canvas = screen.getByTestId('scatter-canvas')
|
|
80
|
+
expect(canvas).toHaveAttribute('role', 'img')
|
|
81
|
+
expect(canvas.getAttribute('aria-label')).toContain('3 points')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('has no accessibility violations', async () => {
|
|
85
|
+
const { container } = render(
|
|
86
|
+
<Scatter {...base} diagonal axis={{ xLabel: 'I', yLabel: 'A' }} selectedId="b" />,
|
|
87
|
+
)
|
|
88
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('has no accessibility violations when empty', async () => {
|
|
92
|
+
const { container } = render(<Scatter data={[]} width={200} height={200} />)
|
|
93
|
+
expect(await axe(container)).toHaveNoViolations()
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
})
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { View, Text, Pressable, type ViewProps } from 'react-native'
|
|
2
|
+
import { cn } from '../../../utils/cn'
|
|
3
|
+
|
|
4
|
+
export interface ScatterDatum {
|
|
5
|
+
/** Stable identity — returned by onPress and used as the React key. */
|
|
6
|
+
id: string
|
|
7
|
+
/** Horizontal position in data space. */
|
|
8
|
+
x: number
|
|
9
|
+
/** Vertical position in data space. */
|
|
10
|
+
y: number
|
|
11
|
+
/** Bubble radius in px. Falls back to a fixed default when omitted. */
|
|
12
|
+
r?: number
|
|
13
|
+
/** Bubble fill color. Falls back to a built-in categorical palette by index. */
|
|
14
|
+
color?: string
|
|
15
|
+
/** Optional label shown for large or selected points. */
|
|
16
|
+
label?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ScatterAxis {
|
|
20
|
+
xLabel?: string
|
|
21
|
+
yLabel?: string
|
|
22
|
+
/** Domain overrides. Any omitted bound is derived from the data. */
|
|
23
|
+
xMin?: number
|
|
24
|
+
xMax?: number
|
|
25
|
+
yMin?: number
|
|
26
|
+
yMax?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ScatterProps extends Omit<ViewProps, 'children'> {
|
|
30
|
+
data: ScatterDatum[]
|
|
31
|
+
/** Layout box. Required — the plot fills exactly this area. */
|
|
32
|
+
width: number
|
|
33
|
+
height: number
|
|
34
|
+
/** Axis labels and optional domain overrides. */
|
|
35
|
+
axis?: ScatterAxis
|
|
36
|
+
/**
|
|
37
|
+
* Draw the y = 1 − x "main sequence" reference line (NDepend idiom, where
|
|
38
|
+
* distance from the diagonal measures the zone of pain / zone of uselessness).
|
|
39
|
+
*/
|
|
40
|
+
diagonal?: boolean
|
|
41
|
+
/** Fires with a point's id on press. */
|
|
42
|
+
onPress?: (id: string) => void
|
|
43
|
+
/** Draws a highlight ring on the matching point. */
|
|
44
|
+
selectedId?: string
|
|
45
|
+
className?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Titan categorical fallback palette (data-1..data-8). */
|
|
49
|
+
const PALETTE = [
|
|
50
|
+
'#5B9BD5', '#14B8A6', '#F4A736', '#F83030',
|
|
51
|
+
'#823CA0', '#D4A520', '#406D87', '#43C6B7',
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
const GRID_LINE = 'rgba(255,255,255,0.07)'
|
|
55
|
+
const AXIS_LINE = 'rgba(255,255,255,0.18)'
|
|
56
|
+
const DIAGONAL_LINE = 'rgba(255,255,255,0.28)'
|
|
57
|
+
const DEFAULT_R = 6
|
|
58
|
+
|
|
59
|
+
const PLOT_LEFT = 40
|
|
60
|
+
const PLOT_RIGHT = 12
|
|
61
|
+
const PLOT_TOP = 12
|
|
62
|
+
const PLOT_BOTTOM = 34
|
|
63
|
+
const TICK_COUNT = 5
|
|
64
|
+
|
|
65
|
+
interface Domain {
|
|
66
|
+
min: number
|
|
67
|
+
max: number
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Derive a padded [min, max] from values, honoring explicit overrides. */
|
|
71
|
+
function domainOf(values: number[], min?: number, max?: number): Domain {
|
|
72
|
+
const lo = min ?? (values.length ? Math.min(...values) : 0)
|
|
73
|
+
const hi = max ?? (values.length ? Math.max(...values) : 1)
|
|
74
|
+
if (hi > lo) return { min: lo, max: hi }
|
|
75
|
+
const pad = Math.abs(hi) * 0.1 || 0.5
|
|
76
|
+
return { min: lo - pad, max: hi + pad }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Evenly spaced tick values across a domain. */
|
|
80
|
+
function ticksOf(d: Domain, count: number): number[] {
|
|
81
|
+
return Array.from({ length: count }, (_, i) => d.min + ((d.max - d.min) * i) / (count - 1))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatTick(v: number): string {
|
|
85
|
+
if (Number.isInteger(v)) return `${v}`
|
|
86
|
+
return v.toFixed(Math.abs(v) < 1 ? 2 : 1)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A single straight segment drawn as one rotated View (SVG-free). */
|
|
90
|
+
function LineSegment({
|
|
91
|
+
x1, y1, x2, y2, color, testID,
|
|
92
|
+
}: { x1: number; y1: number; x2: number; y2: number; color: string; testID: string }) {
|
|
93
|
+
const dx = x2 - x1
|
|
94
|
+
const dy = y2 - y1
|
|
95
|
+
const length = Math.sqrt(dx * dx + dy * dy)
|
|
96
|
+
const angle = Math.atan2(dy, dx) * (180 / Math.PI)
|
|
97
|
+
return (
|
|
98
|
+
<View
|
|
99
|
+
testID={testID}
|
|
100
|
+
accessibilityElementsHidden
|
|
101
|
+
style={{
|
|
102
|
+
position: 'absolute',
|
|
103
|
+
left: x1,
|
|
104
|
+
top: y1,
|
|
105
|
+
width: length,
|
|
106
|
+
height: 0,
|
|
107
|
+
borderTopWidth: 1,
|
|
108
|
+
borderStyle: 'dashed',
|
|
109
|
+
borderTopColor: color,
|
|
110
|
+
transformOrigin: '0 0',
|
|
111
|
+
transform: [{ rotate: `${angle}deg` }],
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* SVG-free scatter / bubble plot (absolutely-positioned Views), matching the
|
|
119
|
+
* codebase's chart convention so it renders identically on web and native. Used
|
|
120
|
+
* for "main-sequence" plots (instability × abstractness) via the `diagonal`
|
|
121
|
+
* reference line, or any two metrics. Positions and bubble sizes are computed
|
|
122
|
+
* from the data; color and labels are caller-supplied.
|
|
123
|
+
*/
|
|
124
|
+
export function Scatter({
|
|
125
|
+
data,
|
|
126
|
+
width,
|
|
127
|
+
height,
|
|
128
|
+
axis = {},
|
|
129
|
+
diagonal = false,
|
|
130
|
+
onPress,
|
|
131
|
+
selectedId,
|
|
132
|
+
className,
|
|
133
|
+
...props
|
|
134
|
+
}: ScatterProps) {
|
|
135
|
+
const innerW = Math.max(1, width - PLOT_LEFT - PLOT_RIGHT)
|
|
136
|
+
const innerH = Math.max(1, height - PLOT_TOP - PLOT_BOTTOM)
|
|
137
|
+
const xd = domainOf(data.map((d) => d.x), axis.xMin, axis.xMax)
|
|
138
|
+
const yd = domainOf(data.map((d) => d.y), axis.yMin, axis.yMax)
|
|
139
|
+
|
|
140
|
+
const toX = (x: number) => PLOT_LEFT + ((x - xd.min) / (xd.max - xd.min)) * innerW
|
|
141
|
+
const toY = (y: number) => PLOT_TOP + (1 - (y - yd.min) / (yd.max - yd.min)) * innerH
|
|
142
|
+
|
|
143
|
+
const points = data.map((d, i) => ({
|
|
144
|
+
datum: d,
|
|
145
|
+
cx: toX(d.x),
|
|
146
|
+
cy: toY(d.y),
|
|
147
|
+
radius: d.r ?? DEFAULT_R,
|
|
148
|
+
color: d.color ?? PALETTE[i % PALETTE.length],
|
|
149
|
+
}))
|
|
150
|
+
|
|
151
|
+
const ariaLabel = `Scatter plot of ${axis.xLabel ?? 'x'} versus ${axis.yLabel ?? 'y'}, ${data.length} point${data.length === 1 ? '' : 's'}`
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<View className={cn('relative', className)} style={{ width, height }} {...props}>
|
|
155
|
+
{/* Non-interactive labelled canvas: grid, axes, ticks, diagonal. */}
|
|
156
|
+
<View
|
|
157
|
+
accessibilityRole="image"
|
|
158
|
+
accessibilityLabel={ariaLabel}
|
|
159
|
+
testID="scatter-canvas"
|
|
160
|
+
style={{ position: 'absolute', top: 0, left: 0, width, height, overflow: 'hidden' }}
|
|
161
|
+
>
|
|
162
|
+
{ticksOf(yd, TICK_COUNT).map((t, i) => {
|
|
163
|
+
const y = toY(t)
|
|
164
|
+
return (
|
|
165
|
+
<View
|
|
166
|
+
key={`y-${i}`}
|
|
167
|
+
accessibilityElementsHidden
|
|
168
|
+
testID="scatter-gridline-y"
|
|
169
|
+
style={{ position: 'absolute', left: PLOT_LEFT, width: innerW, top: y, height: 1, backgroundColor: GRID_LINE }}
|
|
170
|
+
>
|
|
171
|
+
<Text className="text-[9px] text-text-tertiary" style={{ position: 'absolute', left: -PLOT_LEFT, top: -6, width: PLOT_LEFT - 6, textAlign: 'right' }}>
|
|
172
|
+
{formatTick(t)}
|
|
173
|
+
</Text>
|
|
174
|
+
</View>
|
|
175
|
+
)
|
|
176
|
+
})}
|
|
177
|
+
{ticksOf(xd, TICK_COUNT).map((t, i) => {
|
|
178
|
+
const x = toX(t)
|
|
179
|
+
return (
|
|
180
|
+
<View
|
|
181
|
+
key={`x-${i}`}
|
|
182
|
+
accessibilityElementsHidden
|
|
183
|
+
testID="scatter-gridline-x"
|
|
184
|
+
style={{ position: 'absolute', top: PLOT_TOP, height: innerH, left: x, width: 1, backgroundColor: GRID_LINE }}
|
|
185
|
+
>
|
|
186
|
+
<Text className="text-[9px] text-text-tertiary" style={{ position: 'absolute', top: innerH + 4, left: -16, width: 32, textAlign: 'center' }}>
|
|
187
|
+
{formatTick(t)}
|
|
188
|
+
</Text>
|
|
189
|
+
</View>
|
|
190
|
+
)
|
|
191
|
+
})}
|
|
192
|
+
|
|
193
|
+
{/* Axis frame (left + bottom). */}
|
|
194
|
+
<View accessibilityElementsHidden style={{ position: 'absolute', left: PLOT_LEFT, top: PLOT_TOP, width: 1, height: innerH, backgroundColor: AXIS_LINE }} />
|
|
195
|
+
<View accessibilityElementsHidden style={{ position: 'absolute', left: PLOT_LEFT, top: PLOT_TOP + innerH, width: innerW, height: 1, backgroundColor: AXIS_LINE }} />
|
|
196
|
+
|
|
197
|
+
{diagonal && (
|
|
198
|
+
<LineSegment
|
|
199
|
+
testID="scatter-diagonal"
|
|
200
|
+
color={DIAGONAL_LINE}
|
|
201
|
+
x1={toX(xd.min)}
|
|
202
|
+
y1={toY(1 - xd.min)}
|
|
203
|
+
x2={toX(xd.max)}
|
|
204
|
+
y2={toY(1 - xd.max)}
|
|
205
|
+
/>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
{axis.xLabel && (
|
|
209
|
+
<Text testID="scatter-x-label" className="text-[10px] font-semibold text-text-secondary" style={{ position: 'absolute', left: PLOT_LEFT, width: innerW, bottom: 2, textAlign: 'center' }}>
|
|
210
|
+
{axis.xLabel}
|
|
211
|
+
</Text>
|
|
212
|
+
)}
|
|
213
|
+
{axis.yLabel && (
|
|
214
|
+
<Text testID="scatter-y-label" className="text-[10px] font-semibold text-text-secondary" style={{ position: 'absolute', left: -innerH / 2 + 6, top: PLOT_TOP + innerH / 2, width: innerH, textAlign: 'center', transformOrigin: '50% 50%', transform: [{ rotate: '-90deg' }] }}>
|
|
215
|
+
{axis.yLabel}
|
|
216
|
+
</Text>
|
|
217
|
+
)}
|
|
218
|
+
</View>
|
|
219
|
+
|
|
220
|
+
{/* Interactive point overlay (kept out of the image-role canvas). */}
|
|
221
|
+
<View style={{ position: 'absolute', top: 0, left: 0, width, height }} testID="scatter-points">
|
|
222
|
+
{points.map((p) => {
|
|
223
|
+
const selected = p.datum.id === selectedId
|
|
224
|
+
const showLabel = p.datum.label && (selected || p.radius >= 10)
|
|
225
|
+
return (
|
|
226
|
+
<Pressable
|
|
227
|
+
key={p.datum.id}
|
|
228
|
+
testID={`scatter-point-${p.datum.id}`}
|
|
229
|
+
accessibilityRole="button"
|
|
230
|
+
accessibilityLabel={p.datum.label ?? p.datum.id}
|
|
231
|
+
onPress={() => onPress?.(p.datum.id)}
|
|
232
|
+
style={{
|
|
233
|
+
position: 'absolute',
|
|
234
|
+
left: p.cx - p.radius,
|
|
235
|
+
top: p.cy - p.radius,
|
|
236
|
+
width: p.radius * 2,
|
|
237
|
+
height: p.radius * 2,
|
|
238
|
+
borderRadius: p.radius,
|
|
239
|
+
backgroundColor: p.color,
|
|
240
|
+
opacity: selected ? 1 : 0.82,
|
|
241
|
+
borderWidth: selected ? 2 : 0,
|
|
242
|
+
borderColor: '#ffffff',
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
{showLabel && (
|
|
246
|
+
<Text
|
|
247
|
+
numberOfLines={1}
|
|
248
|
+
className="text-[9px] font-semibold text-text-primary"
|
|
249
|
+
style={{ position: 'absolute', left: p.radius * 2 + 3, top: p.radius - 6, width: 80 }}
|
|
250
|
+
>
|
|
251
|
+
{p.datum.label}
|
|
252
|
+
</Text>
|
|
253
|
+
)}
|
|
254
|
+
</Pressable>
|
|
255
|
+
)
|
|
256
|
+
})}
|
|
257
|
+
</View>
|
|
258
|
+
</View>
|
|
259
|
+
)
|
|
260
|
+
}
|
|
@@ -64,7 +64,7 @@ export function Sidebar({
|
|
|
64
64
|
<View
|
|
65
65
|
style={{ width: currentWidth }}
|
|
66
66
|
className={cn(
|
|
67
|
-
'h-full flex-col bg-surface-base border-r border-border
|
|
67
|
+
'h-full flex-col bg-surface-base border-r border-border transition-all duration-200',
|
|
68
68
|
className
|
|
69
69
|
)}
|
|
70
70
|
{...props}
|
|
@@ -515,6 +515,33 @@ describe('Table', () => {
|
|
|
515
515
|
expect(screen.getByRole('button', { name: /sort by name/i })).toBeInTheDocument()
|
|
516
516
|
})
|
|
517
517
|
|
|
518
|
+
it('exposes real ARIA table semantics (table/rowgroup/row/columnheader/cell)', () => {
|
|
519
|
+
renderBasicTable()
|
|
520
|
+
expect(screen.getByRole('table')).toBeInTheDocument()
|
|
521
|
+
// header rowgroup + body rowgroup
|
|
522
|
+
expect(screen.getAllByRole('rowgroup')).toHaveLength(2)
|
|
523
|
+
// 1 header row + 2 body rows
|
|
524
|
+
expect(screen.getAllByRole('row')).toHaveLength(3)
|
|
525
|
+
expect(screen.getAllByRole('columnheader')).toHaveLength(3)
|
|
526
|
+
expect(screen.getAllByRole('cell')).toHaveLength(6)
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
it('sortable header reflects sort state via aria-sort', () => {
|
|
530
|
+
render(
|
|
531
|
+
<Table sortColumn="name" sortDirection="asc" onSort={vi.fn()}>
|
|
532
|
+
<TableHeader>
|
|
533
|
+
<TableRow>
|
|
534
|
+
<TableHeaderCell sortKey="name">Name</TableHeaderCell>
|
|
535
|
+
<TableHeaderCell sortKey="email">Email</TableHeaderCell>
|
|
536
|
+
</TableRow>
|
|
537
|
+
</TableHeader>
|
|
538
|
+
</Table>
|
|
539
|
+
)
|
|
540
|
+
const [name, email] = screen.getAllByRole('columnheader')
|
|
541
|
+
expect(name).toHaveAttribute('aria-sort', 'ascending')
|
|
542
|
+
expect(email).toHaveAttribute('aria-sort', 'none')
|
|
543
|
+
})
|
|
544
|
+
|
|
518
545
|
it('pagination buttons have accessible labels', () => {
|
|
519
546
|
render(
|
|
520
547
|
<TablePagination
|
|
@@ -101,7 +101,7 @@ export function Table({
|
|
|
101
101
|
>
|
|
102
102
|
<View className={cn('w-full', className)} {...props}>
|
|
103
103
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
104
|
-
<View className="w-full min-w-full">
|
|
104
|
+
<View role="table" className="w-full min-w-full">
|
|
105
105
|
{isLoading ? (
|
|
106
106
|
<TableLoadingSkeleton rowCount={loadingRowCount} />
|
|
107
107
|
) : (
|
|
@@ -125,7 +125,7 @@ export interface TableHeaderProps {
|
|
|
125
125
|
export function TableHeader({ children, className }: TableHeaderProps) {
|
|
126
126
|
return (
|
|
127
127
|
<View
|
|
128
|
-
|
|
128
|
+
role="rowgroup"
|
|
129
129
|
className={cn('bg-background-subtle rounded-t-lg', className)}
|
|
130
130
|
>
|
|
131
131
|
{children}
|
|
@@ -143,7 +143,7 @@ export interface TableBodyProps {
|
|
|
143
143
|
*/
|
|
144
144
|
export function TableBody({ children, className }: TableBodyProps) {
|
|
145
145
|
return (
|
|
146
|
-
<View
|
|
146
|
+
<View role="rowgroup" className={className}>
|
|
147
147
|
{children}
|
|
148
148
|
</View>
|
|
149
149
|
)
|
|
@@ -171,7 +171,7 @@ export function TableRow({
|
|
|
171
171
|
}: TableRowProps) {
|
|
172
172
|
return (
|
|
173
173
|
<View
|
|
174
|
-
|
|
174
|
+
role="row"
|
|
175
175
|
className={cn(
|
|
176
176
|
'flex-row border-b border-divider',
|
|
177
177
|
isHoverable && 'web:hover:bg-interactive-hover',
|
|
@@ -212,6 +212,13 @@ export function TableHeaderCell({
|
|
|
212
212
|
const { sortColumn, sortDirection, onSort } = useContext(TableContext)
|
|
213
213
|
const isSorted = sortKey && sortColumn === sortKey
|
|
214
214
|
const isSortable = !!sortKey && !!onSort
|
|
215
|
+
const ariaSort = isSorted
|
|
216
|
+
? sortDirection === 'asc'
|
|
217
|
+
? 'ascending'
|
|
218
|
+
: sortDirection === 'desc'
|
|
219
|
+
? 'descending'
|
|
220
|
+
: 'none'
|
|
221
|
+
: 'none'
|
|
215
222
|
|
|
216
223
|
const handlePress = (e: any) => {
|
|
217
224
|
if (isSortable && sortKey) {
|
|
@@ -245,27 +252,35 @@ export function TableHeaderCell({
|
|
|
245
252
|
)
|
|
246
253
|
|
|
247
254
|
if (isSortable) {
|
|
255
|
+
// columnheader cell wraps the sort button so it carries proper table
|
|
256
|
+
// semantics (role + aria-sort) while the inner control keeps its button role.
|
|
248
257
|
return (
|
|
249
|
-
<
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
onPress={handlePress}
|
|
258
|
+
<View
|
|
259
|
+
role="columnheader"
|
|
260
|
+
aria-sort={ariaSort}
|
|
253
261
|
style={width ? { width } : { flex: 1 }}
|
|
254
|
-
className={cn(
|
|
255
|
-
'flex-row items-center px-4 py-3',
|
|
256
|
-
alignStyles[align],
|
|
257
|
-
'web:hover:bg-interactive-hover active:bg-interactive-active',
|
|
258
|
-
className
|
|
259
|
-
)}
|
|
260
|
-
{...props}
|
|
261
262
|
>
|
|
262
|
-
|
|
263
|
-
|
|
263
|
+
<Pressable
|
|
264
|
+
accessibilityRole="button"
|
|
265
|
+
accessibilityLabel={`Sort by ${children}`}
|
|
266
|
+
onPress={handlePress}
|
|
267
|
+
className={cn(
|
|
268
|
+
'flex-row items-center px-4 py-3',
|
|
269
|
+
alignStyles[align],
|
|
270
|
+
'web:hover:bg-interactive-hover active:bg-interactive-active',
|
|
271
|
+
className
|
|
272
|
+
)}
|
|
273
|
+
{...props}
|
|
274
|
+
>
|
|
275
|
+
{content}
|
|
276
|
+
</Pressable>
|
|
277
|
+
</View>
|
|
264
278
|
)
|
|
265
279
|
}
|
|
266
280
|
|
|
267
281
|
return (
|
|
268
282
|
<View
|
|
283
|
+
role="columnheader"
|
|
269
284
|
style={width ? { width } : { flex: 1 }}
|
|
270
285
|
className={cn(
|
|
271
286
|
'flex-row items-center px-4 py-3',
|
|
@@ -306,6 +321,7 @@ export function TableCell({
|
|
|
306
321
|
|
|
307
322
|
return (
|
|
308
323
|
<View
|
|
324
|
+
role="cell"
|
|
309
325
|
style={width ? { width } : { flex: 1 }}
|
|
310
326
|
className={cn(
|
|
311
327
|
'justify-center px-4 py-3.5',
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { Treemap, type TreemapDatum } from './Treemap'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Treemap> = {
|
|
5
|
+
title: 'Custom/Treemap',
|
|
6
|
+
component: Treemap,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
argTypes: {
|
|
9
|
+
width: { control: { type: 'range', min: 200, max: 900, step: 20 } },
|
|
10
|
+
height: { control: { type: 'range', min: 120, max: 600, step: 20 } },
|
|
11
|
+
scale: { control: 'select', options: ['linear', 'sqrt', 'log'] },
|
|
12
|
+
maxTiles: { control: { type: 'range', min: 4, max: 80, step: 1 } },
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
export default meta
|
|
16
|
+
type Story = StoryObj<typeof Treemap>
|
|
17
|
+
|
|
18
|
+
const sample: TreemapDatum[] = [
|
|
19
|
+
{ id: 'router', value: 5035, color: '#F83030', label: 'router.ts' },
|
|
20
|
+
{ id: 'engine', value: 4378, color: '#F83030', label: 'engine.ts' },
|
|
21
|
+
{ id: 'linker', value: 1695, color: '#F4A736', label: 'linker.ts' },
|
|
22
|
+
{ id: 'validate', value: 1288, color: '#F4A736', label: 'validate.ts' },
|
|
23
|
+
{ id: 'hook', value: 1050, color: '#F4A736', label: 'hook.ts' },
|
|
24
|
+
{ id: 'update', value: 890, color: '#5B9BD5', label: 'update.ts' },
|
|
25
|
+
{ id: 'pool', value: 504, color: '#5B9BD5', label: 'pool.ts' },
|
|
26
|
+
{ id: 'log', value: 342, color: '#5B9BD5', label: 'log.ts' },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
export const Default: Story = {
|
|
30
|
+
args: { data: sample, width: 560, height: 300, scale: 'sqrt' },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A single outlier under a linear scale swallows the canvas... */
|
|
34
|
+
export const OutlierLinear: Story = {
|
|
35
|
+
args: {
|
|
36
|
+
data: [{ id: 'mega', value: 205422, color: '#F83030', label: 'mega.ts' }, ...sample],
|
|
37
|
+
width: 560,
|
|
38
|
+
height: 300,
|
|
39
|
+
scale: 'linear',
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** ...the same data with a sqrt scale keeps every tile legible. */
|
|
44
|
+
export const OutlierSqrt: Story = {
|
|
45
|
+
args: {
|
|
46
|
+
data: [{ id: 'mega', value: 205422, color: '#F83030', label: 'mega.ts' }, ...sample],
|
|
47
|
+
width: 560,
|
|
48
|
+
height: 300,
|
|
49
|
+
scale: 'sqrt',
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const Truncated: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
data: Array.from({ length: 200 }, (_, i) => ({ id: `n${i}`, value: 200 - i })),
|
|
56
|
+
width: 560,
|
|
57
|
+
height: 300,
|
|
58
|
+
maxTiles: 40,
|
|
59
|
+
},
|
|
60
|
+
}
|