@opendata-ai/openchart-core 8.0.0-rc.2 → 8.0.0-rc.21

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 (82) hide show
  1. package/dist/index.d.ts +490 -25
  2. package/dist/index.js +57 -19
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +1 -1
  5. package/dist/styles.css.map +1 -1
  6. package/package.json +1 -2
  7. package/schema/chart.schema.json +485 -35
  8. package/schema/table.schema.json +35 -1
  9. package/schema/vizspec.schema.json +1305 -120
  10. package/src/accessibility/__tests__/alt-text.test.ts +0 -110
  11. package/src/accessibility/__tests__/aria.test.ts +0 -125
  12. package/src/accessibility/alt-text.ts +0 -155
  13. package/src/accessibility/aria.ts +0 -75
  14. package/src/accessibility/index.ts +0 -6
  15. package/src/colors/__tests__/colorblind.test.ts +0 -75
  16. package/src/colors/__tests__/contrast.test.ts +0 -119
  17. package/src/colors/__tests__/palettes.test.ts +0 -74
  18. package/src/colors/colorblind.ts +0 -131
  19. package/src/colors/contrast.ts +0 -143
  20. package/src/colors/index.ts +0 -36
  21. package/src/colors/palettes.ts +0 -206
  22. package/src/helpers/__tests__/deep-merge.test.ts +0 -54
  23. package/src/helpers/__tests__/spec-builders.test.ts +0 -338
  24. package/src/helpers/deep-merge.ts +0 -29
  25. package/src/helpers/spec-builders.ts +0 -479
  26. package/src/index.ts +0 -215
  27. package/src/labels/__tests__/collision.test.ts +0 -310
  28. package/src/labels/collision.ts +0 -249
  29. package/src/labels/index.ts +0 -13
  30. package/src/layout/__tests__/chrome.test.ts +0 -208
  31. package/src/layout/__tests__/text-measure.test.ts +0 -57
  32. package/src/layout/chrome.ts +0 -470
  33. package/src/layout/index.ts +0 -18
  34. package/src/layout/text-measure.ts +0 -404
  35. package/src/layout/text-wrap.ts +0 -93
  36. package/src/locale/__tests__/format.test.ts +0 -606
  37. package/src/locale/format.ts +0 -459
  38. package/src/locale/index.ts +0 -21
  39. package/src/responsive/__tests__/breakpoints.test.ts +0 -146
  40. package/src/responsive/__tests__/metrics.test.ts +0 -52
  41. package/src/responsive/breakpoints.ts +0 -172
  42. package/src/responsive/index.ts +0 -53
  43. package/src/responsive/metrics.ts +0 -299
  44. package/src/schema/__tests__/generated-artifacts.test.ts +0 -42
  45. package/src/schema/__tests__/schema.test.ts +0 -179
  46. package/src/styles/__tests__/reduced-motion-selectors.test.ts +0 -28
  47. package/src/styles/animation.css +0 -199
  48. package/src/styles/base.css +0 -102
  49. package/src/styles/chrome.css +0 -174
  50. package/src/styles/dark.css +0 -44
  51. package/src/styles/graph.css +0 -135
  52. package/src/styles/index.css +0 -28
  53. package/src/styles/keyframes.css +0 -135
  54. package/src/styles/legend.css +0 -18
  55. package/src/styles/reduced-motion.css +0 -67
  56. package/src/styles/series-search.css +0 -153
  57. package/src/styles/sparkline.css +0 -16
  58. package/src/styles/table-animation.css +0 -93
  59. package/src/styles/table.css +0 -398
  60. package/src/styles/token-definitions.ts +0 -199
  61. package/src/styles/tokens.css +0 -156
  62. package/src/styles/tooltip.css +0 -126
  63. package/src/styles/you-draw-it.css +0 -126
  64. package/src/theme/__tests__/dark-mode.test.ts +0 -232
  65. package/src/theme/__tests__/defaults.test.ts +0 -73
  66. package/src/theme/__tests__/resolve.test.ts +0 -277
  67. package/src/theme/dark-mode.ts +0 -258
  68. package/src/theme/defaults.ts +0 -110
  69. package/src/theme/index.ts +0 -8
  70. package/src/theme/presets.ts +0 -64
  71. package/src/theme/resolve.ts +0 -312
  72. package/src/types/__tests__/encoding.test.ts +0 -368
  73. package/src/types/__tests__/gradient.test.ts +0 -95
  74. package/src/types/__tests__/spec.test.ts +0 -499
  75. package/src/types/encoding.ts +0 -280
  76. package/src/types/events.ts +0 -154
  77. package/src/types/index.ts +0 -287
  78. package/src/types/layout.ts +0 -1966
  79. package/src/types/schema-roots.ts +0 -32
  80. package/src/types/spec.ts +0 -2867
  81. package/src/types/table.ts +0 -108
  82. 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,199 +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
- /* Sankey nodes: fade in with stagger by column depth (left-to-right) */
154
- & .oc-sankey-node rect {
155
- animation: oc-enter-fade-only var(--oc-animation-duration)
156
- var(--oc-animation-ease, var(--oc-ease-smooth)) both;
157
- animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));
158
- }
159
-
160
- /* Sankey links: fade in after nodes, with stagger.
161
- Links delay by 30% of duration so nodes appear first, then links flow in. */
162
- & .oc-sankey-link path {
163
- animation: oc-enter-fade-only var(--oc-animation-duration)
164
- var(--oc-animation-ease, var(--oc-ease-smooth)) both;
165
- animation-delay: calc(
166
- var(--oc-animation-duration) *
167
- 0.3 +
168
- var(--oc-animation-stagger) *
169
- var(--oc-mark-index, 0)
170
- );
171
- }
172
-
173
- /* Barlist rows: fade + slide up per row, bars grow left-to-right */
174
- & .oc-barlist-row {
175
- animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.6)
176
- var(--oc-animation-ease, var(--oc-ease-smooth)) both;
177
- animation-delay: var(--oc-row-delay, 0ms);
178
- }
179
-
180
- & .oc-barlist-bar {
181
- animation: oc-enter-bar-h var(--oc-animation-duration)
182
- var(--oc-animation-ease, var(--oc-ease-smooth)) both;
183
- animation-delay: var(--oc-row-delay, 0ms);
184
- }
185
- }
186
-
187
- /* ---------------------------------------------------------------------------
188
- * Sparkline mode: pair the line/area reveal with a stronger ease-out so it
189
- * doesn't read as a uniform left-to-right swipe. cubic-bezier(0.16, 1, 0.3, 1)
190
- * is "expo-out" — fast initial draw that decelerates noticeably at the end,
191
- * giving the trend a hand-drawn feel. Duration is bumped via the engine
192
- * (compile.ts) so the cleanup timer stays in sync.
193
- * --------------------------------------------------------------------------- */
194
-
195
- .oc-animate[data-display="sparkline"] .oc-mark-line,
196
- .oc-animate[data-display="sparkline"] .oc-mark-area {
197
- animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
198
- }
199
- }
@@ -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
- }