@opendata-ai/openchart-core 7.11.0 → 8.0.0-rc.10
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/index.d.ts +1348 -132
- package/dist/index.js +556 -48
- package/dist/index.js.map +1 -1
- package/dist/styles.css +2 -1
- package/dist/styles.css.map +1 -0
- package/package.json +9 -3
- package/schema/chart.schema.json +9086 -0
- package/schema/table.schema.json +861 -0
- package/schema/vizspec.schema.json +17922 -0
- package/src/accessibility/alt-text.ts +39 -1
- package/src/accessibility/aria.ts +4 -2
- package/src/colors/__tests__/colorblind.test.ts +12 -0
- package/src/colors/colorblind.ts +11 -2
- package/src/colors/contrast.ts +11 -0
- package/src/colors/index.ts +9 -1
- package/src/colors/palettes.ts +47 -0
- package/src/helpers/__tests__/deep-merge.test.ts +54 -0
- package/src/helpers/__tests__/spec-builders.test.ts +3 -3
- package/src/helpers/deep-merge.ts +29 -0
- package/src/helpers/spec-builders.ts +7 -7
- package/src/index.ts +29 -1
- package/src/labels/collision.ts +1 -0
- package/src/layout/chrome.ts +24 -0
- package/src/layout/index.ts +1 -1
- package/src/locale/__tests__/format.test.ts +347 -11
- package/src/locale/format.ts +243 -18
- package/src/locale/index.ts +10 -1
- package/src/responsive/breakpoints.ts +7 -2
- package/src/responsive/index.ts +2 -0
- package/src/responsive/metrics.ts +77 -10
- package/src/schema/__tests__/generated-artifacts.test.ts +42 -0
- package/src/schema/__tests__/schema.test.ts +179 -0
- package/src/styles/__tests__/reduced-motion-selectors.test.ts +28 -0
- package/src/styles/animation.css +211 -168
- package/src/styles/base.css +89 -62
- package/src/styles/chrome.css +173 -179
- package/src/styles/dark.css +38 -35
- package/src/styles/graph.css +116 -111
- package/src/styles/index.css +5 -0
- package/src/styles/keyframes.css +130 -99
- package/src/styles/legend.css +14 -12
- package/src/styles/reduced-motion.css +61 -40
- package/src/styles/series-search.css +153 -0
- package/src/styles/sparkline.css +14 -12
- package/src/styles/table-animation.css +82 -80
- package/src/styles/table.css +314 -302
- package/src/styles/token-definitions.ts +199 -0
- package/src/styles/tokens.css +132 -128
- package/src/styles/tooltip.css +109 -115
- package/src/styles/you-draw-it.css +126 -0
- package/src/types/__tests__/encoding.test.ts +86 -17
- package/src/types/__tests__/spec.test.ts +16 -12
- package/src/types/encoding.ts +69 -25
- package/src/types/events.ts +107 -13
- package/src/types/index.ts +42 -3
- package/src/types/layout.ts +484 -11
- package/src/types/schema-roots.ts +32 -0
- package/src/types/spec.ts +718 -87
- package/src/types/table.ts +3 -2
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
});
|
package/src/styles/animation.css
CHANGED
|
@@ -1,170 +1,213 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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);
|
|
12
212
|
}
|
|
13
|
-
|
|
14
|
-
/* Horizontal bars (data-orient is on the mark group, not the SVG root) */
|
|
15
|
-
& .oc-mark-rect[data-orient="horizontal"] rect,
|
|
16
|
-
& .oc-mark-bar[data-orient="horizontal"] rect {
|
|
17
|
-
animation-name: oc-enter-bar-h;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/* Stacked bar/column segments: chain sequentially so each segment starts
|
|
21
|
-
right when the previous one finishes, creating one fluid reveal.
|
|
22
|
-
Uses linear easing so handoffs between segments are seamless (no
|
|
23
|
-
deceleration/acceleration stutter at segment boundaries). */
|
|
24
|
-
& .oc-mark-rect[data-stack-pos] rect {
|
|
25
|
-
animation-duration: var(--oc-stack-segment-duration, 150ms);
|
|
26
|
-
animation-timing-function: linear;
|
|
27
|
-
animation-delay: calc(
|
|
28
|
-
var(--oc-animation-stagger) *
|
|
29
|
-
var(--oc-mark-index, 0) +
|
|
30
|
-
var(--oc-stack-pos, 0) *
|
|
31
|
-
var(--oc-stack-segment-duration, 150ms)
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/* Line marks: entire group clips left-to-right (no WAAPI needed) */
|
|
36
|
-
& .oc-mark-line {
|
|
37
|
-
animation: oc-enter-line var(--oc-animation-duration)
|
|
38
|
-
var(--oc-animation-ease, var(--oc-ease-smooth)) both;
|
|
39
|
-
animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/* Area marks: entire group clips left-to-right + fades in */
|
|
43
|
-
& .oc-mark-area {
|
|
44
|
-
animation: oc-enter-line var(--oc-animation-duration)
|
|
45
|
-
var(--oc-animation-ease, var(--oc-ease-smooth)) both;
|
|
46
|
-
animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/* Arc marks (pie/donut slices): simple fade in.
|
|
50
|
-
Scale transforms break arc positioning because arcs use translate()
|
|
51
|
-
on parent groups. A clean fade is more elegant for pie/donut anyway. */
|
|
52
|
-
& .oc-mark-arc {
|
|
53
|
-
animation: oc-enter-fade-only var(--oc-animation-duration)
|
|
54
|
-
var(--oc-animation-ease, var(--oc-ease-smooth)) both;
|
|
55
|
-
animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/* Point marks (scatter, dot).
|
|
59
|
-
Points render as bare <circle> elements with class oc-mark-point directly,
|
|
60
|
-
not wrapped in a group, so we target circle.oc-mark-point (not descendant).
|
|
61
|
-
Duration is 40% of the configured duration (quick pop-in relative to other marks). */
|
|
62
|
-
& circle.oc-mark-point,
|
|
63
|
-
& circle.oc-mark-circle {
|
|
64
|
-
animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.4)
|
|
65
|
-
var(--oc-animation-ease, var(--oc-ease-smooth)) both;
|
|
66
|
-
animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/* Points on line/area charts: delay so they pop in as the line draws through. */
|
|
70
|
-
& .oc-mark-line ~ circle.oc-mark-point,
|
|
71
|
-
& .oc-mark-area ~ circle.oc-mark-point {
|
|
72
|
-
animation-delay: calc(
|
|
73
|
-
var(--oc-animation-duration) *
|
|
74
|
-
0.35 +
|
|
75
|
-
var(--oc-animation-stagger) *
|
|
76
|
-
var(--oc-mark-index, 0)
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/* Text marks: fade + slight slide up */
|
|
81
|
-
& .oc-mark-text text {
|
|
82
|
-
animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.6)
|
|
83
|
-
var(--oc-animation-ease, var(--oc-ease-smooth)) both;
|
|
84
|
-
animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/* Rule marks: fade in */
|
|
88
|
-
& .oc-mark-rule line {
|
|
89
|
-
animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.5)
|
|
90
|
-
var(--oc-animation-ease, var(--oc-ease-smooth)) both;
|
|
91
|
-
animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/* Tick marks: fade in */
|
|
95
|
-
& .oc-mark-tick line {
|
|
96
|
-
animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.5)
|
|
97
|
-
var(--oc-animation-ease, var(--oc-ease-smooth)) both;
|
|
98
|
-
animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/* Data labels: fade after their parent mark */
|
|
102
|
-
& .oc-mark-label {
|
|
103
|
-
animation: oc-enter-fade 300ms var(--oc-ease-smooth) both;
|
|
104
|
-
animation-delay: calc(
|
|
105
|
-
var(--oc-animation-stagger) *
|
|
106
|
-
var(--oc-mark-index, 0) +
|
|
107
|
-
var(--oc-animation-duration) *
|
|
108
|
-
0.7
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/* Annotations: fade in after marks finish */
|
|
113
|
-
& .oc-annotation {
|
|
114
|
-
animation: oc-enter-fade 400ms var(--oc-ease-smooth) both;
|
|
115
|
-
animation-delay: calc(var(--oc-animation-duration) + var(--oc-annotation-delay, 200ms));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/* Tilemap tiles: fade in with jittered stagger for organic feel.
|
|
119
|
-
Per-tile delay is computed in the renderer with pseudo-random variation. */
|
|
120
|
-
& .oc-tilemap-tile {
|
|
121
|
-
animation: oc-enter-fade-only calc(var(--oc-animation-duration) * 0.35) ease-in both;
|
|
122
|
-
animation-delay: var(--oc-tile-delay, 0ms);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/* Sankey nodes: fade in with stagger by column depth (left-to-right) */
|
|
126
|
-
& .oc-sankey-node rect {
|
|
127
|
-
animation: oc-enter-fade-only var(--oc-animation-duration)
|
|
128
|
-
var(--oc-animation-ease, var(--oc-ease-smooth)) both;
|
|
129
|
-
animation-delay: calc(var(--oc-animation-stagger) * var(--oc-mark-index, 0));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/* Sankey links: fade in after nodes, with stagger.
|
|
133
|
-
Links delay by 30% of duration so nodes appear first, then links flow in. */
|
|
134
|
-
& .oc-sankey-link path {
|
|
135
|
-
animation: oc-enter-fade-only var(--oc-animation-duration)
|
|
136
|
-
var(--oc-animation-ease, var(--oc-ease-smooth)) both;
|
|
137
|
-
animation-delay: calc(
|
|
138
|
-
var(--oc-animation-duration) *
|
|
139
|
-
0.3 +
|
|
140
|
-
var(--oc-animation-stagger) *
|
|
141
|
-
var(--oc-mark-index, 0)
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/* Barlist rows: fade + slide up per row, bars grow left-to-right */
|
|
146
|
-
& .oc-barlist-row {
|
|
147
|
-
animation: oc-enter-fade calc(var(--oc-animation-duration) * 0.6)
|
|
148
|
-
var(--oc-animation-ease, var(--oc-ease-smooth)) both;
|
|
149
|
-
animation-delay: var(--oc-row-delay, 0ms);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
& .oc-barlist-bar {
|
|
153
|
-
animation: oc-enter-bar-h var(--oc-animation-duration)
|
|
154
|
-
var(--oc-animation-ease, var(--oc-ease-smooth)) both;
|
|
155
|
-
animation-delay: var(--oc-row-delay, 0ms);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/* ---------------------------------------------------------------------------
|
|
160
|
-
* Sparkline mode: pair the line/area reveal with a stronger ease-out so it
|
|
161
|
-
* doesn't read as a uniform left-to-right swipe. cubic-bezier(0.16, 1, 0.3, 1)
|
|
162
|
-
* is "expo-out" — fast initial draw that decelerates noticeably at the end,
|
|
163
|
-
* giving the trend a hand-drawn feel. Duration is bumped via the engine
|
|
164
|
-
* (compile.ts) so the cleanup timer stays in sync.
|
|
165
|
-
* --------------------------------------------------------------------------- */
|
|
166
|
-
|
|
167
|
-
.oc-animate[data-display="sparkline"] .oc-mark-line,
|
|
168
|
-
.oc-animate[data-display="sparkline"] .oc-mark-area {
|
|
169
|
-
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
|
170
213
|
}
|