@xeplr/ui-charts 1.0.1
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/README.md +99 -0
- package/index.js +38 -0
- package/lib/Chart.js +32 -0
- package/lib/XeplrChart.js +66 -0
- package/lib/build-option.js +111 -0
- package/lib/chart-options.js +353 -0
- package/lib/container-style.js +33 -0
- package/lib/deep-merge.js +26 -0
- package/lib/register.js +31 -0
- package/lib/use-echarts.js +71 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @xeplr/ui-charts
|
|
2
|
+
|
|
3
|
+
Direct **Apache ECharts** in React (no wrapper), driven by an **organized
|
|
4
|
+
ECharts-mirror spec**. A premium optimizer plugs in at the option level.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
organized spec → buildOption → EChartsOption → [premium.optimize] → useECharts render
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
Primary component — pass the semantic `ChartOptions` object:
|
|
13
|
+
|
|
14
|
+
```jsx
|
|
15
|
+
import { XeplrChart } from '@xeplr/ui-charts';
|
|
16
|
+
|
|
17
|
+
<XeplrChart
|
|
18
|
+
chartOptions={{
|
|
19
|
+
data: [{ month: 'Jan', revenue: 18700 }, /* … */],
|
|
20
|
+
axes: {
|
|
21
|
+
x: { labels: ['month'], title: { show: true, text: 'Monthly Revenue' } },
|
|
22
|
+
y: { labels: ['revenue'] }
|
|
23
|
+
},
|
|
24
|
+
chartType: 'bar'
|
|
25
|
+
}}
|
|
26
|
+
height={360}
|
|
27
|
+
theme="xeplr-dark" // brand-neutral default; or "xeplr-light"
|
|
28
|
+
onWarnings={(w) => console.warn(w)} // optional; unmapped props reported here
|
|
29
|
+
/>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`XeplrChart` runs `chartOptionsToOption()` internally. Lower-level, the
|
|
33
|
+
ECharts-mirror `<Chart spec={…} />` (backed by `buildOption`) is also exported for
|
|
34
|
+
callers who'd rather author ECharts-shaped specs directly.
|
|
35
|
+
|
|
36
|
+
## The spec (organized ECharts mirror)
|
|
37
|
+
|
|
38
|
+
It **is** an ECharts `option`, just organized with sane defaults and two
|
|
39
|
+
conveniences. Anything ECharts accepts flows straight through — that's the escape
|
|
40
|
+
hatch.
|
|
41
|
+
|
|
42
|
+
- **`data: rows`** → `dataset: { source: rows }`.
|
|
43
|
+
- **`field`-binding** → ECharts `encode`:
|
|
44
|
+
- `xAxis:{field:'month'}` + `series:[{type:'bar', field:'sales'}]` → `encode:{x:'month',y:'sales'}`
|
|
45
|
+
- `yAxis:{field}` → horizontal; pie `series:{type:'pie', categoryField, field}` → `encode:{itemName,value}`
|
|
46
|
+
- **Defaults** (spec always wins): grid + `tooltip.trigger:'axis'` for cartesian /
|
|
47
|
+
`'item'` otherwise, legend when multi-series, animation on.
|
|
48
|
+
- **Meta keys** `renderer` / `theme` are consumed by the renderer, stripped from the option.
|
|
49
|
+
|
|
50
|
+
`buildOption(spec)` is a pure function — the contract both the renderer and premium
|
|
51
|
+
optimizers operate on.
|
|
52
|
+
|
|
53
|
+
## Premium seam
|
|
54
|
+
|
|
55
|
+
`<Chart optimize={fn} />` (or `useECharts(spec, { optimize })`) — `optimize` is a
|
|
56
|
+
pure `(EChartsOption) => EChartsOption` applied right before `setOption`. Premium
|
|
57
|
+
algorithms live in the suite, import nothing from here beyond the option shape, and
|
|
58
|
+
optimize **any** chart (spec-built or raw).
|
|
59
|
+
|
|
60
|
+
## Theme
|
|
61
|
+
|
|
62
|
+
This is an **upstream, brand-neutral** package — it ships **no** consumer branding.
|
|
63
|
+
The defaults `xeplr-dark` / `xeplr-light` use the dataviz method's validated
|
|
64
|
+
*reference* palette (blue-led): dark worst-adjacent CVD ΔE 10.3 (floor band), light
|
|
65
|
+
ΔE 24.2. Mark specs (2px lines, ≥8px markers, 4px rounded bars, 2px pie gaps,
|
|
66
|
+
recessive axes) are baked in.
|
|
67
|
+
|
|
68
|
+
**Consumers supply their own brand theme downstream.** The theme *structure* lives
|
|
69
|
+
here in `makeTheme(palette)`; the brand *values* live in the app:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
import { echarts, makeTheme } from '@xeplr/ui-charts';
|
|
73
|
+
echarts.registerTheme('mybrand-dark', makeTheme({
|
|
74
|
+
color: ['#c98500', '#3987e5', /* …validated brand hues… */],
|
|
75
|
+
surface: '#1a1a19', primary: '#fff', secondary: '#c3c2b7',
|
|
76
|
+
muted: '#898781', gridline: '#2c2c2a', baseline: '#383835'
|
|
77
|
+
}));
|
|
78
|
+
// <Chart theme="mybrand-dark" ... />
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Bundle / registration
|
|
82
|
+
|
|
83
|
+
`register.js` registers a default module set (bar/line/pie/scatter + grid/tooltip/
|
|
84
|
+
legend/title/dataset/toolbox/dataZoom + canvas & svg). Need more? Register on the
|
|
85
|
+
same instance:
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
import { echarts } from '@xeplr/ui-charts';
|
|
89
|
+
import { RadarChart } from 'echarts/charts';
|
|
90
|
+
echarts.use([RadarChart]);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## API
|
|
94
|
+
|
|
95
|
+
`buildOption(spec)` · `<Chart>` · `useECharts(spec, opts)` · `echarts` (configured) ·
|
|
96
|
+
`themes` (`makeTheme`, `xeplrDark`, `xeplrLight`, `registerThemes`, `palettes`).
|
|
97
|
+
|
|
98
|
+
`react` and `echarts` are peer deps. `buildOption` + `themes` are pure (no peers) —
|
|
99
|
+
that's what the test suite (`npm test`, 21 tests) covers. Rendering you confirm in-app.
|
package/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @xeplr/ui-charts — organized ECharts spec → option + React <Chart>, using
|
|
2
|
+
// Apache ECharts DIRECTLY (no wrapper), with a premium optimize() seam.
|
|
3
|
+
//
|
|
4
|
+
// const { Chart, buildOption } = require('@xeplr/ui-charts');
|
|
5
|
+
//
|
|
6
|
+
// <Chart spec={{
|
|
7
|
+
// data: rows,
|
|
8
|
+
// xAxis: { field: 'month' }, yAxis: { name: 'Sales' },
|
|
9
|
+
// series: [{ type: 'bar', field: 'sales', name: 'Sales' }]
|
|
10
|
+
// }} height={360} />
|
|
11
|
+
//
|
|
12
|
+
// Layers:
|
|
13
|
+
// buildOption(spec) → EChartsOption (pure; the contract premium optimizes)
|
|
14
|
+
// useECharts / <Chart> (React lifecycle bridge)
|
|
15
|
+
// echarts (configured core — register more modules on it)
|
|
16
|
+
//
|
|
17
|
+
// Styling is theme-agnostic here: the resolved theme (@xeplr/ui-utils) arrives as
|
|
18
|
+
// a ChartOptions object and is translated by chartOptionsToOption. No themes here.
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
// Primary component — pass the semantic ChartOptions object (type.js):
|
|
22
|
+
// <XeplrChart chartOptions={chartOptions} />
|
|
23
|
+
XeplrChart: require('./lib/XeplrChart'),
|
|
24
|
+
|
|
25
|
+
// Semantic ChartOptions → ECharts option. Returns
|
|
26
|
+
// { option, seriesLabel, chartType, warnings }.
|
|
27
|
+
chartOptionsToOption: require('./lib/chart-options'),
|
|
28
|
+
|
|
29
|
+
// Lower-level: organized ECharts-mirror spec path.
|
|
30
|
+
Chart: require('./lib/Chart'),
|
|
31
|
+
buildOption: require('./lib/build-option'),
|
|
32
|
+
|
|
33
|
+
useECharts: require('./lib/use-echarts'),
|
|
34
|
+
echarts: require('./lib/register')
|
|
35
|
+
// Styling is not a ui-charts concern anymore — the resolved theme arrives as a
|
|
36
|
+
// ChartOptions object (from @xeplr/ui-utils resolveTheme) and is translated by
|
|
37
|
+
// chartOptionsToOption. No registered ECharts themes / makeTheme here.
|
|
38
|
+
};
|
package/lib/Chart.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// <Chart /> — declarative wrapper over useECharts for the organized
|
|
2
|
+
// ECharts-mirror spec (see buildOption). For the semantic ChartOptions object,
|
|
3
|
+
// use <XeplrChart>. Plain React.createElement (no JSX) → no build step.
|
|
4
|
+
//
|
|
5
|
+
// const { Chart } = require('@xeplr/ui-charts');
|
|
6
|
+
// <Chart spec={spec} height={360} optimize={premium.optimize} />
|
|
7
|
+
|
|
8
|
+
var React = require('react');
|
|
9
|
+
var useECharts = require('./use-echarts');
|
|
10
|
+
var buildOption = require('./build-option');
|
|
11
|
+
|
|
12
|
+
function Chart(props) {
|
|
13
|
+
props = props || {};
|
|
14
|
+
var option = React.useMemo(function () { return buildOption(props.spec); }, [props.spec]);
|
|
15
|
+
|
|
16
|
+
var api = useECharts(option, {
|
|
17
|
+
theme: props.theme,
|
|
18
|
+
renderer: props.renderer,
|
|
19
|
+
optimize: props.optimize,
|
|
20
|
+
onEvents: props.onEvents,
|
|
21
|
+
notMerge: props.notMerge
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
var style = Object.assign(
|
|
25
|
+
{ width: '100%', height: props.height != null ? props.height : 320 },
|
|
26
|
+
props.style
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return React.createElement('div', { ref: api.containerRef, className: props.className, style: style });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = Chart;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// <XeplrChart /> — the primary React component. Takes the semantic ChartOptions
|
|
2
|
+
// object (see type.js), translates it to an ECharts option via
|
|
3
|
+
// chartOptionsToOption, and renders it. Plain React.createElement (no JSX).
|
|
4
|
+
//
|
|
5
|
+
// const { XeplrChart } = require('@xeplr/ui-charts');
|
|
6
|
+
// <XeplrChart chartOptions={chartOptions} height={360} />
|
|
7
|
+
//
|
|
8
|
+
// Container frame — width / height / top / left — is read from `chartOptions`
|
|
9
|
+
// (CSS values per the type; numbers → px). These size/position the container
|
|
10
|
+
// div, not the ECharts option, so the translator ignores them. `top`/`left`
|
|
11
|
+
// switch the container to absolute positioning. Matching React props override.
|
|
12
|
+
//
|
|
13
|
+
// Props:
|
|
14
|
+
// chartOptions the semantic ChartOptions object (required)
|
|
15
|
+
// height/width number|string — override chartOptions.height/width
|
|
16
|
+
// theme optional registered ECharts theme name. Normally unset —
|
|
17
|
+
// styling comes from chartOptions (built from the theme).
|
|
18
|
+
// renderer 'canvas' (default) | 'svg'
|
|
19
|
+
// optimize (EChartsOption) => EChartsOption — premium seam
|
|
20
|
+
// onWarnings (string[]) => void — receives the translator's warnings.
|
|
21
|
+
// If omitted, warnings are console.warn'd unless `silent`.
|
|
22
|
+
// silent suppress the default console.warn of warnings
|
|
23
|
+
// onEvents { click: fn, ... } ECharts events, bound at init
|
|
24
|
+
// notMerge default true (structural replace); false to merge/animate
|
|
25
|
+
// style, className passed to the container div
|
|
26
|
+
|
|
27
|
+
var React = require('react');
|
|
28
|
+
var useECharts = require('./use-echarts');
|
|
29
|
+
var chartOptionsToOption = require('./chart-options');
|
|
30
|
+
var containerStyle = require('./container-style');
|
|
31
|
+
|
|
32
|
+
function XeplrChart(props) {
|
|
33
|
+
props = props || {};
|
|
34
|
+
|
|
35
|
+
// Translate once per chartOptions change → { option, seriesLabel, chartType, warnings }.
|
|
36
|
+
var result = React.useMemo(function () {
|
|
37
|
+
return chartOptionsToOption(props.chartOptions || {});
|
|
38
|
+
}, [props.chartOptions]);
|
|
39
|
+
|
|
40
|
+
// Surface the translator's warnings (unmapped props) — never silently.
|
|
41
|
+
React.useEffect(function () {
|
|
42
|
+
var w = result.warnings;
|
|
43
|
+
if (!w || !w.length) return;
|
|
44
|
+
if (typeof props.onWarnings === 'function') { props.onWarnings(w); return; }
|
|
45
|
+
if (!props.silent && typeof console !== 'undefined' && console.warn) {
|
|
46
|
+
console.warn('[XeplrChart] ' + w.length + ' unmapped chart propert' + (w.length > 1 ? 'ies' : 'y') + ':\n ' + w.join('\n '));
|
|
47
|
+
}
|
|
48
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
49
|
+
}, [result]);
|
|
50
|
+
|
|
51
|
+
var api = useECharts(result.option, {
|
|
52
|
+
theme: props.theme,
|
|
53
|
+
renderer: props.renderer,
|
|
54
|
+
optimize: props.optimize,
|
|
55
|
+
onEvents: props.onEvents,
|
|
56
|
+
notMerge: props.notMerge
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return React.createElement('div', {
|
|
60
|
+
ref: api.containerRef,
|
|
61
|
+
className: props.className,
|
|
62
|
+
style: containerStyle(props)
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = XeplrChart;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// buildOption(spec) — the pure core. Turns the "organized ECharts mirror" spec
|
|
2
|
+
// into a real ECharts `option`. NO echarts/react imports → trivially testable.
|
|
3
|
+
//
|
|
4
|
+
// What it adds over hand-writing raw ECharts option:
|
|
5
|
+
// 1. `data: rows` → `dataset: { source: rows }`.
|
|
6
|
+
// 2. `field`-binding: `xAxis:{field:'month'}` + `series:[{type:'bar', field:'sales'}]`
|
|
7
|
+
// → ECharts `encode` (dataset dimension mapping), which is fiddly by hand.
|
|
8
|
+
// 3. Sensible structural defaults (grid, tooltip trigger by chart kind, legend
|
|
9
|
+
// when multi-series, animation) — applied only where the spec is silent.
|
|
10
|
+
//
|
|
11
|
+
// Everything else is passthrough: because the spec is ECharts-shaped, any
|
|
12
|
+
// property you set flows straight through. That's the built-in escape hatch.
|
|
13
|
+
//
|
|
14
|
+
// Theme/color are NOT applied here. This is the raw ECharts-mirror escape hatch:
|
|
15
|
+
// set colors/styling directly in the spec, or feed a themed option via the
|
|
16
|
+
// ChartOptions path (XeplrChart). `renderer`/`theme` meta keys are stripped.
|
|
17
|
+
|
|
18
|
+
var deepMerge = require('./deep-merge');
|
|
19
|
+
|
|
20
|
+
var META_KEYS = ['renderer', 'theme'];
|
|
21
|
+
|
|
22
|
+
function buildOption(spec) {
|
|
23
|
+
var s = cloneWithout(spec || {}, META_KEYS);
|
|
24
|
+
|
|
25
|
+
var hasCartesian = s.xAxis !== undefined || s.yAxis !== undefined;
|
|
26
|
+
|
|
27
|
+
// 1. rows → dataset
|
|
28
|
+
if (Array.isArray(s.data) && s.dataset === undefined) {
|
|
29
|
+
s.dataset = { source: s.data };
|
|
30
|
+
}
|
|
31
|
+
delete s.data;
|
|
32
|
+
|
|
33
|
+
// 2. axis field-binding + type defaults
|
|
34
|
+
var xField = null, yField = null;
|
|
35
|
+
if (s.xAxis !== undefined) { var rx = normalizeAxis(s.xAxis, 'category'); s.xAxis = rx.axis; xField = rx.field; }
|
|
36
|
+
if (s.yAxis !== undefined) { var ry = normalizeAxis(s.yAxis, 'value'); s.yAxis = ry.axis; yField = ry.field; }
|
|
37
|
+
|
|
38
|
+
// 3. series field-binding
|
|
39
|
+
var seriesCount = 0;
|
|
40
|
+
if (s.series !== undefined) {
|
|
41
|
+
var arr = toArray(s.series).map(function (ser) {
|
|
42
|
+
return normalizeSeries(ser, { xField: xField, yField: yField });
|
|
43
|
+
});
|
|
44
|
+
s.series = arr;
|
|
45
|
+
seriesCount = arr.length;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 4. structural defaults (spec wins over these)
|
|
49
|
+
var defaults = structuralDefaults(hasCartesian, seriesCount);
|
|
50
|
+
return deepMerge(defaults, s);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
function cloneWithout(obj, keys) {
|
|
56
|
+
var out = {};
|
|
57
|
+
for (var k in obj) if (keys.indexOf(k) === -1) out[k] = obj[k];
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toArray(v) { return Array.isArray(v) ? v.slice() : [v]; }
|
|
62
|
+
|
|
63
|
+
// Strip the convenience `field` off an axis, default its `type`. Returns the
|
|
64
|
+
// cleaned axis plus the field name (for series encode). Handles axis arrays.
|
|
65
|
+
function normalizeAxis(axis, defaultType) {
|
|
66
|
+
if (Array.isArray(axis)) {
|
|
67
|
+
var field = null;
|
|
68
|
+
var arr = axis.map(function (a) {
|
|
69
|
+
var r = normalizeAxis(a, defaultType);
|
|
70
|
+
if (field === null) field = r.field;
|
|
71
|
+
return r.axis;
|
|
72
|
+
});
|
|
73
|
+
return { axis: arr, field: field };
|
|
74
|
+
}
|
|
75
|
+
var a = {};
|
|
76
|
+
for (var k in axis) if (k !== 'field') a[k] = axis[k];
|
|
77
|
+
if (a.type === undefined) a.type = defaultType;
|
|
78
|
+
return { axis: a, field: (axis && axis.field != null) ? axis.field : null };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Resolve `field` (value dim) + `categoryField`/`nameField` into ECharts encode,
|
|
82
|
+
// unless the caller already supplied an explicit `encode`. Strips the sugar.
|
|
83
|
+
function normalizeSeries(ser, ctx) {
|
|
84
|
+
var out = {};
|
|
85
|
+
for (var k in ser) if (k !== 'field' && k !== 'categoryField' && k !== 'nameField') out[k] = ser[k];
|
|
86
|
+
|
|
87
|
+
var field = ser.field;
|
|
88
|
+
var catField = ser.categoryField != null ? ser.categoryField : ser.nameField;
|
|
89
|
+
|
|
90
|
+
if (field != null && out.encode === undefined) {
|
|
91
|
+
if (ctx.xField != null) out.encode = { x: ctx.xField, y: field }; // vertical cartesian
|
|
92
|
+
else if (ctx.yField != null) out.encode = { y: ctx.yField, x: field }; // horizontal cartesian
|
|
93
|
+
else if (catField != null) out.encode = { itemName: catField, value: field }; // pie/funnel
|
|
94
|
+
else out.encode = { value: field };
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function structuralDefaults(hasCartesian, seriesCount) {
|
|
100
|
+
var d = { animation: true };
|
|
101
|
+
if (hasCartesian) {
|
|
102
|
+
d.grid = { left: '3%', right: '4%', bottom: '3%', top: 48, containLabel: true };
|
|
103
|
+
d.tooltip = { trigger: 'axis' };
|
|
104
|
+
} else {
|
|
105
|
+
d.tooltip = { trigger: 'item' };
|
|
106
|
+
}
|
|
107
|
+
if (seriesCount > 1) d.legend = {}; // multi-series → show legend (auto from names)
|
|
108
|
+
return d;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = buildOption;
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
// chartOptionsToOption — translate the semantic, CSS-like `ChartOptions` (see
|
|
2
|
+
// type.js) into an ECharts `option`. STYLING/CHROME ONLY for now: title, axes
|
|
3
|
+
// appearance, legend, tooltip, chart area, plus the data-label + chart-type
|
|
4
|
+
// carried out for the (future) series/data layer.
|
|
5
|
+
//
|
|
6
|
+
// const { option, seriesLabel, chartType, warnings } = chartOptionsToOption(chartOptions);
|
|
7
|
+
//
|
|
8
|
+
// It maps everything with an ECharts equivalent, parses CSS units → ECharts
|
|
9
|
+
// values, and collects `warnings` for properties ECharts can't express (rather
|
|
10
|
+
// than failing). Pure — no echarts/react imports.
|
|
11
|
+
|
|
12
|
+
function chartOptionsToOption(co, opts) {
|
|
13
|
+
co = co || {};
|
|
14
|
+
opts = opts || {};
|
|
15
|
+
var root = opts.rootFontSize || 16;
|
|
16
|
+
var warnings = [];
|
|
17
|
+
var ctx = { root: root, warn: function (m) { warnings.push(m); } };
|
|
18
|
+
|
|
19
|
+
var option = {};
|
|
20
|
+
|
|
21
|
+
if (co.title) {
|
|
22
|
+
var t = co.title;
|
|
23
|
+
if (t.show !== false) {
|
|
24
|
+
option.title = clean(assign(
|
|
25
|
+
{ text: t.text, show: true, textStyle: fontToTextStyle(t.font, ctx, 'title.font') },
|
|
26
|
+
positionKeys(resolvePosition(t.positioning, ctx)),
|
|
27
|
+
styleToEcharts(t.style, ctx, 'title.style'),
|
|
28
|
+
t.font && t.font.align ? { textAlign: t.font.align } : {}
|
|
29
|
+
));
|
|
30
|
+
} else {
|
|
31
|
+
option.title = { show: false };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (co.axes) {
|
|
36
|
+
if (co.axes.x) option.xAxis = axisToEcharts(co.axes.x, ctx, 'axes.x');
|
|
37
|
+
if (co.axes.y) option.yAxis = axisToEcharts(co.axes.y, ctx, 'axes.y');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (co.legend) {
|
|
41
|
+
var lg = co.legend;
|
|
42
|
+
if (lg.show !== false) {
|
|
43
|
+
var lgPos = resolvePosition(lg.positioning, ctx);
|
|
44
|
+
option.legend = clean(assign(
|
|
45
|
+
{
|
|
46
|
+
show: true,
|
|
47
|
+
orient: lgPos._vertical ? 'vertical' : 'horizontal',
|
|
48
|
+
textStyle: fontToTextStyle(lg.font, ctx, 'legend.font'),
|
|
49
|
+
itemGap: parseSize(lg.itemGap, ctx)
|
|
50
|
+
},
|
|
51
|
+
positionKeys(lgPos),
|
|
52
|
+
styleToEcharts(lg.style, ctx, 'legend.style')
|
|
53
|
+
));
|
|
54
|
+
if (lg.iconGap != null) ctx.warn('legend.iconGap has no ECharts equivalent (dropped)');
|
|
55
|
+
} else {
|
|
56
|
+
option.legend = { show: false };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (co.tooltip) {
|
|
61
|
+
var tt = co.tooltip;
|
|
62
|
+
option.tooltip = clean(assign(
|
|
63
|
+
{ show: tt.show !== false, textStyle: fontToTextStyle(tt.font, ctx, 'tooltip.font'), formatter: tt.formatter },
|
|
64
|
+
styleToEcharts(tt.style, ctx, 'tooltip.style')
|
|
65
|
+
));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (co.chartArea) {
|
|
69
|
+
var ca = co.chartArea;
|
|
70
|
+
var grid = {};
|
|
71
|
+
var edges = edgesFrom(ca.margin, ca.padding, ctx);
|
|
72
|
+
assign(grid, edges);
|
|
73
|
+
grid.containLabel = true;
|
|
74
|
+
option.grid = clean(grid);
|
|
75
|
+
if (ca.style && ca.style.background && ca.style.background.color) {
|
|
76
|
+
option.backgroundColor = applyOpacity(ca.style.background.color, ca.style.background.opacity);
|
|
77
|
+
}
|
|
78
|
+
if (ca.style && ca.style.background && ca.style.background.image) ctx.warn('chartArea.style.background.image has no ECharts equivalent (dropped)');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// dataLabel is per-series in ECharts — resolve it now, apply when series exist.
|
|
82
|
+
var seriesLabel;
|
|
83
|
+
if (co.dataLabel) {
|
|
84
|
+
var dl = co.dataLabel;
|
|
85
|
+
seriesLabel = clean(assign(
|
|
86
|
+
{
|
|
87
|
+
show: dl.show !== false,
|
|
88
|
+
position: labelPosition(dl.positioning),
|
|
89
|
+
formatter: dl.formatter
|
|
90
|
+
},
|
|
91
|
+
fontToTextStyle(dl.font, ctx, 'dataLabel.font'),
|
|
92
|
+
styleToEcharts(dl.style, ctx, 'dataLabel.style')
|
|
93
|
+
));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── data + series (field-binding via axes.x/y.labels) ──
|
|
97
|
+
// x.labels[0] = category dimension; each y.labels entry = one series (value dim).
|
|
98
|
+
if (Array.isArray(co.data)) option.dataset = { source: co.data };
|
|
99
|
+
|
|
100
|
+
var xLabels = co.axes && co.axes.x && co.axes.x.labels;
|
|
101
|
+
var yLabels = co.axes && co.axes.y && co.axes.y.labels;
|
|
102
|
+
var xField = Array.isArray(xLabels) ? xLabels[0] : undefined;
|
|
103
|
+
|
|
104
|
+
// per-type mark defaults (theme.marks[chartType]) — line width, bar radius +
|
|
105
|
+
// surface gap, symbol sizes, pie ring. Applied to every series of this type.
|
|
106
|
+
var marks = co.marks && co.chartType ? co.marks[co.chartType] : null;
|
|
107
|
+
|
|
108
|
+
if (co.chartType && Array.isArray(yLabels) && yLabels.length) {
|
|
109
|
+
option.series = yLabels.map(function (yf) {
|
|
110
|
+
var ser = clean({
|
|
111
|
+
type: co.chartType,
|
|
112
|
+
name: yf,
|
|
113
|
+
encode: xField ? { x: xField, y: yf } : { y: yf },
|
|
114
|
+
label: seriesLabel
|
|
115
|
+
});
|
|
116
|
+
if (marks) applyMarks(ser, co.chartType, marks, ctx);
|
|
117
|
+
return ser;
|
|
118
|
+
});
|
|
119
|
+
// cartesian roles: x carries the category, y the value
|
|
120
|
+
if (option.xAxis) { if (option.xAxis.type === undefined) option.xAxis.type = 'category'; }
|
|
121
|
+
else if (xField) option.xAxis = { type: 'category' };
|
|
122
|
+
if (option.yAxis) { if (option.yAxis.type === undefined) option.yAxis.type = 'value'; }
|
|
123
|
+
else option.yAxis = { type: 'value' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── theme-level extras (carried on ChartOptions by the theme) ──
|
|
127
|
+
// palette → the series color cycle; fontFamily → global text default (ECharts
|
|
128
|
+
// cascades option.textStyle.fontFamily to all text, so per-block family is
|
|
129
|
+
// unnecessary). These are the two fields beyond CSS-chrome that a theme needs.
|
|
130
|
+
if (Array.isArray(co.palette)) option.color = co.palette;
|
|
131
|
+
if (co.fontFamily) option.textStyle = assign(option.textStyle || {}, { fontFamily: co.fontFamily });
|
|
132
|
+
|
|
133
|
+
return { option: option, seriesLabel: seriesLabel, chartType: co.chartType, warnings: warnings };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// theme.marks[chartType] → ECharts series style. Each chart type reads the marks
|
|
137
|
+
// it understands; unknown keys are simply not consulted (no warnings — marks are
|
|
138
|
+
// a theme convenience, not user-authored CSS).
|
|
139
|
+
function applyMarks(ser, type, m, ctx) {
|
|
140
|
+
if (!m) return;
|
|
141
|
+
if (type === 'line') {
|
|
142
|
+
var ls = clean({ width: parseSize(m.width, ctx) });
|
|
143
|
+
if (ls) ser.lineStyle = assign(ser.lineStyle || {}, ls);
|
|
144
|
+
if (m.symbol != null) ser.symbol = m.symbol;
|
|
145
|
+
if (m.symbolSize != null) ser.symbolSize = m.symbolSize;
|
|
146
|
+
if (m.smooth != null) ser.smooth = m.smooth;
|
|
147
|
+
} else if (type === 'bar') {
|
|
148
|
+
var bit = {};
|
|
149
|
+
if (m.border && m.border.radius != null) bit.borderRadius = parseSize(m.border.radius, ctx);
|
|
150
|
+
if (m.gap) { // the 2px surface gap between fills → a same-color border ring
|
|
151
|
+
if (m.gap.color != null) bit.borderColor = m.gap.color;
|
|
152
|
+
if (m.gap.width != null) bit.borderWidth = parseSize(m.gap.width, ctx);
|
|
153
|
+
}
|
|
154
|
+
if (Object.keys(bit).length) ser.itemStyle = assign(ser.itemStyle || {}, bit);
|
|
155
|
+
} else if (type === 'scatter') {
|
|
156
|
+
if (m.symbolSize != null) ser.symbolSize = m.symbolSize;
|
|
157
|
+
} else if (type === 'pie') {
|
|
158
|
+
var pit = {};
|
|
159
|
+
if (m.border) {
|
|
160
|
+
if (m.border.color != null) pit.borderColor = m.border.color;
|
|
161
|
+
if (m.border.width != null) pit.borderWidth = parseSize(m.border.width, ctx);
|
|
162
|
+
}
|
|
163
|
+
if (Object.keys(pit).length) ser.itemStyle = assign(ser.itemStyle || {}, pit);
|
|
164
|
+
if (m.label && m.label.font && m.label.font.color != null) {
|
|
165
|
+
ser.label = assign(ser.label || {}, { color: m.label.font.color });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── sub-mappers ──────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
function axisToEcharts(ax, ctx, path) {
|
|
173
|
+
if (!ax) return undefined;
|
|
174
|
+
var a = { show: ax.show !== false };
|
|
175
|
+
|
|
176
|
+
if (ax.line) {
|
|
177
|
+
a.axisLine = clean({
|
|
178
|
+
show: ax.line.show !== false,
|
|
179
|
+
lineStyle: clean({ color: ax.line.color, width: parseSize(ax.line.width, ctx), type: lineType(ax.line.style) })
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (ax.grid) {
|
|
183
|
+
a.splitLine = clean({
|
|
184
|
+
show: !!ax.grid.show,
|
|
185
|
+
lineStyle: clean({ color: ax.grid.color, width: parseSize(ax.grid.width, ctx), type: lineType(ax.grid.style) })
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (ax.ticks) {
|
|
189
|
+
var tk = ax.ticks;
|
|
190
|
+
a.axisTick = clean({ show: tk.show !== false, length: parseSize(tk.length, ctx) });
|
|
191
|
+
a.axisLabel = clean(assign(
|
|
192
|
+
{ show: tk.show !== false, rotate: tk.angle, margin: parseSize(tk.margin, ctx), color: tk.color },
|
|
193
|
+
fontToTextStyle(tk.font, ctx, path + '.ticks.font')
|
|
194
|
+
));
|
|
195
|
+
}
|
|
196
|
+
if (ax.title) {
|
|
197
|
+
a.name = ax.title.text;
|
|
198
|
+
a.nameTextStyle = fontToTextStyle(ax.title.font, ctx, path + '.title.font');
|
|
199
|
+
if (ax.title.margin) a.nameGap = parseSize(ax.title.margin.top || ax.title.margin.bottom || ax.title.margin.left || ax.title.margin.right, ctx);
|
|
200
|
+
}
|
|
201
|
+
return clean(a);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function fontToTextStyle(font, ctx, path) {
|
|
205
|
+
if (!font) return undefined;
|
|
206
|
+
var ts = {
|
|
207
|
+
fontFamily: font.family,
|
|
208
|
+
fontSize: parseSize(font.size, ctx),
|
|
209
|
+
fontWeight: font.weight,
|
|
210
|
+
fontStyle: font.style, // normal|italic|oblique (ECharts supports these)
|
|
211
|
+
color: applyOpacity(font.color, font.opacity),
|
|
212
|
+
lineHeight: parseSize(font.lineHeight, ctx),
|
|
213
|
+
align: font.align
|
|
214
|
+
};
|
|
215
|
+
['letterSpacing', 'transform', 'decoration', 'variant'].forEach(function (k) {
|
|
216
|
+
if (font[k] != null) ctx.warn(path + '.' + k + ' has no ECharts equivalent (dropped)');
|
|
217
|
+
});
|
|
218
|
+
return clean(ts);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function styleToEcharts(style, ctx, path) {
|
|
222
|
+
if (!style) return undefined;
|
|
223
|
+
var out = {};
|
|
224
|
+
if (style.background) {
|
|
225
|
+
if (style.background.color) out.backgroundColor = applyOpacity(style.background.color, style.background.opacity);
|
|
226
|
+
if (style.background.image) ctx.warn(path + '.background.image has no ECharts equivalent (dropped)');
|
|
227
|
+
}
|
|
228
|
+
if (style.border) {
|
|
229
|
+
var b = style.border;
|
|
230
|
+
if (b.color != null) out.borderColor = b.color;
|
|
231
|
+
if (b.width != null) out.borderWidth = parseSize(b.width, ctx);
|
|
232
|
+
if (b.radius != null) out.borderRadius = parseSize(b.radius, ctx);
|
|
233
|
+
if (b.style != null) {
|
|
234
|
+
if (b.style === 'double') ctx.warn(path + '.border.style "double" has no ECharts equivalent (using solid)');
|
|
235
|
+
out.borderType = b.style === 'double' ? 'solid' : b.style;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (style.shadow) {
|
|
239
|
+
var sh = style.shadow;
|
|
240
|
+
if (sh.blur != null) out.shadowBlur = parseSize(sh.blur, ctx);
|
|
241
|
+
if (sh.color != null) out.shadowColor = sh.color;
|
|
242
|
+
if (sh.x != null) out.shadowOffsetX = parseSize(sh.x, ctx);
|
|
243
|
+
if (sh.y != null) out.shadowOffsetY = parseSize(sh.y, ctx);
|
|
244
|
+
if (sh.spread != null) ctx.warn(path + '.shadow.spread has no ECharts equivalent (dropped)');
|
|
245
|
+
}
|
|
246
|
+
if (style.opacity != null) ctx.warn(path + '.opacity on a container has no ECharts equivalent — use a color alpha (dropped)');
|
|
247
|
+
return clean(out);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// position keyword + margin edges → {left,top,right,bottom} (+ _vertical hint)
|
|
251
|
+
function resolvePosition(p, ctx) {
|
|
252
|
+
if (!p) return {};
|
|
253
|
+
var out = {};
|
|
254
|
+
switch (p.position) {
|
|
255
|
+
case 'top': out.top = 0; out.left = 'center'; break;
|
|
256
|
+
case 'bottom': out.bottom = 0; out.left = 'center'; break;
|
|
257
|
+
case 'left': out.left = 0; out.top = 'middle'; out._vertical = true; break;
|
|
258
|
+
case 'right': out.right = 0; out.top = 'middle'; out._vertical = true; break;
|
|
259
|
+
case 'center': out.left = 'center'; out.top = 'middle'; break;
|
|
260
|
+
}
|
|
261
|
+
if (p.margin) {
|
|
262
|
+
if (p.margin.top != null) out.top = parseSize(p.margin.top, ctx);
|
|
263
|
+
if (p.margin.right != null) out.right = parseSize(p.margin.right, ctx);
|
|
264
|
+
if (p.margin.bottom != null) out.bottom = parseSize(p.margin.bottom, ctx);
|
|
265
|
+
if (p.margin.left != null) out.left = parseSize(p.margin.left, ctx);
|
|
266
|
+
}
|
|
267
|
+
if (p.align) {
|
|
268
|
+
if (out._vertical) out.top = p.align === 'start' ? 'top' : p.align === 'end' ? 'bottom' : 'middle';
|
|
269
|
+
else out.left = p.align === 'start' ? 'left' : p.align === 'end' ? 'right' : 'center';
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function positionKeys(pos) {
|
|
275
|
+
var out = {};
|
|
276
|
+
['left', 'top', 'right', 'bottom'].forEach(function (k) { if (pos[k] !== undefined) out[k] = pos[k]; });
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function edgesFrom(margin, padding, ctx) {
|
|
281
|
+
var out = {};
|
|
282
|
+
['top', 'right', 'bottom', 'left'].forEach(function (e) {
|
|
283
|
+
var m = margin && margin[e] != null ? parseSize(margin[e], ctx) : undefined;
|
|
284
|
+
var p = padding && padding[e] != null ? parseSize(padding[e], ctx) : undefined;
|
|
285
|
+
var v = sumSizes(m, p);
|
|
286
|
+
if (v !== undefined) out[e] = v;
|
|
287
|
+
});
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function labelPosition(positioning) {
|
|
292
|
+
if (!positioning || !positioning.position) return undefined;
|
|
293
|
+
return positioning.position === 'center' ? 'inside' : positioning.position;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── primitives ───────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
function parseSize(v, ctx) {
|
|
299
|
+
if (v == null) return undefined;
|
|
300
|
+
if (typeof v === 'number') return v;
|
|
301
|
+
var s = String(v).trim();
|
|
302
|
+
if (s === '' || s === 'auto') return undefined;
|
|
303
|
+
if (s.charAt(s.length - 1) === '%') return s; // keep percentage string
|
|
304
|
+
var m = s.match(/^(-?[\d.]+)(px|rem|em|pt)?$/);
|
|
305
|
+
if (!m) return undefined;
|
|
306
|
+
var num = parseFloat(m[1]);
|
|
307
|
+
var unit = m[2] || 'px';
|
|
308
|
+
var base = (ctx && ctx.root) || 16;
|
|
309
|
+
if (unit === 'rem' || unit === 'em') return num * base;
|
|
310
|
+
if (unit === 'pt') return num * (96 / 72);
|
|
311
|
+
return num; // px / unitless
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function sumSizes(a, b) {
|
|
315
|
+
if (a === undefined) return b;
|
|
316
|
+
if (b === undefined) return a;
|
|
317
|
+
if (typeof a === 'number' && typeof b === 'number') return a + b;
|
|
318
|
+
return a; // can't add a % and a px cleanly — margin wins
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function applyOpacity(color, opacity) {
|
|
322
|
+
if (color == null) return undefined;
|
|
323
|
+
if (opacity == null) return color;
|
|
324
|
+
var m6 = /^#([0-9a-f]{6})$/i.exec(color);
|
|
325
|
+
var m3 = /^#([0-9a-f]{3})$/i.exec(color);
|
|
326
|
+
var r, g, b;
|
|
327
|
+
if (m6) { var n = parseInt(m6[1], 16); r = (n >> 16) & 255; g = (n >> 8) & 255; b = n & 255; }
|
|
328
|
+
else if (m3) { var c = m3[1]; r = parseInt(c[0] + c[0], 16); g = parseInt(c[1] + c[1], 16); b = parseInt(c[2] + c[2], 16); }
|
|
329
|
+
else return color;
|
|
330
|
+
return 'rgba(' + r + ',' + g + ',' + b + ',' + opacity + ')';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function lineType(style) {
|
|
334
|
+
if (style == null) return undefined;
|
|
335
|
+
return (style === 'solid' || style === 'dashed' || style === 'dotted') ? style : undefined;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function assign(target) {
|
|
339
|
+
for (var i = 1; i < arguments.length; i++) {
|
|
340
|
+
var src = arguments[i];
|
|
341
|
+
if (src) for (var k in src) if (src[k] !== undefined) target[k] = src[k];
|
|
342
|
+
}
|
|
343
|
+
return target;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function clean(obj) {
|
|
347
|
+
if (!obj) return undefined;
|
|
348
|
+
var out = {}, has = false;
|
|
349
|
+
for (var k in obj) { if (obj[k] !== undefined) { out[k] = obj[k]; has = true; } }
|
|
350
|
+
return has ? out : undefined;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
module.exports = chartOptionsToOption;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Resolve the chart container's CSS frame from chartOptions.{width,height,top,left},
|
|
2
|
+
// overridable by matching React props. These size/position the container div —
|
|
3
|
+
// they are NOT ECharts option properties. `top`/`left` switch to absolute
|
|
4
|
+
// positioning. Pure (no react) → unit-testable.
|
|
5
|
+
//
|
|
6
|
+
// containerStyle({ chartOptions, width?, height?, top?, left?, style? }) → CSSObject
|
|
7
|
+
|
|
8
|
+
function containerStyle(props) {
|
|
9
|
+
props = props || {};
|
|
10
|
+
var co = props.chartOptions || {};
|
|
11
|
+
|
|
12
|
+
var width = firstDefined(props.width, co.width, '100%');
|
|
13
|
+
var height = firstDefined(props.height, co.height, 320);
|
|
14
|
+
var top = firstDefined(props.top, co.top);
|
|
15
|
+
var left = firstDefined(props.left, co.left);
|
|
16
|
+
|
|
17
|
+
var style = { width: width, height: height };
|
|
18
|
+
if (top !== undefined || left !== undefined) {
|
|
19
|
+
style.position = 'absolute';
|
|
20
|
+
if (top !== undefined) style.top = top;
|
|
21
|
+
if (left !== undefined) style.left = left;
|
|
22
|
+
}
|
|
23
|
+
return Object.assign(style, props.style);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function firstDefined() {
|
|
27
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
28
|
+
if (arguments[i] !== undefined && arguments[i] !== null) return arguments[i];
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = containerStyle;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Deep-merge for plain objects only. Arrays and non-plain values are REPLACED
|
|
2
|
+
// (not merged) — so `series: [...]` from the spec overrides wholesale, while
|
|
3
|
+
// nested config objects (grid, tooltip, textStyle, …) merge key-by-key.
|
|
4
|
+
// Right side (override) wins.
|
|
5
|
+
|
|
6
|
+
function isPlainObject(v) {
|
|
7
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v) &&
|
|
8
|
+
(Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function deepMerge(base, override) {
|
|
12
|
+
if (!isPlainObject(base) || !isPlainObject(override)) {
|
|
13
|
+
return override === undefined ? base : override;
|
|
14
|
+
}
|
|
15
|
+
var out = {};
|
|
16
|
+
var k;
|
|
17
|
+
for (k in base) out[k] = base[k];
|
|
18
|
+
for (k in override) {
|
|
19
|
+
if (isPlainObject(out[k]) && isPlainObject(override[k])) out[k] = deepMerge(out[k], override[k]);
|
|
20
|
+
else out[k] = override[k];
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = deepMerge;
|
|
26
|
+
module.exports.isPlainObject = isPlainObject;
|
package/lib/register.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Configured ECharts core — à-la-carte registration keeps bundles tree-shaken
|
|
2
|
+
// (this is the whole point of using ECharts directly). We register a sensible
|
|
3
|
+
// DEFAULT set so the common charts work out of the box; consumers who need more
|
|
4
|
+
// import the extra modules and call `.use([...])` on this same instance.
|
|
5
|
+
//
|
|
6
|
+
// const { echarts } = require('@xeplr/ui-charts');
|
|
7
|
+
// import { RadarChart } from 'echarts/charts';
|
|
8
|
+
// echarts.use([RadarChart]); // extend when needed
|
|
9
|
+
|
|
10
|
+
var echarts = require('echarts/core');
|
|
11
|
+
|
|
12
|
+
var { BarChart, LineChart, PieChart, ScatterChart } = require('echarts/charts');
|
|
13
|
+
var {
|
|
14
|
+
GridComponent, TooltipComponent, LegendComponent, TitleComponent,
|
|
15
|
+
DatasetComponent, ToolboxComponent, DataZoomComponent, MarkLineComponent, MarkPointComponent
|
|
16
|
+
} = require('echarts/components');
|
|
17
|
+
var { CanvasRenderer, SVGRenderer } = require('echarts/renderers');
|
|
18
|
+
|
|
19
|
+
echarts.use([
|
|
20
|
+
BarChart, LineChart, PieChart, ScatterChart,
|
|
21
|
+
GridComponent, TooltipComponent, LegendComponent, TitleComponent,
|
|
22
|
+
DatasetComponent, ToolboxComponent, DataZoomComponent, MarkLineComponent, MarkPointComponent,
|
|
23
|
+
CanvasRenderer, SVGRenderer
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
// No theme registration: styling flows through the ECharts `option` built from
|
|
27
|
+
// the ChartOptions theme (see @xeplr/ui-utils default.theme.json → chartOptionsToOption),
|
|
28
|
+
// so there's a single source of styling. Register your own theme on this
|
|
29
|
+
// instance only if you specifically want ECharts' theme layer.
|
|
30
|
+
|
|
31
|
+
module.exports = echarts;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// useECharts — the imperative-lifecycle bridge. Takes an already-built ECharts
|
|
2
|
+
// `option`; the calling component is responsible for producing it (memoized).
|
|
3
|
+
// Init once, setOption on option change, ResizeObserver → resize, dispose on
|
|
4
|
+
// unmount. This is the ~30 lines written once so charts are declarative above.
|
|
5
|
+
//
|
|
6
|
+
// const option = useMemo(() => buildOption(spec), [spec]);
|
|
7
|
+
// const { containerRef } = useECharts(option, { theme, optimize });
|
|
8
|
+
// return <div ref={containerRef} style={{ height: 320 }} />;
|
|
9
|
+
//
|
|
10
|
+
// The premium seam is `optimize`: a pure (option) => option applied right before
|
|
11
|
+
// setOption — so premium algos optimize the FINAL option and work for any chart.
|
|
12
|
+
// theme + renderer are fixed at init (ECharts can't swap them live) — change them
|
|
13
|
+
// by remounting (e.g. a React `key`).
|
|
14
|
+
|
|
15
|
+
var React = require('react');
|
|
16
|
+
var echarts = require('./register');
|
|
17
|
+
|
|
18
|
+
function useECharts(option, options) {
|
|
19
|
+
options = options || {};
|
|
20
|
+
var containerRef = React.useRef(null);
|
|
21
|
+
var chartRef = React.useRef(null);
|
|
22
|
+
|
|
23
|
+
// No default registered ECharts theme: styling is carried entirely by the
|
|
24
|
+
// `option` (built from the ChartOptions theme). `theme` stays an optional
|
|
25
|
+
// escape hatch for anyone who registers + names their own ECharts theme.
|
|
26
|
+
var theme = options.theme;
|
|
27
|
+
var renderer = options.renderer || 'canvas';
|
|
28
|
+
|
|
29
|
+
React.useEffect(function () {
|
|
30
|
+
var el = containerRef.current;
|
|
31
|
+
if (!el) return undefined;
|
|
32
|
+
|
|
33
|
+
var chart = echarts.init(el, theme, { renderer: renderer });
|
|
34
|
+
chartRef.current = chart;
|
|
35
|
+
|
|
36
|
+
if (options.onEvents) {
|
|
37
|
+
Object.keys(options.onEvents).forEach(function (name) {
|
|
38
|
+
chart.on(name, options.onEvents[name]);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var ro = null;
|
|
43
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
44
|
+
ro = new ResizeObserver(function () { if (chartRef.current) chartRef.current.resize(); });
|
|
45
|
+
ro.observe(el);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return function () {
|
|
49
|
+
if (ro) ro.disconnect();
|
|
50
|
+
chart.dispose();
|
|
51
|
+
chartRef.current = null;
|
|
52
|
+
};
|
|
53
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
React.useEffect(function () {
|
|
57
|
+
var chart = chartRef.current;
|
|
58
|
+
if (!chart) return;
|
|
59
|
+
var opt = option || {};
|
|
60
|
+
if (typeof options.optimize === 'function') opt = options.optimize(opt);
|
|
61
|
+
chart.setOption(opt, { notMerge: options.notMerge !== false, lazyUpdate: true });
|
|
62
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
63
|
+
}, [option, options.optimize]);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
containerRef: containerRef,
|
|
67
|
+
getInstance: function () { return chartRef.current; }
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = useECharts;
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xeplr/ui-charts",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Organized ECharts spec → option builder + React <Chart> for direct Apache ECharts (no wrapper). Premium optimizers plug in at the option level.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"lib/"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test test/*.test.js"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"echarts": "^5.4.0",
|
|
15
|
+
"react": ">=17"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"echarts",
|
|
19
|
+
"charts",
|
|
20
|
+
"react",
|
|
21
|
+
"dataviz",
|
|
22
|
+
"visualization",
|
|
23
|
+
"xeplr"
|
|
24
|
+
],
|
|
25
|
+
"author": "xeplr",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/Xeplr/xeplr-ui-charts"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"echarts": "^6.1.0"
|
|
36
|
+
}
|
|
37
|
+
}
|