@opendata-ai/openchart-core 8.0.0-rc.9 → 8.1.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.
Files changed (83) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +403 -45
  3. package/dist/index.js +50 -18
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/dist/styles.css.map +1 -1
  7. package/package.json +1 -2
  8. package/schema/chart.schema.json +375 -69
  9. package/schema/table.schema.json +35 -1
  10. package/schema/vizspec.schema.json +992 -171
  11. package/src/accessibility/__tests__/alt-text.test.ts +0 -110
  12. package/src/accessibility/__tests__/aria.test.ts +0 -125
  13. package/src/accessibility/alt-text.ts +0 -155
  14. package/src/accessibility/aria.ts +0 -75
  15. package/src/accessibility/index.ts +0 -6
  16. package/src/colors/__tests__/colorblind.test.ts +0 -75
  17. package/src/colors/__tests__/contrast.test.ts +0 -119
  18. package/src/colors/__tests__/palettes.test.ts +0 -74
  19. package/src/colors/colorblind.ts +0 -131
  20. package/src/colors/contrast.ts +0 -143
  21. package/src/colors/index.ts +0 -36
  22. package/src/colors/palettes.ts +0 -206
  23. package/src/helpers/__tests__/deep-merge.test.ts +0 -54
  24. package/src/helpers/__tests__/spec-builders.test.ts +0 -338
  25. package/src/helpers/deep-merge.ts +0 -29
  26. package/src/helpers/spec-builders.ts +0 -479
  27. package/src/index.ts +0 -215
  28. package/src/labels/__tests__/collision.test.ts +0 -310
  29. package/src/labels/collision.ts +0 -249
  30. package/src/labels/index.ts +0 -13
  31. package/src/layout/__tests__/chrome.test.ts +0 -208
  32. package/src/layout/__tests__/text-measure.test.ts +0 -57
  33. package/src/layout/chrome.ts +0 -470
  34. package/src/layout/index.ts +0 -18
  35. package/src/layout/text-measure.ts +0 -404
  36. package/src/layout/text-wrap.ts +0 -93
  37. package/src/locale/__tests__/format.test.ts +0 -606
  38. package/src/locale/format.ts +0 -459
  39. package/src/locale/index.ts +0 -21
  40. package/src/responsive/__tests__/breakpoints.test.ts +0 -146
  41. package/src/responsive/__tests__/metrics.test.ts +0 -52
  42. package/src/responsive/breakpoints.ts +0 -177
  43. package/src/responsive/index.ts +0 -53
  44. package/src/responsive/metrics.ts +0 -299
  45. package/src/schema/__tests__/generated-artifacts.test.ts +0 -42
  46. package/src/schema/__tests__/schema.test.ts +0 -179
  47. package/src/styles/__tests__/reduced-motion-selectors.test.ts +0 -28
  48. package/src/styles/animation.css +0 -213
  49. package/src/styles/base.css +0 -102
  50. package/src/styles/chrome.css +0 -174
  51. package/src/styles/dark.css +0 -44
  52. package/src/styles/graph.css +0 -135
  53. package/src/styles/index.css +0 -28
  54. package/src/styles/keyframes.css +0 -151
  55. package/src/styles/legend.css +0 -18
  56. package/src/styles/reduced-motion.css +0 -67
  57. package/src/styles/series-search.css +0 -153
  58. package/src/styles/sparkline.css +0 -16
  59. package/src/styles/table-animation.css +0 -93
  60. package/src/styles/table.css +0 -398
  61. package/src/styles/token-definitions.ts +0 -199
  62. package/src/styles/tokens.css +0 -156
  63. package/src/styles/tooltip.css +0 -126
  64. package/src/styles/you-draw-it.css +0 -126
  65. package/src/theme/__tests__/dark-mode.test.ts +0 -232
  66. package/src/theme/__tests__/defaults.test.ts +0 -73
  67. package/src/theme/__tests__/resolve.test.ts +0 -277
  68. package/src/theme/dark-mode.ts +0 -258
  69. package/src/theme/defaults.ts +0 -110
  70. package/src/theme/index.ts +0 -8
  71. package/src/theme/presets.ts +0 -64
  72. package/src/theme/resolve.ts +0 -312
  73. package/src/types/__tests__/encoding.test.ts +0 -368
  74. package/src/types/__tests__/gradient.test.ts +0 -95
  75. package/src/types/__tests__/spec.test.ts +0 -499
  76. package/src/types/encoding.ts +0 -280
  77. package/src/types/events.ts +0 -236
  78. package/src/types/index.ts +0 -290
  79. package/src/types/layout.ts +0 -2024
  80. package/src/types/schema-roots.ts +0 -32
  81. package/src/types/spec.ts +0 -2925
  82. package/src/types/table.ts +0 -108
  83. package/src/types/theme.ts +0 -209
@@ -1,179 +0,0 @@
1
- /**
2
- * Behavior tests for the published JSON Schema files.
3
- *
4
- * These validate the SHIPPED artifacts (`packages/core/schema/*.json`) against
5
- * ajv, the same draft-07 validator an LLM tool-use harness would use. They
6
- * prove three properties the plan requires:
7
- *
8
- * 1. Every one of the 16 mark types has a minimal valid spec the schema
9
- * accepts (guards against a future mark being added to the types but not
10
- * reflected in the generated schema).
11
- * 2. Hallucinated fields (unknown top-level keys, unknown marks, unknown
12
- * encoding channels) are rejected — the mechanic that kills empty-chart
13
- * generation failures.
14
- * 3. Representative realistic specs across chart and table validate.
15
- *
16
- * The per-mark specs are built from `MARK_ENCODING_RULES`, the same source the
17
- * engine validates against, so the coverage tracks the real required-channel
18
- * matrix rather than a hand-maintained copy.
19
- */
20
-
21
- import { readFileSync } from 'node:fs';
22
- import { dirname, join } from 'node:path';
23
- import { fileURLToPath } from 'node:url';
24
- import Ajv, { type ValidateFunction } from 'ajv';
25
- import { describe, expect, it } from 'vitest';
26
- import { MARK_ENCODING_RULES } from '../../types/encoding';
27
- import { MARK_TYPES } from '../../types/spec';
28
-
29
- const schemaDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'schema');
30
-
31
- function loadValidator(filename: string): ValidateFunction {
32
- const schema = JSON.parse(readFileSync(join(schemaDir, filename), 'utf8'));
33
- // strict:false — the generated schema uses draft-07 with union types that
34
- // ajv's strict mode flags; allowUnionTypes matches the tool-use posture.
35
- const ajv = new Ajv({ strict: false, allowUnionTypes: true });
36
- return ajv.compile(schema);
37
- }
38
-
39
- const validateChart = loadValidator('chart.schema.json');
40
- const validateViz = loadValidator('vizspec.schema.json');
41
- const validateTable = loadValidator('table.schema.json');
42
-
43
- /** A data row carrying one column of each field type. */
44
- const sampleData = [
45
- { cat: 'A', amount: 10, when: '2020-01-01' },
46
- { cat: 'B', amount: 20, when: '2021-01-01' },
47
- ];
48
-
49
- /** Pick a field of an allowed type for a required channel. */
50
- function fieldForTypes(allowedTypes: string[]): { field: string; type: string } {
51
- if (allowedTypes.includes('temporal')) return { field: 'when', type: 'temporal' };
52
- if (allowedTypes.includes('quantitative')) return { field: 'amount', type: 'quantitative' };
53
- if (allowedTypes.includes('nominal')) return { field: 'cat', type: 'nominal' };
54
- if (allowedTypes.includes('ordinal')) return { field: 'cat', type: 'ordinal' };
55
- return { field: 'cat', type: 'nominal' };
56
- }
57
-
58
- /**
59
- * Build a minimal spec the schema should accept for a given mark, from its
60
- * required channels in MARK_ENCODING_RULES.
61
- */
62
- function minimalSpecForMark(mark: string): Record<string, unknown> {
63
- const rules = MARK_ENCODING_RULES[mark as keyof typeof MARK_ENCODING_RULES] ?? {};
64
- const encoding: Record<string, unknown> = {};
65
- for (const [channel, rule] of Object.entries(rules)) {
66
- if (rule?.required) {
67
- const types = rule.allowedTypes.length ? rule.allowedTypes : ['quantitative'];
68
- encoding[channel] = fieldForTypes(types);
69
- }
70
- }
71
- // Ensure at least one positional channel so the spec is meaningful.
72
- if (encoding.x === undefined && encoding.y === undefined) {
73
- const yTypes = rules.y?.allowedTypes?.length ? rules.y.allowedTypes : ['quantitative'];
74
- encoding.y = fieldForTypes(yTypes);
75
- }
76
- return { mark, data: sampleData, encoding };
77
- }
78
-
79
- describe('chart.schema.json', () => {
80
- it('has a schema definition for exactly the 16 declared mark types', () => {
81
- // Every MARK_TYPES entry must produce a spec the schema accepts.
82
- for (const mark of MARK_TYPES) {
83
- const spec = minimalSpecForMark(mark);
84
- const valid = validateChart(spec);
85
- expect(valid, `mark "${mark}" should validate: ${JSON.stringify(validateChart.errors)}`).toBe(
86
- true,
87
- );
88
- }
89
- });
90
-
91
- it('rejects an unknown mark type', () => {
92
- const spec = {
93
- mark: 'notarealmark',
94
- data: sampleData,
95
- encoding: { y: fieldForTypes(['quantitative']) },
96
- };
97
- expect(validateChart(spec)).toBe(false);
98
- });
99
-
100
- it('rejects a hallucinated top-level field', () => {
101
- const spec = {
102
- mark: 'bar',
103
- data: sampleData,
104
- encoding: { y: { field: 'amount', type: 'quantitative' } },
105
- totallyMadeUpField: 42,
106
- };
107
- expect(validateChart(spec)).toBe(false);
108
- });
109
-
110
- it('rejects a hallucinated encoding channel', () => {
111
- const spec = {
112
- mark: 'bar',
113
- data: sampleData,
114
- encoding: { notAChannel: { field: 'amount', type: 'quantitative' } },
115
- };
116
- expect(validateChart(spec)).toBe(false);
117
- });
118
-
119
- it('accepts a realistic bar chart with chrome and a color legend', () => {
120
- const spec = {
121
- mark: 'bar',
122
- data: [
123
- { language: 'Python', popularity: 29 },
124
- { language: 'JavaScript', popularity: 24 },
125
- ],
126
- encoding: {
127
- x: { field: 'popularity', type: 'quantitative' },
128
- y: { field: 'language', type: 'nominal' },
129
- color: { field: 'popularity', type: 'quantitative' },
130
- },
131
- chrome: { title: 'Python leads', source: 'Source: survey' },
132
- };
133
- expect(validateChart(spec), JSON.stringify(validateChart.errors)).toBe(true);
134
- });
135
- });
136
-
137
- describe('vizspec.schema.json', () => {
138
- it('accepts a chart spec (mark-discriminated union member)', () => {
139
- expect(validateViz(minimalSpecForMark('line'))).toBe(true);
140
- });
141
-
142
- it('accepts a table spec (type-discriminated union member)', () => {
143
- const spec = {
144
- type: 'table',
145
- data: sampleData,
146
- columns: [{ key: 'cat' }, { key: 'amount' }],
147
- };
148
- expect(validateViz(spec), JSON.stringify(validateViz.errors)).toBe(true);
149
- });
150
-
151
- it('rejects a spec that is neither a chart nor any known type', () => {
152
- expect(validateViz({ nonsense: true })).toBe(false);
153
- });
154
- });
155
-
156
- describe('table.schema.json', () => {
157
- it('accepts a table with formatted columns', () => {
158
- const spec = {
159
- type: 'table',
160
- data: sampleData,
161
- columns: [
162
- { key: 'cat', label: 'Category' },
163
- { key: 'amount', format: '.0f', align: 'right' },
164
- ],
165
- search: true,
166
- };
167
- expect(validateTable(spec), JSON.stringify(validateTable.errors)).toBe(true);
168
- });
169
-
170
- it('rejects a hallucinated top-level field on a table', () => {
171
- const spec = {
172
- type: 'table',
173
- data: sampleData,
174
- columns: [{ key: 'cat' }],
175
- madeUp: true,
176
- };
177
- expect(validateTable(spec)).toBe(false);
178
- });
179
- });
@@ -1,28 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { resolve } from 'node:path';
3
- import { describe, expect, it } from 'vitest';
4
- import { CSS_TOKEN_ROOT_SELECTORS } from '../token-definitions';
5
-
6
- describe('reduced-motion.css', () => {
7
- it('uses the same root selectors as CSS_TOKEN_ROOT_SELECTORS', () => {
8
- const css = readFileSync(resolve(__dirname, '../reduced-motion.css'), 'utf8');
9
- const selectors = CSS_TOKEN_ROOT_SELECTORS.every((sel) => css.includes(sel));
10
- expect(selectors).toBe(true);
11
-
12
- const isBlocks = [...css.matchAll(/:is\(\s*([\s\S]*?)\s*\)/g)];
13
- const selectorsInCss = new Set(
14
- isBlocks.flatMap((m) =>
15
- m[1]
16
- .split(',')
17
- .map((s) => s.trim())
18
- .filter(Boolean),
19
- ),
20
- );
21
- for (const sel of CSS_TOKEN_ROOT_SELECTORS) {
22
- expect(selectorsInCss.has(sel)).toBe(true);
23
- }
24
- for (const sel of selectorsInCss) {
25
- expect(CSS_TOKEN_ROOT_SELECTORS).toContain(sel);
26
- }
27
- });
28
- });
@@ -1,213 +0,0 @@
1
- @layer oc.animation {
2
- /* ---------------------------------------------------------------------------
3
- * Animation scoped rules (.oc-animate enables animation on the chart root)
4
- * --------------------------------------------------------------------------- */
5
-
6
- .oc-animate {
7
- /* Vertical bars (default): smooth ease-out, no overshoot.
8
- oc-mark-bar is reserved for future mark type aliases. */
9
- & .oc-mark-rect rect,
10
- & .oc-mark-bar rect {
11
- animation: oc-enter-bar var(--oc-animation-duration) var(--oc-ease-smooth) both;
12
- animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));
13
- }
14
-
15
- /* Horizontal bars (data-orient is on the mark group, not the SVG root) */
16
- & .oc-mark-rect[data-orient="horizontal"] rect,
17
- & .oc-mark-bar[data-orient="horizontal"] rect {
18
- animation-name: oc-enter-bar-h;
19
- }
20
-
21
- /* Stacked bar/column segments: chain sequentially so each segment starts
22
- right when the previous one finishes, creating one fluid reveal.
23
- Uses linear easing so handoffs between segments are seamless (no
24
- deceleration/acceleration stutter at segment boundaries). */
25
- & .oc-mark-rect[data-stack-pos] rect {
26
- animation-duration: var(--oc-stack-segment-duration, 150ms);
27
- animation-timing-function: linear;
28
- animation-delay: calc(
29
- var(--oc-animation-stagger) *
30
- var(--oc-mark-index, 0) +
31
- var(--oc-stack-pos, 0) *
32
- var(--oc-stack-segment-duration, 150ms)
33
- );
34
- }
35
-
36
- /* Line marks: entire group clips left-to-right (no WAAPI needed) */
37
- & .oc-mark-line {
38
- animation: oc-enter-line var(--oc-animation-duration)
39
- var(--oc-animation-ease, var(--oc-ease-smooth)) both;
40
- animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));
41
- }
42
-
43
- /* Area marks: entire group clips left-to-right + fades in */
44
- & .oc-mark-area {
45
- animation: oc-enter-line var(--oc-animation-duration)
46
- var(--oc-animation-ease, var(--oc-ease-smooth)) both;
47
- animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));
48
- }
49
-
50
- /* Arc marks (pie/donut slices): simple fade in.
51
- Scale transforms break arc positioning because arcs use translate()
52
- on parent groups. A clean fade is more elegant for pie/donut anyway. */
53
- & .oc-mark-arc {
54
- animation: oc-enter-fade-only var(--oc-animation-duration)
55
- var(--oc-animation-ease, var(--oc-ease-smooth)) both;
56
- animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));
57
- }
58
-
59
- /* Point marks (scatter, dot).
60
- Points render as bare <circle> elements with class oc-mark-point directly,
61
- not wrapped in a group, so we target circle.oc-mark-point (not descendant).
62
- Duration is 40% of the configured duration (quick pop-in relative to other marks). */
63
- & circle.oc-mark-point,
64
- & circle.oc-mark-circle {
65
- animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.4)
66
- var(--oc-animation-ease, var(--oc-ease-smooth)) both;
67
- animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));
68
- }
69
-
70
- /* Points on line/area charts: delay so they pop in as the line draws through. */
71
- & .oc-mark-line ~ circle.oc-mark-point,
72
- & .oc-mark-area ~ circle.oc-mark-point {
73
- animation-delay: calc(
74
- var(--oc-animation-duration) *
75
- 0.35 +
76
- var(--oc-animation-stagger) *
77
- var(--oc-mark-index, 0)
78
- );
79
- }
80
-
81
- /* Text marks: fade + slight slide up */
82
- & .oc-mark-text text {
83
- animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.6)
84
- var(--oc-animation-ease, var(--oc-ease-smooth)) both;
85
- animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));
86
- }
87
-
88
- /* Rule marks: fade in */
89
- & .oc-mark-rule line {
90
- animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.5)
91
- var(--oc-animation-ease, var(--oc-ease-smooth)) both;
92
- animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));
93
- }
94
-
95
- /* Tick marks: fade in */
96
- & .oc-mark-tick line {
97
- animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.5)
98
- var(--oc-animation-ease, var(--oc-ease-smooth)) both;
99
- animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));
100
- }
101
-
102
- /* Data labels: fade after their parent mark */
103
- & .oc-mark-label {
104
- animation: oc-enter-fade 300ms var(--oc-ease-smooth) both;
105
- animation-delay: calc(
106
- var(--oc-animation-stagger) *
107
- var(--oc-mark-index, 0) +
108
- var(--oc-animation-duration) *
109
- 0.7
110
- );
111
- }
112
-
113
- /* Annotations: fade in after marks finish */
114
- & .oc-annotation {
115
- animation: oc-enter-fade 400ms var(--oc-ease-smooth) both;
116
- animation-delay: calc(var(--oc-animation-duration) + var(--oc-annotation-delay, 200ms));
117
- }
118
-
119
- /* Tilemap tiles: fade in with jittered stagger for organic feel.
120
- Per-tile delay is computed in the renderer with pseudo-random variation. */
121
- & .oc-tilemap-tile {
122
- animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.35) ease-in both;
123
- animation-delay: var(--oc-tile-delay, 0ms);
124
- }
125
-
126
- /* Map features: fill color reveal with shuffled stagger.
127
- Per-feature --oc-map-delay is computed in the renderer via a seeded
128
- Fisher-Yates shuffle so features pop in organically. */
129
- & .oc-map-feature {
130
- animation: oc-enter-map-fill var(--oc-animation-duration)
131
- var(--oc-animation-ease, var(--oc-ease-smooth)) both;
132
- animation-delay: var(--oc-map-delay, 0ms);
133
- }
134
-
135
- /* Bulk mode: high feature counts (counties, ~3k+) get a single group
136
- fade instead of per-feature fill animations. opacity is GPU-compositable
137
- so this stays smooth regardless of child count. */
138
- & .oc-map-features[data-bulk-animate] {
139
- animation: oc-enter-fade-only var(--oc-animation-duration)
140
- var(--oc-animation-ease, var(--oc-ease-smooth)) both;
141
- }
142
-
143
- & .oc-map-features[data-bulk-animate] .oc-map-feature {
144
- animation: none;
145
- }
146
-
147
- /* Map borders: fade in near the end of the feature sweep */
148
- & .oc-map-borders path {
149
- animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.4) ease-in both;
150
- animation-delay: calc(var(--oc-animation-duration) * 0.7);
151
- }
152
-
153
- /* Map points: gentle scale (0.75->1) + fade pop-in after borders.
154
- transform-origin set per-circle in the renderer so scale happens
155
- around each circle's own center, not the SVG origin. */
156
- & .oc-map-point {
157
- animation: oc-enter-map-point calc(var(--oc-animation-duration) * 0.4)
158
- var(--oc-animation-ease, var(--oc-ease-smooth)) both;
159
- animation-delay: calc(
160
- var(--oc-animation-duration) *
161
- 0.8 +
162
- var(--oc-mark-index, 0) *
163
- var(--oc-point-stagger, 60ms)
164
- );
165
- }
166
-
167
- /* Sankey nodes: fade in with stagger by column depth (left-to-right) */
168
- & .oc-sankey-node rect {
169
- animation: oc-enter-fade-only var(--oc-animation-duration)
170
- var(--oc-animation-ease, var(--oc-ease-smooth)) both;
171
- animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));
172
- }
173
-
174
- /* Sankey links: fade in after nodes, with stagger.
175
- Links delay by 30% of duration so nodes appear first, then links flow in. */
176
- & .oc-sankey-link path {
177
- animation: oc-enter-fade-only var(--oc-animation-duration)
178
- var(--oc-animation-ease, var(--oc-ease-smooth)) both;
179
- animation-delay: calc(
180
- var(--oc-animation-duration) *
181
- 0.3 +
182
- var(--oc-animation-stagger) *
183
- var(--oc-mark-index, 0)
184
- );
185
- }
186
-
187
- /* Barlist rows: fade + slide up per row, bars grow left-to-right */
188
- & .oc-barlist-row {
189
- animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.6)
190
- var(--oc-animation-ease, var(--oc-ease-smooth)) both;
191
- animation-delay: var(--oc-row-delay, 0ms);
192
- }
193
-
194
- & .oc-barlist-bar {
195
- animation: oc-enter-bar-h var(--oc-animation-duration)
196
- var(--oc-animation-ease, var(--oc-ease-smooth)) both;
197
- animation-delay: var(--oc-row-delay, 0ms);
198
- }
199
- }
200
-
201
- /* ---------------------------------------------------------------------------
202
- * Sparkline mode: pair the line/area reveal with a stronger ease-out so it
203
- * doesn't read as a uniform left-to-right swipe. cubic-bezier(0.16, 1, 0.3, 1)
204
- * is "expo-out" — fast initial draw that decelerates noticeably at the end,
205
- * giving the trend a hand-drawn feel. Duration is bumped via the engine
206
- * (compile.ts) so the cleanup timer stays in sync.
207
- * --------------------------------------------------------------------------- */
208
-
209
- .oc-animate[data-display="sparkline"] .oc-mark-line,
210
- .oc-animate[data-display="sparkline"] .oc-mark-area {
211
- animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
212
- }
213
- }
@@ -1,102 +0,0 @@
1
- @layer oc.base {
2
- /* ---------------------------------------------------------------------------
3
- * Wrapper roots
4
- * --------------------------------------------------------------------------- */
5
-
6
- .oc-chart-root {
7
- width: 100%;
8
- height: 100%;
9
- }
10
-
11
- .oc-table-root,
12
- .oc-graph-root,
13
- .oc-sankey-root,
14
- .oc-barlist-root,
15
- .oc-tilemap-root,
16
- .oc-map-root {
17
- width: 100%;
18
- height: 100%;
19
- }
20
-
21
- .oc-table-root {
22
- overflow: auto;
23
- }
24
-
25
- /* ---------------------------------------------------------------------------
26
- * Chart container
27
- * --------------------------------------------------------------------------- */
28
-
29
- .oc-chart {
30
- font-family: var(--oc-font-family);
31
- display: block;
32
- width: 100%;
33
- }
34
-
35
- /* ---------------------------------------------------------------------------
36
- * BarList SVG container
37
- * --------------------------------------------------------------------------- */
38
-
39
- .oc-tilemap,
40
- .oc-map {
41
- display: block;
42
- width: 100%;
43
- height: auto;
44
- }
45
-
46
- .oc-map-feature {
47
- transition: opacity 400ms ease;
48
- }
49
-
50
- .oc-barlist {
51
- display: block;
52
- width: 100%;
53
- }
54
-
55
- /* ---------------------------------------------------------------------------
56
- * Screen reader only utility
57
- * --------------------------------------------------------------------------- */
58
-
59
- .oc-sr-only {
60
- position: absolute;
61
- width: 1px;
62
- height: 1px;
63
- padding: 0;
64
- margin: -1px;
65
- overflow: hidden;
66
- clip-path: inset(50%);
67
- white-space: nowrap;
68
- border-width: 0;
69
- }
70
-
71
- /* ---------------------------------------------------------------------------
72
- * Editable hover feedback
73
- * --------------------------------------------------------------------------- */
74
-
75
- .oc-editable-hover {
76
- outline: 1.5px solid var(--oc-editable-hover);
77
- outline-offset: 2px;
78
- border-radius: 2px;
79
- }
80
-
81
- /* ---------------------------------------------------------------------------
82
- * Keyboard focus indicators
83
- *
84
- * --oc-focus carries a light value in tokens.css and a dark value in
85
- * dark.css, so both rings stay visible in either mode.
86
- * --------------------------------------------------------------------------- */
87
-
88
- /* Container ring when the chart itself receives keyboard focus. */
89
- .oc-root:focus-visible {
90
- outline: 2px solid var(--oc-focus);
91
- outline-offset: 2px;
92
- }
93
-
94
- /* Mark ring during arrow-key navigation (see wireKeyboardNav). Outline on
95
- * SVG elements draws around the mark's bounding box, same technique as
96
- * .oc-editable-hover above. */
97
- .oc-mark-focused {
98
- outline: 2px solid var(--oc-focus);
99
- outline-offset: 1px;
100
- border-radius: 2px;
101
- }
102
- }
@@ -1,174 +0,0 @@
1
- @layer oc.components {
2
- /* ---------------------------------------------------------------------------
3
- * Chrome (eyebrow, title, subtitle, source, byline, footer, brand)
4
- * --------------------------------------------------------------------------- */
5
-
6
- .oc-chrome {
7
- font-family: var(--oc-font-family);
8
- }
9
-
10
- .oc-eyebrow {
11
- font-size: var(--oc-eyebrow-size);
12
- font-weight: var(--oc-eyebrow-weight);
13
- letter-spacing: var(--oc-eyebrow-tracking);
14
- text-transform: uppercase;
15
- fill: var(--oc-accent);
16
- }
17
-
18
- .oc-title {
19
- font-size: var(--oc-title-size);
20
- font-weight: var(--oc-title-weight);
21
- letter-spacing: var(--oc-title-tracking);
22
- fill: var(--oc-text);
23
- }
24
-
25
- .oc-subtitle {
26
- font-size: var(--oc-subtitle-size);
27
- font-weight: var(--oc-subtitle-weight);
28
- fill: var(--oc-text-muted);
29
- }
30
-
31
- .oc-source,
32
- .oc-byline,
33
- .oc-footer {
34
- font-size: var(--oc-source-size);
35
- font-weight: var(--oc-source-weight);
36
- fill: var(--oc-text-muted);
37
- }
38
-
39
- .oc-brand {
40
- font-size: 11px;
41
- font-weight: 510;
42
- letter-spacing: 0.02em;
43
- fill: var(--oc-text-faint);
44
- }
45
-
46
- .oc-brand-dot {
47
- fill: var(--oc-accent);
48
- }
49
-
50
- .oc-eyebrow-dot {
51
- fill: var(--oc-accent);
52
- }
53
-
54
- /* ---------------------------------------------------------------------------
55
- * Metric bar (.oc-metrics) — optional row of label/value cells above chart
56
- * --------------------------------------------------------------------------- */
57
-
58
- .oc-metrics {
59
- font-family: var(--oc-font-family);
60
- }
61
-
62
- .oc-metric-label {
63
- font-size: 10px;
64
- font-weight: 510;
65
- letter-spacing: 0.08em;
66
- text-transform: uppercase;
67
- fill: var(--oc-text-muted);
68
- }
69
-
70
- .oc-metric-value {
71
- font-size: 22px;
72
- font-weight: 510;
73
- letter-spacing: -0.01em;
74
- fill: var(--oc-text);
75
- font-variant-numeric: tabular-nums;
76
- }
77
-
78
- .oc-metric-delta-up {
79
- fill: var(--oc-positive);
80
- font-size: 12px;
81
- font-weight: 510;
82
- }
83
-
84
- .oc-metric-delta-down {
85
- fill: var(--oc-negative);
86
- font-size: 12px;
87
- font-weight: 510;
88
- }
89
-
90
- .oc-metric-secondary {
91
- fill: var(--oc-positive);
92
- font-size: 12px;
93
- font-weight: 400;
94
- }
95
-
96
- /* ---------------------------------------------------------------------------
97
- * Axis tick labels — inline variant sits above each gridline at the
98
- * chart-area left edge (no left gutter). Used by line/area y-axes.
99
- * --------------------------------------------------------------------------- */
100
-
101
- .oc-axis-tick-inline {
102
- font-size: 11px;
103
- font-weight: 400;
104
- fill: var(--oc-text-muted);
105
- }
106
-
107
- /* ---------------------------------------------------------------------------
108
- * Endpoint labels (right-side per-series column for line/area charts)
109
- *
110
- * Swatch idiom: a thicker colored stroke segment with an optional thinner
111
- * lighter strip below. Mirrors the refreshed traditional legend swatch so a
112
- * single chart never shows two swatch idioms (mock-2 alignment). Per-series
113
- * color is stamped inline by the renderer; CSS tokens here cover the muted
114
- * value text and the leader line so theme overrides flow through naturally.
115
- * --------------------------------------------------------------------------- */
116
-
117
- .oc-endpoint-labels {
118
- font-family: var(--oc-font-family);
119
- }
120
-
121
- .oc-endpoint-label {
122
- /* Color set inline per series; this fallback only kicks in if the renderer
123
- ever omits the inline fill. */
124
- fill: var(--oc-endpoint-label-color, var(--oc-text));
125
- }
126
-
127
- .oc-endpoint-value {
128
- fill: var(--oc-endpoint-value-color, var(--oc-text-muted));
129
- }
130
-
131
- .oc-endpoint-leader {
132
- stroke: var(--oc-endpoint-leader-color, currentColor);
133
- }
134
-
135
- /* .oc-endpoint-marker — fill/stroke set inline (open-ring style: bg fill, series stroke). */
136
- /* .oc-endpoint-swatch — color set inline per series. */
137
-
138
- /* ---------------------------------------------------------------------------
139
- * Annotation dot + subtitle (text-annotation extensions)
140
- * --------------------------------------------------------------------------- */
141
-
142
- /* .oc-annotation-dot — fill/stroke resolved by the engine (open-ring default = bg fill, series stroke). */
143
-
144
- .oc-annotation-subtitle {
145
- fill: var(--oc-annotation-subtitle-color, var(--oc-text-muted));
146
- }
147
-
148
- /* ---------------------------------------------------------------------------
149
- * Table footer chrome
150
- * --------------------------------------------------------------------------- */
151
-
152
- /* Footer row: source/footer text on the left, brand watermark on the right,
153
- * sharing a single baseline. gap keeps them apart if the row gets narrow. */
154
- .oc-table-footer-row {
155
- display: flex;
156
- align-items: baseline;
157
- gap: 16px;
158
- padding: 16px 16px 4px;
159
- }
160
-
161
- /* Chrome block sits inside the flex row now, so it no longer owns the
162
- * footer's top padding or horizontal inset (the row provides both). */
163
- .oc-chrome-footer {
164
- padding-top: 0;
165
- }
166
-
167
- /* Brand watermark: pushed to the right edge (margin-left:auto holds it there
168
- * even when the source text is absent), never shrinks below its own text. */
169
- .oc-table-footer-row .oc-table-ref {
170
- margin-left: auto;
171
- flex-shrink: 0;
172
- text-align: right;
173
- }
174
- }