@transtyle/exporter-echarts 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +42 -0
- package/src/index.js +226 -0
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@transtyle/exporter-echarts",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Transtyle exporter for Apache ECharts theme objects.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@transtyle/ir": "0.1.0-alpha.0"
|
|
12
|
+
},
|
|
13
|
+
"transtyle": {
|
|
14
|
+
"kind": "exporter",
|
|
15
|
+
"name": "echarts",
|
|
16
|
+
"irSpec": "v0-draft",
|
|
17
|
+
"pluginApi": "0",
|
|
18
|
+
"targets": {
|
|
19
|
+
"echarts": [
|
|
20
|
+
">=5 <7"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"modes": [
|
|
24
|
+
"color-scheme"
|
|
25
|
+
],
|
|
26
|
+
"capabilities": [
|
|
27
|
+
"build"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": { "access": "public" },
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/transtyle/transtyle.git",
|
|
37
|
+
"directory": "packages/exporter-echarts"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/transtyle/transtyle#readme",
|
|
40
|
+
"bugs": "https://github.com/transtyle/transtyle/issues",
|
|
41
|
+
"license": "MIT"
|
|
42
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @transtyle/exporter-echarts — emits Apache ECharts theme objects from the
|
|
3
|
+
* resolved IR. Spec: docs/specs/exporters/echarts.md.
|
|
4
|
+
*
|
|
5
|
+
* ECharts has no runtime mode concept: theme choice happens at init. The
|
|
6
|
+
* color-scheme dimension therefore maps to one theme per mode — the native
|
|
7
|
+
* pattern — and is classified `native` via file multiplication.
|
|
8
|
+
* Colors are hex (canvas rendering); OKLCH → hex may gamut-clamp (→ approximated).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { droppedDimensions } from '@transtyle/ir';
|
|
12
|
+
|
|
13
|
+
const S = 'semantic.color.';
|
|
14
|
+
const P = 'semantic.palette.categorical.';
|
|
15
|
+
const PALETTE_SIZE = 8;
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
name: 'echarts',
|
|
19
|
+
|
|
20
|
+
emit(normalized, ctx) {
|
|
21
|
+
const files = [];
|
|
22
|
+
const coverage = [];
|
|
23
|
+
const themeNames = [];
|
|
24
|
+
|
|
25
|
+
for (const mode of normalized.modeValues) {
|
|
26
|
+
const map = normalized.modes[mode];
|
|
27
|
+
const themeName = `${ctx.projectName}-${mode}`;
|
|
28
|
+
themeNames.push(themeName);
|
|
29
|
+
const { theme, modeCoverage } = buildTheme(map, mode, ctx);
|
|
30
|
+
// Coverage is identical across modes by construction; record once.
|
|
31
|
+
if (coverage.length === 0) coverage.push(...modeCoverage);
|
|
32
|
+
|
|
33
|
+
const json = JSON.stringify(theme, null, 2) + '\n';
|
|
34
|
+
files.push({ path: `theme.${themeName}.json`, contents: json, kind: 'theme' });
|
|
35
|
+
files.push({
|
|
36
|
+
path: `theme.${themeName}.js`,
|
|
37
|
+
contents: umdWrapper(themeName, theme, ctx),
|
|
38
|
+
kind: 'script',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Mode dimensions this exporter doesn't express (T8, ir.md#modes) — a
|
|
43
|
+
// no-op unless the compile actually declares one, e.g. `density`.
|
|
44
|
+
coverage.push(...droppedDimensions(normalized.dimensionNames, ['color-scheme']));
|
|
45
|
+
|
|
46
|
+
files.push({ path: 'usage.md', contents: renderUsage(ctx, themeNames, coverage), kind: 'doc' });
|
|
47
|
+
return { files, coverage };
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// ---------- theme construction ----------
|
|
52
|
+
|
|
53
|
+
function buildTheme(map, mode, ctx) {
|
|
54
|
+
const coverage = [];
|
|
55
|
+
let clampedAny = false;
|
|
56
|
+
|
|
57
|
+
const hex = (path) => {
|
|
58
|
+
const value = map.get(path)?.value;
|
|
59
|
+
if (!value) return undefined;
|
|
60
|
+
const { text, clamped } = ctx.formatHex(value);
|
|
61
|
+
if (clamped) clampedAny = { path };
|
|
62
|
+
return text;
|
|
63
|
+
};
|
|
64
|
+
const prov = (path) => map.get(path)?.provenance.kind;
|
|
65
|
+
const cov = (variable, slot, cls, note, provSlot = slot) => {
|
|
66
|
+
// AL5 sweep: when the slot doesn't resolve, `hex()` returns undefined and
|
|
67
|
+
// JSON.stringify silently omits the key — the right behavior, since ECharts
|
|
68
|
+
// then uses its own default. But the row still claimed `native`, so the
|
|
69
|
+
// report asserted coverage for a property the theme file doesn't contain
|
|
70
|
+
// (`semantic.color.border` is authored in all four examples and absent from
|
|
71
|
+
// a minimal design system). Only single-path slots are checkable; the
|
|
72
|
+
// palette row's slot is a range label.
|
|
73
|
+
if (/^semantic\.[\w.-]+$/.test(slot) && map.get(slot)?.value === undefined) {
|
|
74
|
+
coverage.push({
|
|
75
|
+
variable,
|
|
76
|
+
slot: '—',
|
|
77
|
+
class: 'dropped',
|
|
78
|
+
note: `nothing to bind: this design system has no ${slot}, so the property is omitted and ECharts' own default applies`,
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const provKind = prov(provSlot);
|
|
83
|
+
const finalCls = cls === 'approximated' ? 'approximated' : (provKind === 'derived' ? 'derived' : cls);
|
|
84
|
+
coverage.push({ variable, slot, class: finalCls, provenance: provKind, ...(note && { note }) });
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Categorical palette — the reason this exporter exists
|
|
88
|
+
const palette = [];
|
|
89
|
+
for (let i = 1; i <= PALETTE_SIZE; i++) {
|
|
90
|
+
const c = hex(`${P}${i}`);
|
|
91
|
+
if (c) palette.push(c);
|
|
92
|
+
}
|
|
93
|
+
cov('color[]', `${P}1–${PALETTE_SIZE}`, 'native', undefined, `${P}1`);
|
|
94
|
+
|
|
95
|
+
const background = hex(`${S}elevation.0.surface`);
|
|
96
|
+
const text = hex(`${S}text.base`);
|
|
97
|
+
const textMuted = hex(`${S}text.muted`);
|
|
98
|
+
const border = hex(`${S}border`);
|
|
99
|
+
const overlay = hex(`${S}elevation.3.surface`);
|
|
100
|
+
cov('backgroundColor', `${S}elevation.0.surface`, 'native');
|
|
101
|
+
cov('textStyle.color', `${S}text.base`, 'native');
|
|
102
|
+
cov('title/legend/axisLabel', `${S}text.muted`, 'native');
|
|
103
|
+
cov('axisLine/splitLine/tooltip.borderColor', `${S}border`, 'native');
|
|
104
|
+
cov('tooltip.backgroundColor', `${S}elevation.3.surface`, 'native');
|
|
105
|
+
|
|
106
|
+
// fontFamily: ECharts wants a single CSS-style string
|
|
107
|
+
const fontEntry = map.get('semantic.font.sans');
|
|
108
|
+
const fontFamily = fontEntry ? fontEntry.value.join(', ') : undefined;
|
|
109
|
+
if (fontFamily) cov('textStyle.fontFamily', 'semantic.font.sans', 'native');
|
|
110
|
+
|
|
111
|
+
// radius: rem → px numeric (unit conversion = approximated)
|
|
112
|
+
const radiusEntry = map.get('semantic.radius.md');
|
|
113
|
+
const radiusPx = radiusEntry ? remToPx(radiusEntry.value) : undefined;
|
|
114
|
+
if (radiusPx !== undefined) {
|
|
115
|
+
cov('tooltip.borderRadius', 'semantic.radius.md', 'approximated', 'rem → px (base 16)');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (clampedAny) {
|
|
119
|
+
coverage.push({
|
|
120
|
+
variable: '(gamut)', slot: clampedAny.path, class: 'approximated',
|
|
121
|
+
note: 'sRGB gamut clamp during oklch → hex',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Honest unsupported: themable ECharts surfaces the IR does not cover yet
|
|
126
|
+
coverage.push({ variable: 'series-specific styles (candlestick, gauge, …)', slot: '—', class: 'unsupported', note: 'beyond catalog semantics; extend the emitted theme manually' });
|
|
127
|
+
|
|
128
|
+
const axisCommon = {
|
|
129
|
+
axisLine: { lineStyle: { color: border } },
|
|
130
|
+
axisTick: { lineStyle: { color: border } },
|
|
131
|
+
axisLabel: { color: textMuted },
|
|
132
|
+
splitLine: { lineStyle: { color: [border] } },
|
|
133
|
+
splitArea: { show: false },
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const theme = {
|
|
137
|
+
color: palette,
|
|
138
|
+
backgroundColor: background,
|
|
139
|
+
textStyle: { color: text, ...(fontFamily && { fontFamily }) },
|
|
140
|
+
title: {
|
|
141
|
+
textStyle: { color: text },
|
|
142
|
+
subtextStyle: { color: textMuted },
|
|
143
|
+
},
|
|
144
|
+
legend: { textStyle: { color: textMuted } },
|
|
145
|
+
tooltip: {
|
|
146
|
+
backgroundColor: overlay,
|
|
147
|
+
borderColor: border,
|
|
148
|
+
borderWidth: 1,
|
|
149
|
+
...(radiusPx !== undefined && { borderRadius: radiusPx }),
|
|
150
|
+
textStyle: { color: text },
|
|
151
|
+
},
|
|
152
|
+
toolbox: { iconStyle: { borderColor: textMuted } },
|
|
153
|
+
categoryAxis: axisCommon,
|
|
154
|
+
valueAxis: axisCommon,
|
|
155
|
+
logAxis: axisCommon,
|
|
156
|
+
timeAxis: axisCommon,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return { theme, modeCoverage: coverage };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function remToPx(value) {
|
|
163
|
+
const rem = /^([\d.]+)rem$/.exec(value);
|
|
164
|
+
if (rem) return Math.round(parseFloat(rem[1]) * 16);
|
|
165
|
+
const px = /^([\d.]+)px$/.exec(value);
|
|
166
|
+
if (px) return Math.round(parseFloat(px[1]));
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------- artifacts ----------
|
|
171
|
+
|
|
172
|
+
function umdWrapper(themeName, theme, ctx) {
|
|
173
|
+
return [
|
|
174
|
+
'/*',
|
|
175
|
+
` * GENERATED by transtyle — do not edit; source: ${ctx.projectName} token files`,
|
|
176
|
+
' * Registers the theme when loaded after echarts via <script>, or exports it (CJS/ESM interop via default).',
|
|
177
|
+
' */',
|
|
178
|
+
'(function (global, factory) {',
|
|
179
|
+
" if (typeof module === 'object' && module.exports) { module.exports = factory(); }",
|
|
180
|
+
' else if (global.echarts) { global.echarts.registerTheme(' + JSON.stringify(themeName) + ', factory()); }',
|
|
181
|
+
"}(typeof self !== 'undefined' ? self : this, function () {",
|
|
182
|
+
' return ' + JSON.stringify(theme, null, 2).replace(/\n/g, '\n ') + ';',
|
|
183
|
+
'}));',
|
|
184
|
+
'',
|
|
185
|
+
].join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function renderUsage(ctx, themeNames, coverage) {
|
|
189
|
+
const counts = {};
|
|
190
|
+
for (const c of coverage) counts[c.class] = (counts[c.class] ?? 0) + 1;
|
|
191
|
+
const summary = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(' · ');
|
|
192
|
+
const [lightName, darkName] = themeNames;
|
|
193
|
+
return `# Using this ECharts theme
|
|
194
|
+
|
|
195
|
+
Generated from the **${ctx.projectName}** design system by transtyle. Coverage: ${summary}.
|
|
196
|
+
One theme per color-scheme mode — theme selection at \`init\` is ECharts' native pattern.
|
|
197
|
+
|
|
198
|
+
## ES modules / bundlers
|
|
199
|
+
|
|
200
|
+
\`\`\`js
|
|
201
|
+
import * as echarts from 'echarts';
|
|
202
|
+
import light from './theme.${lightName}.json' with { type: 'json' };
|
|
203
|
+
${darkName ? `import dark from './theme.${darkName}.json' with { type: 'json' };` : ''}
|
|
204
|
+
|
|
205
|
+
echarts.registerTheme('${lightName}', light);
|
|
206
|
+
${darkName ? `echarts.registerTheme('${darkName}', dark);` : ''}
|
|
207
|
+
const chart = echarts.init(el, '${lightName}');
|
|
208
|
+
\`\`\`
|
|
209
|
+
|
|
210
|
+
## Script tags
|
|
211
|
+
|
|
212
|
+
\`\`\`html
|
|
213
|
+
<script src="https://cdn.jsdelivr.net/npm/echarts@5"></script>
|
|
214
|
+
<script src="./theme.${lightName}.js"></script> <!-- registers itself -->
|
|
215
|
+
<script>const chart = echarts.init(el, '${lightName}');</script>
|
|
216
|
+
\`\`\`
|
|
217
|
+
|
|
218
|
+
## Switching modes
|
|
219
|
+
|
|
220
|
+
ECharts themes are fixed at init. To follow a light/dark toggle: \`chart.dispose()\` and re-\`init\` with the other theme name (preserve state via \`chart.getOption()\` if needed).
|
|
221
|
+
|
|
222
|
+
## Extending
|
|
223
|
+
|
|
224
|
+
Series-type-specific styling (candlestick colors, gauge bands, …) is beyond design-token semantics and reported as \`unsupported\` in \`report.json\`. Extend by merging your own options on top of the theme at \`init\` — never edit these files; regenerate them with \`transtyle build echarts\`.
|
|
225
|
+
`;
|
|
226
|
+
}
|