vccs 1.0.0-beta.2 → 1.0.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.
@@ -0,0 +1,49 @@
1
+ # Tracker
2
+
3
+ Use `Tracker` for an ordered sequence of discrete statuses, such as daily service health or build results. Use `Sparkline` for a numeric trend and `Heatmap` for a two-dimensional value matrix.
4
+
5
+ ## Data shape
6
+
7
+ - `data` contains one row per bar, ordered oldest first.
8
+ - `data-key` selects the status and defaults to `status`.
9
+ - `name-key` selects the label and bar identity; it defaults to `date`.
10
+ - Built-in statuses are `up`, `degraded`, `down`, and `maintenance`; custom status strings need their own colors and labels.
11
+
12
+ ## Minimal example
13
+
14
+ ```vue
15
+ <script setup lang="ts">
16
+ import { Tooltip, Tracker } from 'vccs'
17
+
18
+ const data = [
19
+ { date: '2026-08-01', status: 'up' },
20
+ { date: '2026-08-02', status: 'up' },
21
+ { date: '2026-08-03', status: 'degraded' },
22
+ { date: '2026-08-04', status: 'up' },
23
+ { date: '2026-08-05', status: 'down' },
24
+ { date: '2026-08-06', status: 'up' },
25
+ { date: '2026-08-07', status: 'maintenance' },
26
+ ]
27
+ </script>
28
+
29
+ <template>
30
+ <Tracker
31
+ :data="data"
32
+ :height="32"
33
+ >
34
+ <Tooltip :cursor="false" />
35
+ </Tracker>
36
+ </template>
37
+ ```
38
+
39
+ ## Chart-specific rules
40
+
41
+ - Keep input rows oldest first; the component does not sort them into time order.
42
+ - Use a stable `name-key` so bars keep their identity when a moving window drops one row and appends another.
43
+ - For custom statuses, set both `status-colors` and `status-labels`; unknown statuses otherwise use the muted color and their raw text.
44
+ - Place `Tooltip` in the default slot. Cell events receive the original input row.
45
+ - Use the `#cell` slot to replace a bar; its slot cell wraps the domain row rather than being the row itself.
46
+
47
+ ## Related guides
48
+
49
+ `installation`, `vue-api`, `typed-rows`, `ssr-testing`, `sparkline`, `heatmap`, `calendar-heatmap`.
@@ -0,0 +1,68 @@
1
+ # Treemap
2
+
3
+ Use `Treemap` to compare hierarchical part-to-whole values as rectangles, especially when there are many leaves. Use `PieChart` for a short flat list of proportions and `SunburstChart` when radial rings communicate the hierarchy better.
4
+
5
+ ## Data shape
6
+
7
+ - `data` is an array of top-level nodes, not a single root object.
8
+ - Each node has a `name` by default and may have a `children` array.
9
+ - Leaves need a positive numeric `value`; `data-key` and `name-key` select different fields.
10
+ - Parent values are derived by summing their descendants, so do not add totals to parent rows.
11
+
12
+ ## Minimal example
13
+
14
+ ```vue
15
+ <script setup lang="ts">
16
+ import { Tooltip, Treemap } from 'vccs'
17
+
18
+ const data = [
19
+ {
20
+ name: 'Search',
21
+ children: [
22
+ { name: 'Google', value: 420 },
23
+ { name: 'Bing', value: 90 },
24
+ ],
25
+ },
26
+ {
27
+ name: 'Social',
28
+ children: [
29
+ { name: 'LinkedIn', value: 150 },
30
+ { name: 'Mastodon', value: 80 },
31
+ ],
32
+ },
33
+ {
34
+ name: 'Direct',
35
+ children: [
36
+ { name: 'Bookmarks', value: 160 },
37
+ { name: 'Newsletter', value: 110 },
38
+ ],
39
+ },
40
+ ]
41
+
42
+ const colors = ['#f97316', '#14b8a6', '#f59e0b', '#06b6d4']
43
+ </script>
44
+
45
+ <template>
46
+ <Treemap
47
+ :data="data"
48
+ :height="320"
49
+ data-key="value"
50
+ :colors="colors"
51
+ >
52
+ <Tooltip :cursor="false" />
53
+ </Treemap>
54
+ </template>
55
+ ```
56
+
57
+ Animation is enabled by default; retrieve `ssr-testing` before taking screenshots or asserting exact geometry.
58
+
59
+ ## Chart-specific rules
60
+
61
+ - The default `type="flat"` displays leaves. Their `colors` entry comes from their top-level group.
62
+ - Set `type="nest"` for drill-down. Clicking a group shows its children and a breadcrumb trail for going back.
63
+ - Use `stroke` to change the rectangle border color.
64
+ - Customize rectangles and labels with the `#content` slot, not a function-valued `content` prop.
65
+
66
+ ## Related guides
67
+
68
+ `installation`, `vue-api`, `ssr-testing`, `sunburst-chart`, `pie-chart`.
@@ -0,0 +1,68 @@
1
+ # Installation and registration
2
+
3
+ vccs requires Vue `^3.5`, the `motion-v` `^2.4` peer, and an ESM bundler. The Nuxt module requires Nuxt 4; the Vite resolver requires `unplugin-vue-components` in the application's dev dependencies.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pnpm add vccs motion-v
9
+ ```
10
+
11
+ Install `motion-v` explicitly because not every package manager installs peers automatically.
12
+
13
+ ## Explicit imports in Vite
14
+
15
+ Import components from `vccs` and compose them in the template. Retrieve the chart guide before choosing its children.
16
+
17
+ ```ts
18
+ import { Bar, BarChart, Tooltip, XAxis, YAxis } from 'vccs'
19
+ ```
20
+
21
+ Do not deep-import source files, runtime prop definitions, or internal helpers. The only public subpaths are `vccs/nuxt` and `vccs/resolver`.
22
+
23
+ ## Auto-imports in Vite
24
+
25
+ ```sh
26
+ pnpm add -D unplugin-vue-components
27
+ ```
28
+
29
+ ```ts
30
+ import vue from '@vitejs/plugin-vue'
31
+ import { defineConfig } from 'vite'
32
+ import Components from 'unplugin-vue-components/vite'
33
+ import { VccsResolver } from 'vccs/resolver'
34
+
35
+ export default defineConfig({
36
+ plugins: [
37
+ vue(),
38
+ Components({ resolvers: [VccsResolver()] }),
39
+ ],
40
+ })
41
+ ```
42
+
43
+ The resolver imports a component only when the template uses it. Keep explicit imports for types and `defineChartComponents`.
44
+
45
+ ## Nuxt 4
46
+
47
+ ```ts
48
+ export default defineNuxtConfig({
49
+ modules: ['vccs/nuxt'],
50
+ })
51
+ ```
52
+
53
+ The module registers every public component and composable. Import types and `defineChartComponents` from `vccs`.
54
+
55
+ When another UI library also registers `Tooltip`, `Legend`, or `Label`, add a prefix:
56
+
57
+ ```ts
58
+ export default defineNuxtConfig({
59
+ modules: ['vccs/nuxt'],
60
+ vccs: { prefix: 'V' },
61
+ })
62
+ ```
63
+
64
+ This registers names such as `VBarChart` and `VLine`. Nuxt 3 is unsupported by the module; do not hide its build error with `transpile` or `ClientOnly` workarounds.
65
+
66
+ ## Related guides
67
+
68
+ `vue-api`, `typed-rows`, `ssr-testing`.
@@ -0,0 +1,50 @@
1
+ # SSR and deterministic testing
2
+
3
+ Server-rendered charts normally preserve their entrance start, then animate after hydration when they enter the viewport. Do not mistake that intended entrance state for missing SVG or a hydration failure.
4
+
5
+ ## Complete server HTML
6
+
7
+ For a complete chart without client JavaScript, provide numeric `width` and `height` and disable animation on the chart root:
8
+
9
+ ```vue
10
+ <AreaChart
11
+ :data="data"
12
+ :width="900"
13
+ :height="250"
14
+ :is-animation-active="false"
15
+ >
16
+ <Area data-key="desktop" />
17
+ </AreaChart>
18
+ ```
19
+
20
+ Disabling animation alone does not reveal an unmeasured responsive chart. With only `height`, the server reserves a box using an initial width and the browser measures the real width later.
21
+
22
+ Do not add `ClientOnly` to solve chart SSR. Use it only when the data itself exists exclusively in the browser.
23
+
24
+ ## Nuxt
25
+
26
+ Retrieve `installation` and register `vccs/nuxt` under Nuxt 4. The module handles component and composable registration; fixed-size SSR needs no client-only wrapper.
27
+
28
+ ## Deterministic tests and screenshots
29
+
30
+ Set `is-animation-active` to `false` on the chart root, or on an individual series when only that series must be still:
31
+
32
+ ```vue
33
+ <BarChart
34
+ :data="rows"
35
+ :height="300"
36
+ :is-animation-active="false"
37
+ >
38
+ <Bar data-key="desktop" />
39
+ </BarChart>
40
+ ```
41
+
42
+ The chart value supplies defaults to its items; an explicit series value overrides it. Disable animation before exact geometry assertions, screenshots, or pixel-exact SSR comparisons.
43
+
44
+ In component tests, measure the container deliberately and assert library behavior through public classes such as `.v-charts-surface` and item attributes such as `data-v-charts-item-index`. Do not depend on generated SVG ids.
45
+
46
+ Charts follow the operating system's reduced-motion setting, but tests must not depend on the host setting: disable animation explicitly.
47
+
48
+ ## Related guides
49
+
50
+ `installation`, `vue-api`, `typed-rows`.
@@ -0,0 +1,47 @@
1
+ # Typed chart rows
2
+
3
+ A child component cannot infer the row type from its chart root's `data`. Create row-typed component aliases once when wrong `data-key` values or untyped tooltip rows should become compile-time errors.
4
+
5
+ ## Minimal example
6
+
7
+ ```vue
8
+ <script setup lang="ts">
9
+ import { Area, AreaChart, Tooltip, XAxis, defineChartComponents } from 'vccs'
10
+
11
+ interface Visit {
12
+ date: string
13
+ desktop: number
14
+ mobile: number
15
+ }
16
+
17
+ defineProps<{ visits: Visit[] }>()
18
+
19
+ const Chart = defineChartComponents<Visit>()({ AreaChart, Area, XAxis, Tooltip })
20
+ </script>
21
+
22
+ <template>
23
+ <Chart.AreaChart
24
+ :data="visits"
25
+ :height="300"
26
+ >
27
+ <Chart.XAxis data-key="date" />
28
+ <Chart.Area data-key="desktop" />
29
+ <Chart.Area data-key="mobile" />
30
+ <Chart.Tooltip />
31
+ </Chart.AreaChart>
32
+ </template>
33
+ ```
34
+
35
+ `Chart.Area data-key="tablet"` now fails typechecking because `tablet` is not a key of `Visit`. Tooltip content created through `Chart.Tooltip` receives payload entries whose `payload` is `Visit`.
36
+
37
+ ## Rules
38
+
39
+ - Select every component used with that row type in one `defineChartComponents<Row>()({ ... })` call; use the resulting aliases in the template.
40
+ - The runtime component references are unchanged. This helper narrows declarations, not behavior.
41
+ - Only selected components are referenced by the consumer bundle.
42
+ - Standalone charts can infer rows from `data`; use their retrieved guide's generic pattern when explicit aliases are unnecessary.
43
+ - Import reusable model and payload types such as `BrushStartEndIndex`, `TooltipActiveIndex`, `TooltipPayloadEntry`, and `TooltipPayload` from `vccs`. Runtime `*VueProps` objects and internal context helpers are not public.
44
+
45
+ ## Related guides
46
+
47
+ `installation`, `vue-api`, `ssr-testing`.
@@ -0,0 +1,72 @@
1
+ # Vue API rules
2
+
3
+ vccs keeps Recharts component names but uses Vue rendering and reactivity contracts. Apply these rules only after retrieving the chart guide; supported slots and models are component-specific.
4
+
5
+ ## Slots replace render props
6
+
7
+ Use named slots for custom shapes, dots, labels, ticks, tooltip content, and cursors. A function passed to a prop such as `dot` or `content` is not a Vue render contract.
8
+
9
+ ```vue
10
+ <Tooltip>
11
+ <template #content="{ active, payload, label }">
12
+ <div v-if="active">
13
+ <strong>{{ label }}</strong>
14
+ <div>{{ payload?.[0]?.name }}: {{ payload?.[0]?.value }}</div>
15
+ </div>
16
+ </template>
17
+ </Tooltip>
18
+ ```
19
+
20
+ Do not spread tooltip slot props with `v-bind="tooltipProps"`; the project's ESLint auto-fix removes that spread. Destructure the slot instead. Each tooltip payload entry exposes the original row as `payload`.
21
+
22
+ Common slot payloads:
23
+
24
+ - Tooltip `content`: `{ active, payload, label, coordinate, accessibilityLayer }`. Payload entries carry `name`, `value`, `color`, `unit`, `dataKey`, and the original row as `payload`.
25
+ - Line, Area, and Scatter `dot` / `activeDot`: `{ cx, cy, index, value, payload }` plus the series' presentation attributes. Render SVG at `cx` and `cy`.
26
+
27
+ ## Templates and events
28
+
29
+ - Write `class`, not `className`.
30
+ - Write SVG attributes in kebab-case, such as `stroke-width` and `stroke-dasharray`.
31
+ - Use Vue events such as `@click`, not callback props such as `:on-click`.
32
+ - Item event callbacks receive `(item, index, event)`.
33
+
34
+ ## Models
35
+
36
+ - `v-model:active-index` is `number | null`; `null` means no active item. Never substitute `-1`.
37
+ - `v-model:range` on `Brush` is `BrushStartEndIndex | null`; `null` selects all rows and replaces separate start/end models.
38
+ - `v-model:hidden` on `Legend` controls hidden series keys.
39
+ - A supplied model value is controlled state. An omitted model lets the component keep local state.
40
+
41
+ Only bind a model documented by the retrieved chart guide. Do not infer that every chart root supports every model.
42
+
43
+ ## Responsive size
44
+
45
+ Charts are responsive by default. Set a numeric `height` and let the chart fill its container's width; pass numeric `width` and `height` for a fixed box.
46
+
47
+ ```vue
48
+ <LineChart
49
+ :data="rows"
50
+ :height="300"
51
+ >
52
+ <!-- axes and series -->
53
+ </LineChart>
54
+ ```
55
+
56
+ Do not add `ResponsiveContainer`; it is deprecated and scheduled for removal in 2.0.
57
+
58
+ ## Data keys and missing values
59
+
60
+ `data-key` accepts a field name, numeric index, or accessor function. Set `name` or `name-key` when an accessor function's series needs a readable label. `NaN` and `Infinity` are missing values, not finite coordinates.
61
+
62
+ Retrieve `typed-rows` when wrong keys should become compile-time errors.
63
+
64
+ ## Theme and selectors
65
+
66
+ A series without an explicit color uses `--v-charts-series-1` through `--v-charts-series-8`. Override those variables to theme a dashboard instead of repeating colors on every series.
67
+
68
+ Library classes use the `v-charts-` prefix. Item marks expose `data-v-charts-item-index`; use these hooks for CSS and tests rather than Recharts class names.
69
+
70
+ ## Related guides
71
+
72
+ `installation`, `typed-rows`, `ssr-testing`.
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console -- a command-line report */
3
+ import { existsSync, readFileSync, realpathSync } from 'node:fs'
4
+ import { dirname, join } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const root = dirname(fileURLToPath(import.meta.url))
8
+ const packageJson = JSON.parse(readFileSync(join(root, '..', 'package.json'), 'utf8'))
9
+
10
+ const chartIds = [
11
+ 'area-chart',
12
+ 'bar-chart',
13
+ 'bar-list',
14
+ 'calendar-heatmap',
15
+ 'cohort-chart',
16
+ 'composed-chart',
17
+ 'funnel-chart',
18
+ 'heatmap',
19
+ 'journey-sankey',
20
+ 'line-chart',
21
+ 'pie-chart',
22
+ 'radar-chart',
23
+ 'radial-bar-chart',
24
+ 'sankey',
25
+ 'scatter-chart',
26
+ 'sparkline',
27
+ 'sunburst-chart',
28
+ 'tracker',
29
+ 'treemap',
30
+ ]
31
+
32
+ export function loadCatalog() {
33
+ const catalog = JSON.parse(readFileSync(join(root, 'catalog.json'), 'utf8'))
34
+ return { ...catalog, vccsVersion: packageJson.version }
35
+ }
36
+
37
+ function normalize(value) {
38
+ return value.toLowerCase().replaceAll('_', '-').trim()
39
+ }
40
+
41
+ const stopwords = new Set(['a', 'an', 'and', 'are', 'build', 'by', 'chart', 'create', 'for', 'from', 'graph', 'how', 'in', 'is', 'make', 'need', 'of', 'on', 'plot', 'show', 'the', 'to', 'use', 'using', 'want', 'with'])
42
+
43
+ function queryTokens(query) {
44
+ return normalize(query).split(/[^\p{L}\p{N}]+/u)
45
+ .filter(token => token.length > 1 && !stopwords.has(token))
46
+ }
47
+
48
+ function compact(value) {
49
+ return normalize(value).replace(/[^\p{L}\p{N}]/gu, '')
50
+ }
51
+
52
+ function summary(entry, score) {
53
+ return {
54
+ id: entry.id,
55
+ title: entry.title,
56
+ description: entry.description,
57
+ category: entry.category,
58
+ path: entry.path,
59
+ score,
60
+ }
61
+ }
62
+
63
+ function score(entry, query) {
64
+ const normalizedQuery = normalize(query)
65
+ if (!normalizedQuery)
66
+ return 0
67
+
68
+ const names = [entry.id, entry.title, ...entry.components]
69
+ const identity = normalize(names.join(' '))
70
+ const topics = normalize([entry.description, ...entry.useCases, ...entry.keywords].join(' '))
71
+ const exactName = names.some(name => compact(name) === compact(query))
72
+ let result = exactName ? 160 : identity.includes(normalizedQuery) ? 60 : 0
73
+
74
+ for (const token of queryTokens(query)) {
75
+ if (identity.includes(token))
76
+ result += 20
77
+ if (topics.includes(token))
78
+ result += 6
79
+ }
80
+ for (const phrase of [...entry.useCases, ...entry.keywords]) {
81
+ const normalizedPhrase = normalize(phrase)
82
+ if (normalizedPhrase.length > 1 && normalizedQuery.includes(normalizedPhrase))
83
+ result += 12
84
+ }
85
+ return result
86
+ }
87
+
88
+ export function searchCatalog(catalog, query, limit = 8) {
89
+ return catalog.guides
90
+ .map(entry => summary(entry, score(entry, query)))
91
+ .filter(entry => entry.score > 0)
92
+ .sort((a, b) => b.score - a.score || a.id.localeCompare(b.id))
93
+ .slice(0, limit)
94
+ }
95
+
96
+ export function retrieveGuides(catalog, ids) {
97
+ return ids.map((id) => {
98
+ const entry = catalog.guides.find(guide => guide.id === id)
99
+ if (!entry)
100
+ throw new Error(`Unknown guide "${id}". Run the list command to see valid ids.`)
101
+ return readFileSync(join(root, entry.path), 'utf8')
102
+ }).join('\n\n---\n\n')
103
+ }
104
+
105
+ export function validateCatalog(catalog) {
106
+ const errors = []
107
+ const seen = new Set()
108
+ for (const entry of catalog.guides) {
109
+ if (seen.has(entry.id))
110
+ errors.push(`Duplicate guide id: ${entry.id}`)
111
+ seen.add(entry.id)
112
+ for (const field of ['title', 'description', 'category', 'path']) {
113
+ if (!entry[field]) {
114
+ errors.push(`${entry.id}: missing ${field}`)
115
+ }
116
+ }
117
+ if (entry.path.includes('..') || !entry.path.startsWith('guides/'))
118
+ errors.push(`${entry.id}: unsafe guide path ${entry.path}`)
119
+ if (!existsSync(join(root, entry.path))) {
120
+ errors.push(`${entry.id}: missing file ${entry.path}`)
121
+ continue
122
+ }
123
+ if (entry.category === 'chart') {
124
+ const content = readFileSync(join(root, entry.path), 'utf8')
125
+ for (const marker of ['## Data shape', '## Minimal example', '## Chart-specific rules', '```vue']) {
126
+ if (!content.includes(marker)) {
127
+ errors.push(`${entry.id}: missing ${marker}`)
128
+ }
129
+ }
130
+ if (!content.includes(entry.components[0]))
131
+ errors.push(`${entry.id}: example does not name ${entry.components[0]}`)
132
+ }
133
+ }
134
+
135
+ const actualChartIds = catalog.guides.filter(entry => entry.category === 'chart').map(entry => entry.id).sort()
136
+ for (const id of chartIds) {
137
+ if (!actualChartIds.includes(id)) {
138
+ errors.push(`Missing chart guide: ${id}`)
139
+ }
140
+ }
141
+ for (const id of actualChartIds) {
142
+ if (!chartIds.includes(id)) {
143
+ errors.push(`Unexpected chart guide: ${id}`)
144
+ }
145
+ }
146
+ return errors
147
+ }
148
+
149
+ function usage() {
150
+ return 'Usage: vccs-usage <search "query" | list | retrieve "id[,id...]" | validate>'
151
+ }
152
+
153
+ function run(argv) {
154
+ const [command, argument] = argv
155
+ const catalog = loadCatalog()
156
+ if (command === 'search' && argument) {
157
+ const results = searchCatalog(catalog, argument)
158
+ if (!results.length)
159
+ throw new Error('No matching guides. Run the list command to browse all guides.')
160
+ console.log(JSON.stringify(results, null, 2))
161
+ return
162
+ }
163
+ if (command === 'list' && !argument) {
164
+ console.log(JSON.stringify(catalog.guides.map(entry => summary(entry, undefined)), null, 2))
165
+ return
166
+ }
167
+ if (command === 'retrieve' && argument) {
168
+ const ids = argument.split(',').map(id => id.trim()).filter(Boolean)
169
+ process.stdout.write(retrieveGuides(catalog, ids))
170
+ return
171
+ }
172
+ if (command === 'validate' && !argument) {
173
+ const errors = validateCatalog(catalog)
174
+ if (errors.length)
175
+ throw new Error(errors.join('\n'))
176
+ const chartCount = catalog.guides.filter(entry => entry.category === 'chart').length
177
+ console.log(`PASS: vccs ${catalog.vccsVersion}; ${catalog.guides.length} guides; ${chartCount} chart types; all chart guides have data and Vue examples.`)
178
+ return
179
+ }
180
+ throw new Error(usage())
181
+ }
182
+
183
+ if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
184
+ try {
185
+ run(process.argv.slice(2))
186
+ }
187
+ catch (error) {
188
+ console.error(error.message)
189
+ process.exitCode = 1
190
+ }
191
+ }