@xeplr/ui-charts 1.0.1 → 1.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Xeplr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -33,6 +33,51 @@ import { XeplrChart } from '@xeplr/ui-charts';
33
33
  ECharts-mirror `<Chart spec={…} />` (backed by `buildOption`) is also exported for
34
34
  callers who'd rather author ECharts-shaped specs directly.
35
35
 
36
+ ### `chartType`
37
+
38
+ An **ECharts series type** — `bar`, `line`, `pie`, `scatter`, or any other,
39
+ including one you registered yourself. ECharts is the vocabulary; anything it
40
+ accepts flows through untouched.
41
+
42
+ Three aliases cover the charts every UI offers that ECharts has no series type
43
+ for. Each resolves to a real type *before* anything reaches ECharts, so an
44
+ unrenderable series is never handed over:
45
+
46
+ | you say | ECharts gets |
47
+ |---|---|
48
+ | `area` | `line` + `areaStyle` |
49
+ | `donut` | `pie` + `radius: ['45%','70%']` |
50
+ | `hbar` | `bar` with the axis roles swapped — category on y, value on x |
51
+
52
+ Saying it in ECharts' own terms is the same chart: `chartType:'line'` with a
53
+ series `areaStyle` and `chartType:'area'` produce identical output.
54
+
55
+ A value axis gets `boundaryGap: [0, '10%']` by default — headroom, so the
56
+ tallest point doesn't sit on the top gridline and an area chart isn't clipped
57
+ flat against it. Nothing is added below, since a baseline belongs at zero. It
58
+ applies to whichever axis carries the value, so `hbar` gets it on x. Set
59
+ `axes.y.boundaryGap` (ECharts' own property, passed through verbatim) to
60
+ override.
61
+
62
+ Binding follows the type, and three types don't bind through `encode` at all —
63
+ handing them one draws nothing, silently:
64
+
65
+ | binding | types | shape |
66
+ |---|---|---|
67
+ | cartesian | bar, line, scatter, area… | `encode:{x,y}` + a pair of axes |
68
+ | name/value | pie, donut, funnel, sunburst | `encode:{itemName,value}`, **no axes** |
69
+ | radar | radar | `radar.indicator` from the categories; each measure is one shape over them, sharing one max |
70
+ | treemap | treemap | a flat `series.data` of `{name,value}` — treemap has no dataset support |
71
+ | heatmap | heatmap | **two** category axes + `[x,y,value]` cells + a `visualMap` |
72
+
73
+ A heatmap crosses two groupings, so it reads a second dimension from
74
+ `axes.x.labels[1]` — `x.labels` has always been the dimension list and other
75
+ charts simply use the first. Given only one it warns rather than drawing a
76
+ single stripe that would look like it worked.
77
+
78
+ Non-cartesian types are given **no axes at all**, including when the theme
79
+ carries axis styling, which would otherwise draw a bare cross behind the slices.
80
+
36
81
  ## The spec (organized ECharts mirror)
37
82
 
38
83
  It **is** an ECharts `option`, just organized with sane defaults and two
@@ -9,6 +9,8 @@
9
9
  // values, and collects `warnings` for properties ECharts can't express (rather
10
10
  // than failing). Pure — no echarts/react imports.
11
11
 
12
+ var applyRules = require('./rules');
13
+
12
14
  function chartOptionsToOption(co, opts) {
13
15
  co = co || {};
14
16
  opts = opts || {};
@@ -70,7 +72,21 @@ function chartOptionsToOption(co, opts) {
70
72
  var grid = {};
71
73
  var edges = edgesFrom(ca.margin, ca.padding, ctx);
72
74
  assign(grid, edges);
73
- grid.containLabel = true;
75
+ // CONTAIN WHAT LABELS?
76
+ //
77
+ // containLabel exists to stop axis labels being clipped: ECharts shrinks
78
+ // the plot inward until the text fits. With both axes hidden there is no
79
+ // text, so it reserves room for nothing — and a chart asked for zero
80
+ // margin on every edge still had visible space around it.
81
+ //
82
+ // That is exactly the minimalist case: no axes, no legend, no title, and
83
+ // an explicit margin of 0, which then did not look like 0. Derived rather
84
+ // than exposed as a setting, because "reserve space for the labels you are
85
+ // drawing" is not a preference — it is a description of whether any are
86
+ // being drawn.
87
+ var xShown = !(co.axes && co.axes.x && co.axes.x.show === false);
88
+ var yShown = !(co.axes && co.axes.y && co.axes.y.show === false);
89
+ grid.containLabel = xShown || yShown;
74
90
  option.grid = clean(grid);
75
91
  if (ca.style && ca.style.background && ca.style.background.color) {
76
92
  option.backgroundColor = applyOpacity(ca.style.background.color, ca.style.background.opacity);
@@ -80,17 +96,60 @@ function chartOptionsToOption(co, opts) {
80
96
 
81
97
  // dataLabel is per-series in ECharts — resolve it now, apply when series exist.
82
98
  var seriesLabel;
99
+ var seriesLabelLayout;
100
+ var seriesLabelLine;
83
101
  if (co.dataLabel) {
84
102
  var dl = co.dataLabel;
85
103
  seriesLabel = clean(assign(
86
104
  {
87
105
  show: dl.show !== false,
88
106
  position: labelPosition(dl.positioning),
89
- formatter: dl.formatter
107
+ formatter: dl.formatter,
108
+ // The gap between a label and the mark it belongs to.
109
+ distance: parseSize(dl.distance, ctx),
110
+ rotate: dl.rotate,
111
+ // A nudge for ALL of them. Per-label nudging is labelLayout's job.
112
+ offset: (dl.offsetX != null || dl.offsetY != null)
113
+ ? [parseSize(dl.offsetX, ctx) || 0, parseSize(dl.offsetY, ctx) || 0]
114
+ : undefined,
115
+ minMargin: parseSize(dl.minMargin, ctx),
116
+ width: parseSize(dl.maxWidth, ctx),
117
+ overflow: dl.overflow
90
118
  },
91
119
  fontToTextStyle(dl.font, ctx, 'dataLabel.font'),
92
120
  styleToEcharts(dl.style, ctx, 'dataLabel.style')
93
121
  ));
122
+
123
+ // THE CALLOUT — the leader line out to the label. Native to pie, donut and
124
+ // funnel; other types ignore it, so it is emitted whenever asked for
125
+ // rather than gated on a type the caller may know better than we do.
126
+ if (dl.labelLine) {
127
+ var ll = dl.labelLine;
128
+ seriesLabelLine = clean({
129
+ show: ll.show !== false,
130
+ length: parseSize(ll.length, ctx),
131
+ length2: parseSize(ll.length2, ctx),
132
+ smooth: ll.smooth,
133
+ minTurnAngle: ll.minTurnAngle,
134
+ lineStyle: clean({
135
+ color: ll.lineStyle && ll.lineStyle.color,
136
+ width: parseSize(ll.lineStyle && ll.lineStyle.width, ctx),
137
+ type: lineType(ll.lineStyle && ll.lineStyle.style)
138
+ })
139
+ });
140
+ }
141
+
142
+ // COLLISION. The usual reason a chart's labels are unreadable is that
143
+ // they overprint each other, and ECharts can resolve it for the whole
144
+ // series — worth reaching for before anybody starts nudging labels one at
145
+ // a time.
146
+ var layout = dl.layout || {};
147
+ if (layout.hideOverlap != null || layout.moveOverlap) {
148
+ seriesLabelLayout = clean({
149
+ hideOverlap: layout.hideOverlap,
150
+ moveOverlap: layout.moveOverlap
151
+ });
152
+ }
94
153
  }
95
154
 
96
155
  // ── data + series (field-binding via axes.x/y.labels) ──
@@ -99,30 +158,228 @@ function chartOptionsToOption(co, opts) {
99
158
 
100
159
  var xLabels = co.axes && co.axes.x && co.axes.x.labels;
101
160
  var yLabels = co.axes && co.axes.y && co.axes.y.labels;
102
- var xField = Array.isArray(xLabels) ? xLabels[0] : undefined;
161
+ var xField = Array.isArray(xLabels) ? labelField(xLabels[0]) : undefined;
162
+
163
+ // What ECharts is actually being asked for. `chartType` is an ECharts series
164
+ // type — the aliases below resolve to one, so nothing ECharts cannot read
165
+ // ever reaches it.
166
+ var shape = resolveChartType(co.chartType);
103
167
 
104
168
  // per-type mark defaults (theme.marks[chartType]) — line width, bar radius +
105
169
  // 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
170
+ // Looked up under the NAME that was asked for first, so a theme can style
171
+ // `donut` apart from `pie`, then under the type it resolves to.
172
+ var marks = co.marks && co.chartType
173
+ ? (co.marks[co.chartType] || co.marks[shape.type])
174
+ : null;
175
+
176
+ var rows = Array.isArray(co.data) ? co.data : [];
177
+
178
+ if (shape.type && Array.isArray(yLabels) && yLabels.length) {
179
+ if (shape.binding === 'radar') {
180
+ // A radar has its own coordinate system. The CATEGORIES become the axes
181
+ // (indicators) and each measure becomes one shape laid over them — which
182
+ // is the transpose of every other chart here, and why it cannot go
183
+ // through encode.
184
+ var indicators = categoriesOf(rows, xField).map(function (c) { return { name: String(c) }; });
185
+ // One shared max, so two measures on the same radar can be compared.
186
+ // Per-axis maxima would rescale every spoke independently and make the
187
+ // shape meaningless.
188
+ var peak = 0;
189
+ yLabels.forEach(function (l) {
190
+ rows.forEach(function (r) {
191
+ var v = Number(r[labelField(l)]);
192
+ if (isFinite(v) && v > peak) peak = v;
193
+ });
115
194
  });
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' };
195
+ if (peak > 0) indicators.forEach(function (ind) { ind.max = peak; });
196
+ option.radar = clean({ indicator: indicators });
197
+ option.series = [clean({
198
+ type: 'radar',
199
+ data: yLabels.map(function (label) {
200
+ return {
201
+ name: labelName(label),
202
+ value: rows.map(function (r) { return r[labelField(label)]; })
203
+ };
204
+ }),
205
+ label: seriesLabel,
206
+ labelLayout: seriesLabelLayout,
207
+ labelLine: seriesLabelLine
208
+ })];
209
+ delete option.xAxis;
210
+ delete option.yAxis;
211
+
212
+ } else if (shape.binding === 'treemap') {
213
+ // Treemap has no dataset support at all — it reads series.data, and each
214
+ // node is {name, value}. One measure: a rectangle has one area.
215
+ var tf = labelField(yLabels[0]);
216
+ option.series = [clean({
217
+ type: 'treemap',
218
+ data: rows.map(function (r) {
219
+ return { name: String(r[xField]), value: Number(r[tf]) };
220
+ }).filter(function (d) { return isFinite(d.value); }),
221
+ label: seriesLabel,
222
+ labelLayout: seriesLabelLayout,
223
+ labelLine: seriesLabelLine,
224
+ breadcrumb: { show: false }
225
+ })];
226
+ delete option.xAxis;
227
+ delete option.yAxis;
228
+
229
+ } else if (shape.binding === 'heatmap') {
230
+ // TWO categorical axes and a value. The second dimension comes from
231
+ // x.labels[1] — x.labels has always been the dimension list, and every
232
+ // other chart simply uses the first of them, so a heatmap needs no new
233
+ // field to say what it needs.
234
+ var xf2 = Array.isArray(xLabels) && xLabels.length > 1 ? labelField(xLabels[1]) : null;
235
+ var vf = labelField(yLabels[0]);
236
+ if (!xf2) {
237
+ // Said rather than drawn empty. A heatmap is a grid; with one
238
+ // dimension there is no grid, and rendering a single stripe would look
239
+ // like a chart that worked.
240
+ ctx.warn('a heatmap needs two dimensions — give axes.x.labels a second column');
241
+ option.series = [];
242
+ } else {
243
+ var xCats = categoriesOf(rows, xField);
244
+ var yCats = categoriesOf(rows, xf2);
245
+ var xAt = {}; xCats.forEach(function (c, i) { xAt[String(c)] = i; });
246
+ var yAt = {}; yCats.forEach(function (c, i) { yAt[String(c)] = i; });
247
+ var cells = [];
248
+ var lo = Infinity;
249
+ var hi = -Infinity;
250
+ rows.forEach(function (r) {
251
+ var v = Number(r[vf]);
252
+ if (!isFinite(v)) return;
253
+ if (v < lo) lo = v;
254
+ if (v > hi) hi = v;
255
+ cells.push([xAt[String(r[xField])], yAt[String(r[xf2])], v]);
256
+ });
257
+ option.xAxis = assign(option.xAxis || {}, { type: 'category', data: xCats, splitArea: { show: true } });
258
+ option.yAxis = assign(option.yAxis || {}, { type: 'category', data: yCats, splitArea: { show: true } });
259
+ // A value axis's headroom is meaningless here — both axes are
260
+ // categorical — and would push the grid off by a phantom cell.
261
+ delete option.xAxis.boundaryGap;
262
+ delete option.yAxis.boundaryGap;
263
+ // Without a visualMap every cell renders the same shade, which is a
264
+ // heatmap that carries no information.
265
+ option.visualMap = {
266
+ min: isFinite(lo) ? lo : 0,
267
+ max: isFinite(hi) ? hi : 1,
268
+ calculable: true,
269
+ orient: 'horizontal',
270
+ left: 'center',
271
+ bottom: 0,
272
+ inRange: Array.isArray(co.palette) && co.palette.length > 1
273
+ ? { color: [co.palette[co.palette.length - 1], co.palette[0]] }
274
+ : undefined
275
+ };
276
+ option.series = [clean({ type: 'heatmap', data: cells, label: seriesLabel, labelLayout: seriesLabelLayout })];
277
+ }
278
+
279
+ } else {
280
+ option.series = yLabels.map(function (label) {
281
+ var yf = labelField(label);
282
+ var ser = clean({
283
+ type: shape.type,
284
+ name: labelName(label),
285
+ // Cartesian series bind to axes; the rest name a slice and its size.
286
+ // A pie given {x, y} silently renders nothing — it is looking for
287
+ // itemName and value and finds neither.
288
+ encode: shape.cartesian
289
+ ? (shape.horizontal
290
+ ? (xField ? { y: xField, x: yf } : { x: yf })
291
+ : (xField ? { x: xField, y: yf } : { y: yf }))
292
+ : (xField ? { itemName: xField, value: yf } : { value: yf }),
293
+ label: seriesLabel,
294
+ labelLayout: seriesLabelLayout,
295
+ labelLine: seriesLabelLine
296
+ });
297
+ // The alias's own shaping — areaStyle for an area, a ring for a donut.
298
+ // Applied before marks so a theme can still override it.
299
+ if (shape.series) assign(ser, shape.series);
300
+ if (marks) applyMarks(ser, shape.type, marks, ctx);
301
+ return ser;
302
+ });
303
+
304
+ if (shape.cartesian) {
305
+ // Axis ROLES, not axis names: one carries the category, the other the
306
+ // value. A horizontal bar is the same chart with those swapped, which is
307
+ // how ECharts itself expresses it — there is no 'hbar' series type.
308
+ var categoryAxis = shape.horizontal ? 'yAxis' : 'xAxis';
309
+ var valueAxis = shape.horizontal ? 'xAxis' : 'yAxis';
310
+ if (option[categoryAxis]) {
311
+ if (option[categoryAxis].type === undefined) option[categoryAxis].type = 'category';
312
+ } else if (xField) option[categoryAxis] = { type: 'category' };
313
+ if (!option[valueAxis]) option[valueAxis] = {};
314
+ if (option[valueAxis].type === undefined) option[valueAxis].type = 'value';
315
+ // Headroom above the tallest value.
316
+ //
317
+ // ECharts ends a value axis exactly at the data's own maximum, so the
318
+ // highest point sits ON the top gridline and an area chart is clipped
319
+ // flat against it — it reads as a chart that ran out of room rather than
320
+ // one that peaked. VALUE_HEADROOM lifts the top of the scale clear of it.
321
+ //
322
+ // Applied to whichever axis carries the VALUE, which is x on a
323
+ // horizontal bar — hence here, where the role is known, rather than in a
324
+ // theme that can only name x and y.
325
+ if (option[valueAxis].boundaryGap === undefined) {
326
+ option[valueAxis].boundaryGap = VALUE_HEADROOM;
327
+ }
328
+
329
+ // ── AND THE RANGE THE HEADROOM APPLIES TO ────────────────────────
330
+ //
331
+ // VALUE_HEADROOM above did NOTHING until this line existed. ECharts
332
+ // ignores boundaryGap on a value axis while `scale` is off: it builds
333
+ // a rounded range from zero instead, and a percentage extension is
334
+ // absorbed into that rounding. Measured — [0,'10%'] and no
335
+ // boundaryGap at all produced the identical axis.
336
+ //
337
+ // Which also means the zero baseline was never a decision. It is
338
+ // ECharts' default for `scale`, nobody set it, and a comment
339
+ // explaining the `0` in boundaryGap as "a baseline belongs at zero"
340
+ // described something that value does not do.
341
+ //
342
+ // WHICH CHARTS KEEP ZERO, and why it is not a matter of taste:
343
+ //
344
+ // bar length IS the quantity. Truncate the axis and a 5%
345
+ // difference draws as double — the chart states
346
+ // something false about the numbers.
347
+ // stacked the segments are cumulative sums from a baseline.
348
+ // Move the baseline and they stop adding up to the
349
+ // total they are drawn to show.
350
+ // filled an area's fill reads as volume under the curve, so it
351
+ // measures from zero the same way a bar does.
352
+ //
353
+ // Everything else — a plain line, a scatter — encodes CHANGE, and
354
+ // zero flattens exactly the variation it was plotted to reveal. A
355
+ // series moving between 62,000 and 66,000 becomes a straight line
356
+ // pinned to the top of an empty chart, and shrinking the card makes
357
+ // it flatter still.
358
+ var stacked = Boolean(shape.series && shape.series.stack);
359
+ var filled = Boolean(shape.series && shape.series.areaStyle);
360
+ var zeroBased = shape.type === 'bar' || stacked || filled;
361
+ // Only a real value axis: a heatmap is cartesian with TWO category
362
+ // axes, and `scale` on a category axis means nothing.
363
+ if (option[valueAxis].type === 'value' && option[valueAxis].scale === undefined) {
364
+ option[valueAxis].scale = !zeroBased;
365
+ }
366
+ } else {
367
+ // A pie has no axes. Left in place they draw a bare cross behind the
368
+ // slices — and any axis STYLING the theme carries would be rendered for
369
+ // a chart that has none.
370
+ delete option.xAxis;
371
+ delete option.yAxis;
372
+ }
373
+ }
124
374
  }
125
375
 
376
+ // ── conditional formatting ──
377
+ //
378
+ // Applied AFTER the series exist, because a rule is scoped to one of them
379
+ // and needs its index, its palette colour and the field it plots. See
380
+ // rules.js for which ECharts mechanism each target uses and why they differ.
381
+ applyRules(co, option, ctx);
382
+
126
383
  // ── theme-level extras (carried on ChartOptions by the theme) ──
127
384
  // palette → the series color cycle; fontFamily → global text default (ECharts
128
385
  // cascades option.textStyle.fontFamily to all text, so per-block family is
@@ -133,6 +390,118 @@ function chartOptionsToOption(co, opts) {
133
390
  return { option: option, seriesLabel: seriesLabel, chartType: co.chartType, warnings: warnings };
134
391
  }
135
392
 
393
+ // ── chart type ───────────────────────────────────────────────────────────
394
+ //
395
+ // ECharts is the vocabulary. `chartType` IS an ECharts series type and anything
396
+ // ECharts accepts flows straight through — including types registered by the
397
+ // consumer, which is why an unrecognised one is passed on rather than refused.
398
+ //
399
+ // The exceptions are the three names every charting UI offers that ECharts has
400
+ // no series type for. They are not new types: each resolves HERE to a real one
401
+ // plus the property that makes it that chart, so what reaches ECharts is always
402
+ // something ECharts can read. An 'area' series would simply not render, and it
403
+ // would not say why.
404
+ //
405
+ // area → line, filled
406
+ // donut → pie with a hole
407
+ // hbar → bar with the axis roles swapped
408
+ // stackedBar → bar, stacked
409
+ //
410
+ // A caller who prefers to say it in ECharts' own terms still can: chartType
411
+ // 'line' with a series areaStyle is the same chart, and passes through
412
+ // untouched.
413
+ var ALIAS = {
414
+ area: { type: 'line', series: { areaStyle: {} } },
415
+ donut: { type: 'pie', series: { radius: ['45%', '70%'] } },
416
+ hbar: { type: 'bar', horizontal: true },
417
+ stackedBar: { type: 'bar', series: { stack: 'total' } },
418
+ // Stacking is orthogonal to orientation and to fill, so it combines with
419
+ // both. Named rather than left to the caller to assemble, because
420
+ // "horizontal AND stacked" is one chart somebody picks off a list, not two
421
+ // properties they are expected to know compose.
422
+ stackedHbar: { type: 'bar', horizontal: true, series: { stack: 'total' } },
423
+ stackedArea: { type: 'line', series: { areaStyle: {}, stack: 'total' } }
424
+ };
425
+
426
+ // ECharts types that are NOT plotted against a pair of axes. Everything else
427
+ // is assumed cartesian, which is the right default for the types a consumer is
428
+ // most likely to register (bar/line/scatter variants).
429
+ // How far past the tallest value a value axis runs — boundaryGap's
430
+ // [below, above] pair.
431
+ //
432
+ // The point is the '10%' ABOVE: without it a value axis ends exactly at the
433
+ // data's maximum, so the highest point sits on the top gridline and an area
434
+ // chart is clipped flat against it, reading as a chart that ran out of room
435
+ // rather than one that peaked.
436
+ //
437
+ // The `0` below is not a baseline decision and never was. It says only "add no
438
+ // padding underneath", which suits both cases: a zero-based chart is already
439
+ // at zero, and a scaled one should start at its own minimum rather than
440
+ // somewhere arbitrarily below it. What actually holds a bar chart at zero is
441
+ // `scale`, set where the chart's shape is known — see the block that applies
442
+ // this constant. An earlier comment here credited the `0` with that job, which
443
+ // is how a value that does nothing on its own went years without being
444
+ // questioned.
445
+ var VALUE_HEADROOM = [0, '10%'];
446
+
447
+ var NON_CARTESIAN = {
448
+ pie: 1, funnel: 1, gauge: 1, radar: 1, treemap: 1, sunburst: 1,
449
+ sankey: 1, graph: 1, tree: 1, themeRiver: 1
450
+ };
451
+
452
+ // HOW a type reads its data. Most bind through the dataset with an encode, but
453
+ // three do not — and handing those an encode draws nothing at all, silently.
454
+ //
455
+ // cartesian encode {x, y} against a pair of axes bar, line, …
456
+ // nameValue encode {itemName, value} pie, funnel
457
+ // radar its own coordinate system: indicators + arrays
458
+ // treemap a flat {name, value} list; no dataset support
459
+ // heatmap cartesian, but TWO category axes and a value [x, y, v]
460
+ var BINDING = { radar: 'radar', treemap: 'treemap', heatmap: 'heatmap' };
461
+
462
+ // An axis label is the FIELD to bind, and optionally the name to show for it:
463
+ // either 'revenue' or { field: 'revenue', name: 'Revenue (£)' }.
464
+ //
465
+ // They are separable because the field is a key in the data and the name is
466
+ // for a person to read, and those are rarely the same string. Without this a
467
+ // legend can only ever show the raw column key — which in a BI tool is
468
+ // something like `a1b2c3__orders_total`. ECharts already separates them; this
469
+ // is series.name alongside encode, said once.
470
+ function labelField(label) {
471
+ return label && typeof label === 'object' ? label.field : label;
472
+ }
473
+ function labelName(label) {
474
+ if (label && typeof label === 'object') return label.name != null ? label.name : label.field;
475
+ return label;
476
+ }
477
+
478
+ function resolveChartType(name) {
479
+ if (!name) return { type: undefined, cartesian: true, horizontal: false, series: null, binding: 'cartesian' };
480
+ var alias = ALIAS[name];
481
+ var type = alias ? alias.type : name;
482
+ return {
483
+ type: type,
484
+ cartesian: !NON_CARTESIAN[type],
485
+ horizontal: Boolean(alias && alias.horizontal),
486
+ series: (alias && alias.series) || null,
487
+ binding: BINDING[type] || (NON_CARTESIAN[type] ? 'nameValue' : 'cartesian')
488
+ };
489
+ }
490
+
491
+ // Distinct values of a field, in the order the rows present them. Order comes
492
+ // from the data rather than sorting, so a caller that has already ordered its
493
+ // rows — by month, by rank — keeps that order on the axis.
494
+ function categoriesOf(rows, field) {
495
+ var seen = {};
496
+ var out = [];
497
+ for (var i = 0; i < (rows || []).length; i++) {
498
+ var v = rows[i][field];
499
+ var k = String(v);
500
+ if (!seen[k]) { seen[k] = 1; out.push(v); }
501
+ }
502
+ return out;
503
+ }
504
+
136
505
  // theme.marks[chartType] → ECharts series style. Each chart type reads the marks
137
506
  // it understands; unknown keys are simply not consulted (no warnings — marks are
138
507
  // a theme convenience, not user-authored CSS).
@@ -147,7 +516,23 @@ function applyMarks(ser, type, m, ctx) {
147
516
  } else if (type === 'bar') {
148
517
  var bit = {};
149
518
  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
519
+ // The surface gap between fills, as a border the colour of the page.
520
+ //
521
+ // STACKED SERIES ONLY, and that is the whole of it. A border is the only
522
+ // thing that can separate two segments of one stacked bar — they share an
523
+ // edge, so no amount of spacing reaches between them.
524
+ //
525
+ // Between separate bars it is the wrong tool and an actively destructive
526
+ // one. A border eats the fill from BOTH sides at a fixed pixel width while
527
+ // the bar itself narrows with the category count: at 150 categories a bar
528
+ // is 8.5px and a 2px ring takes 47% of it; at 300 it is 4.2px and the ring
529
+ // takes all of it, painting the bar the colour of the background. The
530
+ // chart renders, reports no error, and shows nothing.
531
+ //
532
+ // Bars that merely sit next to each other are already separated by
533
+ // ECharts' own barGap and barCategoryGap, which are percentages and so
534
+ // hold at any width.
535
+ if (m.gap && ser.stack) {
151
536
  if (m.gap.color != null) bit.borderColor = m.gap.color;
152
537
  if (m.gap.width != null) bit.borderWidth = parseSize(m.gap.width, ctx);
153
538
  }
@@ -187,9 +572,27 @@ function axisToEcharts(ax, ctx, path) {
187
572
  }
188
573
  if (ax.ticks) {
189
574
  var tk = ax.ticks;
190
- a.axisTick = clean({ show: tk.show !== false, length: parseSize(tk.length, ctx) });
575
+ // `color` is the tick MARK, alongside `length` and `margin` which also
576
+ // describe the mark. The label's colour is `ticks.font.color`.
577
+ //
578
+ // Both used to land on axisLabel.color, where the font one always won by
579
+ // being assigned second — so `ticks.color` could not take effect at all,
580
+ // and no tick mark could be coloured. Two controls, one of them dead.
581
+ a.axisTick = clean({
582
+ show: tk.show !== false,
583
+ length: parseSize(tk.length, ctx),
584
+ lineStyle: clean({ color: tk.color })
585
+ });
191
586
  a.axisLabel = clean(assign(
192
- { show: tk.show !== false, rotate: tk.angle, margin: parseSize(tk.margin, ctx), color: tk.color },
587
+ {
588
+ show: tk.show !== false, rotate: tk.angle, margin: parseSize(tk.margin, ctx),
589
+ // The axis says the same numbers the tooltip and the data labels do,
590
+ // so it takes the same kind of formatter they already take. Without
591
+ // one, a currency column's axis was the only place in the chart that
592
+ // could not be told what the numbers mean — and it is the part that
593
+ // is always on screen.
594
+ formatter: tk.formatter
595
+ },
193
596
  fontToTextStyle(tk.font, ctx, path + '.ticks.font')
194
597
  ));
195
598
  }
@@ -198,6 +601,17 @@ function axisToEcharts(ax, ctx, path) {
198
601
  a.nameTextStyle = fontToTextStyle(ax.title.font, ctx, path + '.title.font');
199
602
  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
603
  }
604
+ // Straight through, in ECharts' own vocabulary and meaning: a boolean or a
605
+ // [start, end] pair on a category axis, a [min, max] extension on a value
606
+ // one. Passing it verbatim is deliberate — inventing a second name for a
607
+ // property ECharts already has is how two vocabularies start.
608
+ if (ax.boundaryGap !== undefined) a.boundaryGap = ax.boundaryGap;
609
+ // Whether the axis fits the data or includes zero. Passed straight through
610
+ // and, because it is set here from the caller's options, it wins over the
611
+ // per-shape default applied later — which is the whole point: the default is
612
+ // right for the chart type, and this is for the reader who knows better
613
+ // about their own numbers.
614
+ if (ax.scale !== undefined) a.scale = ax.scale;
201
615
  return clean(a);
202
616
  }
203
617
 
package/lib/register.js CHANGED
@@ -9,17 +9,31 @@
9
9
 
10
10
  var echarts = require('echarts/core');
11
11
 
12
- var { BarChart, LineChart, PieChart, ScatterChart } = require('echarts/charts');
12
+ var {
13
+ BarChart, LineChart, PieChart, ScatterChart,
14
+ // The four that need their own coordinate system or their own data shape.
15
+ // Registered by default rather than left to the consumer: a chart type the
16
+ // translator understands but the runtime cannot draw fails as an empty box
17
+ // with "Unknown series" in the console, which is a worse default than the
18
+ // few kB these cost.
19
+ FunnelChart, RadarChart, TreemapChart, HeatmapChart
20
+ } = require('echarts/charts');
13
21
  var {
14
22
  GridComponent, TooltipComponent, LegendComponent, TitleComponent,
15
- DatasetComponent, ToolboxComponent, DataZoomComponent, MarkLineComponent, MarkPointComponent
23
+ DatasetComponent, ToolboxComponent, DataZoomComponent, MarkLineComponent, MarkPointComponent,
24
+ // RadarComponent is the radar's AXES — the series alone draws nothing.
25
+ // VisualMapComponent is what turns a heatmap's numbers into colours; without
26
+ // it every cell renders the same shade.
27
+ RadarComponent, VisualMapComponent
16
28
  } = require('echarts/components');
17
29
  var { CanvasRenderer, SVGRenderer } = require('echarts/renderers');
18
30
 
19
31
  echarts.use([
20
32
  BarChart, LineChart, PieChart, ScatterChart,
33
+ FunnelChart, RadarChart, TreemapChart, HeatmapChart,
21
34
  GridComponent, TooltipComponent, LegendComponent, TitleComponent,
22
35
  DatasetComponent, ToolboxComponent, DataZoomComponent, MarkLineComponent, MarkPointComponent,
36
+ RadarComponent, VisualMapComponent,
23
37
  CanvasRenderer, SVGRenderer
24
38
  ]);
25
39
 
package/lib/rules.js ADDED
@@ -0,0 +1,367 @@
1
+ var R = require('@xeplr/rules');
2
+ var matches = R.matches;
3
+ var formatNumber = R.formatNumber;
4
+
5
+ // CONDITIONAL FORMATTING — "when this column is under zero, make THAT red".
6
+ //
7
+ // A rule has three parts, and the third depends on the second:
8
+ //
9
+ // WHEN field, op, value which column to test, and against what
10
+ // APPLY target which object on the chart it styles
11
+ // THEN then: { … } only what that object can actually take
12
+ //
13
+ // The point of naming a target is that a chart is not one thing. Colouring the
14
+ // bar, colouring the number printed above the bar, and colouring the category
15
+ // name under it are three different objects with three different vocabularies —
16
+ // a bar has an opacity and no font, an axis label has a font and no opacity.
17
+ // One rule list that cannot say which one it means can only ever style the one
18
+ // somebody guessed at.
19
+ //
20
+ // ── THE MECHANISM PER TARGET, and why they differ ────────────────────────
21
+ //
22
+ // Every one of these is ECharts' own API. They differ because ECharts itself
23
+ // supports different things in different places, which was established by
24
+ // rendering each and reading the output rather than by reading the docs:
25
+ //
26
+ // mark itemStyle callbacks. itemStyle.color accepts a function and
27
+ // is handed the whole row, so a rule can test any column and any
28
+ // type — including a string. Verified on bar, line, scatter,
29
+ // pie, funnel and treemap.
30
+ //
31
+ // dataLabel label.formatter + label.rich. Label STYLE callbacks do not
32
+ // work: `label: { color: fn }` puts the function's source text
33
+ // into the SVG's fill attribute. So the styling has to travel as
34
+ // rich-text markup, which is ECharts' mechanism for styling part
35
+ // of a string.
36
+ //
37
+ // axisX/Y axisLabel.formatter + axisLabel.rich. axisLabel.color DOES
38
+ // take a callback — it is documented as (value, index) => color
39
+ // and it works — but axisLabel.fontWeight does not. Rather than
40
+ // support colour one way and weight another, both go through
41
+ // rich, which is one code path and supports everything.
42
+ //
43
+ // THE CONDITION LIVES ELSEWHERE. `matches`, the operators and number
44
+ // formatting are in @xeplr/rules, because "when margin is under zero" is the
45
+ // same question whether the answer paints a bar, a table cell or an arrow —
46
+ // and a table asking it must not have to depend on a charting library. What
47
+ // stays here is the half that genuinely IS ECharts: turning a match into
48
+ // itemStyle callbacks and label formatters.
49
+ //
50
+ // A visualMap is deliberately NOT used, though it was the obvious candidate.
51
+ // It carries three restrictions that are the mechanism's rather than ECharts':
52
+ // it cannot test a string column at all, it paints an unmatched item black,
53
+ // and a second one on the same series replaces the first outright — silently,
54
+ // along with everything the first had coloured. itemStyle callbacks have none
55
+ // of those.
56
+
57
+ var TARGETS = { mark: 1, dataLabel: 1, axisX: 1, axisY: 1 };
58
+
59
+ // itemStyle keys a mark rule may set. ECharts accepts a callback for each.
60
+ var MARK_KEYS = [
61
+ 'color', 'opacity', 'borderColor', 'borderWidth', 'borderType',
62
+ 'borderRadius', 'shadowBlur', 'shadowColor', 'shadowOffsetX', 'shadowOffsetY'
63
+ ];
64
+
65
+ // rich-text keys a text rule may set. A subset of what ECharts' rich accepts —
66
+ // the ones that mean something for a single run of text on a chart.
67
+ var TEXT_KEYS = [
68
+ 'color', 'fontStyle', 'fontWeight', 'fontSize', 'fontFamily',
69
+ 'backgroundColor', 'borderColor', 'borderWidth', 'borderRadius',
70
+ 'padding', 'textBorderColor', 'textBorderWidth', 'lineHeight'
71
+ ];
72
+
73
+ // ── the THEN, for text: number formatting ────────────────────────────────
74
+
75
+ /**
76
+ * Whether a run of text can carry rich-text styling.
77
+ *
78
+ * ECharts' rich markup is `{styleName|text}` and has no escape sequence, so a
79
+ * value containing a brace or a pipe cannot be wrapped — a `}` inside would
80
+ * end the run early and the remainder would print unstyled.
81
+ *
82
+ * Answered rather than worked around: the text is printed as it is and the
83
+ * styling is dropped for that one label. Stripping the character would be
84
+ * silent data corruption, and wrapping it anyway would truncate the value.
85
+ */
86
+ function richSafe(text) {
87
+ return String(text).indexOf('{') < 0 && String(text).indexOf('}') < 0 && String(text).indexOf('|') < 0;
88
+ }
89
+
90
+ // ── applying ─────────────────────────────────────────────────────────────
91
+
92
+ function pick(source, keys) {
93
+ var out = null;
94
+ for (var i = 0; i < keys.length; i++) {
95
+ if (source[keys[i]] !== undefined) {
96
+ if (!out) out = {};
97
+ out[keys[i]] = source[keys[i]];
98
+ }
99
+ }
100
+ return out;
101
+ }
102
+
103
+ /**
104
+ * The rules, sorted into the buckets that each get their own mechanism.
105
+ *
106
+ * Reported as it goes: a rule that names a target nothing understands, or sets
107
+ * a property its target cannot take, is dropped HERE with a reason rather than
108
+ * emitted for ECharts to ignore. A control that appears to work is the failure
109
+ * this whole module is arranged to avoid.
110
+ */
111
+ function sortRules(co, option, ctx) {
112
+ var yLabels = (co.axes && co.axes.y && co.axes.y.labels) || [];
113
+ var series = option.series || [];
114
+ var out = { mark: {}, dataLabel: {}, axisX: [], axisY: [] };
115
+
116
+ (co.rules || []).forEach(function (rule, i) {
117
+ var where = 'rules[' + i + ']';
118
+ if (!rule || !rule.field) { ctx.warn(where + ' names no column to test (ignored)'); return; }
119
+ if (!rule.op) { ctx.warn(where + ' has no comparison (ignored)'); return; }
120
+ var target = rule.target || 'mark';
121
+ if (!TARGETS[target]) { ctx.warn(where + ' applies to "' + target + '", which is not something on a chart (ignored)'); return; }
122
+
123
+ var then = rule.then || {};
124
+ var keys = target === 'mark' ? MARK_KEYS : TEXT_KEYS;
125
+ var style = pick(then, keys);
126
+ var changesText = target !== 'mark' && (then.hide || then.format);
127
+ if (!style && !changesText) {
128
+ ctx.warn(where + ' sets nothing its target can take, so it would do nothing (ignored)');
129
+ return;
130
+ }
131
+ // Said, because the panel is generated per target and a rule that arrives
132
+ // with the wrong vocabulary came from somewhere else — a hand-edited
133
+ // config, or a target changed after the properties were chosen.
134
+ var stray = Object.keys(then).filter(function (k) {
135
+ return keys.indexOf(k) < 0 && k !== 'hide' && k !== 'format';
136
+ });
137
+ if (stray.length) ctx.warn(where + ' sets ' + stray.join(', ') + ', which "' + target + '" has no such thing (dropped)');
138
+
139
+ var entry = { rule: rule, style: style, hide: Boolean(then.hide), format: then.format || null };
140
+
141
+ if (target === 'axisX' || target === 'axisY') {
142
+ // An axis-label formatter is handed the axis's own value and its index,
143
+ // and nothing else — there is no row to look at. So a rule here can only
144
+ // test the column that axis is showing.
145
+ out[target].push(entry);
146
+ return;
147
+ }
148
+
149
+ var targets = [];
150
+ if (rule.series) {
151
+ var idx = -1;
152
+ for (var s = 0; s < yLabels.length; s++) if (labelFieldOf(yLabels[s]) === rule.series) idx = s;
153
+ if (idx < 0 || !series[idx]) {
154
+ ctx.warn(where + ' applies to "' + rule.series + '", which this chart does not plot (ignored)');
155
+ return;
156
+ }
157
+ targets = [idx];
158
+ } else {
159
+ for (var t = 0; t < series.length; t++) targets.push(t);
160
+ }
161
+ targets.forEach(function (si) {
162
+ (out[target][si] || (out[target][si] = [])).push(entry);
163
+ });
164
+ });
165
+ return out;
166
+ }
167
+
168
+ function labelFieldOf(label) {
169
+ return label && typeof label === 'object' ? label.field : label;
170
+ }
171
+
172
+ /** The row behind a datum, whatever binding put it there. */
173
+ function rowOf(params) {
174
+ var d = params && params.data;
175
+ if (d && typeof d === 'object' && !Array.isArray(d)) return d;
176
+ return null;
177
+ }
178
+
179
+ /**
180
+ * MARKS — one itemStyle callback per property any rule sets.
181
+ *
182
+ * The callback returns the FIRST matching rule's value, and the base value
183
+ * otherwise. The base matters: returning undefined for `color` renders the
184
+ * mark with no fill at all, so an unmatched bar would disappear rather than
185
+ * keep its colour. Every other property tolerates undefined, but colour is
186
+ * given the series' palette entry explicitly.
187
+ */
188
+ function applyMarkRules(entriesBySeries, option, palette) {
189
+ Object.keys(entriesBySeries).forEach(function (si) {
190
+ var entries = entriesBySeries[si];
191
+ var ser = option.series[si];
192
+ if (!ser || !entries || !entries.length) return;
193
+
194
+ var props = {};
195
+ entries.forEach(function (e) {
196
+ Object.keys(e.style || {}).forEach(function (k) { props[k] = 1; });
197
+ });
198
+
199
+ var base = ser.itemStyle || {};
200
+ var next = {};
201
+ for (var k in base) next[k] = base[k];
202
+
203
+ Object.keys(props).forEach(function (key) {
204
+ var fallback = base[key] !== undefined
205
+ ? base[key]
206
+ : (key === 'color' ? palette[Number(si) % palette.length] : undefined);
207
+ next[key] = function (params) {
208
+ var row = rowOf(params);
209
+ for (var i = 0; i < entries.length; i++) {
210
+ var e = entries[i];
211
+ if (!e.style || e.style[key] === undefined) continue;
212
+ if (matches(e.rule, row ? row[e.rule.field] : undefined)) return e.style[key];
213
+ }
214
+ return fallback;
215
+ };
216
+ });
217
+ ser.itemStyle = next;
218
+ });
219
+ }
220
+
221
+ /**
222
+ * DATA LABELS — one formatter and one rich map per series.
223
+ *
224
+ * The formatter has to reproduce what the label would have said, because
225
+ * setting one replaces ECharts' default entirely. `valueField` is what the
226
+ * series plots; a non-cartesian series labels by name, which is what its
227
+ * binding put in params.name.
228
+ */
229
+ function applyLabelRules(entriesBySeries, option, co, ctx) {
230
+ var yLabels = (co.axes && co.axes.y && co.axes.y.labels) || [];
231
+ var byName = !(co.chartType === undefined) && isNameValue(co.chartType);
232
+
233
+ Object.keys(entriesBySeries).forEach(function (si) {
234
+ var entries = entriesBySeries[si];
235
+ var ser = option.series[si];
236
+ if (!ser || !entries || !entries.length) return;
237
+
238
+ if (ser.label && ser.label.formatter) {
239
+ // Composing with a caller's own formatter is not possible without
240
+ // knowing what it means to produce. Said plainly, because a rule that
241
+ // quietly lost to a template set three panels away is unfindable.
242
+ ctx.warn('a data-label formatter is already set, so conditional formatting on the label is not applied — clear Data labels → Format to use rules');
243
+ return;
244
+ }
245
+
246
+ var valueField = labelFieldOf(yLabels[Number(si)]);
247
+ var rich = {};
248
+ entries.forEach(function (e, n) {
249
+ if (e.style) rich['r' + si + '_' + n] = e.style;
250
+ });
251
+
252
+ // A COPY. seriesLabel is one object shared by every series, so writing a
253
+ // formatter into it would give every series this series' rules.
254
+ var label = {};
255
+ for (var k in (ser.label || {})) label[k] = ser.label[k];
256
+
257
+ label.formatter = function (params) {
258
+ var row = rowOf(params);
259
+ var raw = byName ? params.name : (row && valueField != null ? row[valueField] : params.value);
260
+ var text = raw === null || raw === undefined ? '' : String(raw);
261
+
262
+ for (var i = 0; i < entries.length; i++) {
263
+ var e = entries[i];
264
+ if (!matches(e.rule, row ? row[e.rule.field] : undefined)) continue;
265
+ if (e.hide) return '';
266
+ if (e.format) {
267
+ var formatted = formatNumber(raw, e.format);
268
+ if (formatted !== null) text = formatted;
269
+ }
270
+ var name = 'r' + si + '_' + i;
271
+ if (rich[name] && richSafe(text)) return '{' + name + '|' + text + '}';
272
+ return text;
273
+ }
274
+ return text;
275
+ };
276
+ if (Object.keys(rich).length) label.rich = rich;
277
+ ser.label = label;
278
+ });
279
+ }
280
+
281
+ function isNameValue(type) {
282
+ return type === 'pie' || type === 'donut' || type === 'funnel' || type === 'gauge';
283
+ }
284
+
285
+ /**
286
+ * AXIS LABELS — the same formatter/rich pair, on the axis.
287
+ *
288
+ * An axis-label formatter receives the tick's own value and its index. There
289
+ * is no row, so a rule here tests what the axis is showing and nothing else —
290
+ * which is exactly right for "make IPD red" and impossible for "make the IPD
291
+ * tick red when its margin is negative".
292
+ */
293
+ function applyAxisRules(entries, option, which, ctx) {
294
+ if (!entries.length) return;
295
+ var key = which === 'axisX' ? 'xAxis' : 'yAxis';
296
+ var axis = option[key];
297
+ if (!axis) {
298
+ ctx.warn('a rule styles the ' + (which === 'axisX' ? 'X' : 'Y') + ' axis labels, but this chart has no such axis (ignored)');
299
+ return;
300
+ }
301
+ if (axis.axisLabel && axis.axisLabel.formatter) {
302
+ ctx.warn('an ' + (which === 'axisX' ? 'X' : 'Y') + ' axis label formatter is already set, so conditional formatting on it is not applied');
303
+ return;
304
+ }
305
+
306
+ var rich = {};
307
+ entries.forEach(function (e, n) {
308
+ if (e.style) rich[which + '_' + n] = e.style;
309
+ });
310
+
311
+ var axisLabel = {};
312
+ for (var k in (axis.axisLabel || {})) axisLabel[k] = axis.axisLabel[k];
313
+
314
+ axisLabel.formatter = function (value) {
315
+ var text = value === null || value === undefined ? '' : String(value);
316
+ for (var i = 0; i < entries.length; i++) {
317
+ var e = entries[i];
318
+ if (!matches(e.rule, value)) continue;
319
+ if (e.hide) return '';
320
+ if (e.format) {
321
+ var formatted = formatNumber(value, e.format);
322
+ if (formatted !== null) text = formatted;
323
+ }
324
+ var name = which + '_' + i;
325
+ if (rich[name] && richSafe(text)) return '{' + name + '|' + text + '}';
326
+ return text;
327
+ }
328
+ return text;
329
+ };
330
+ if (Object.keys(rich).length) axisLabel.rich = rich;
331
+ axis.axisLabel = axisLabel;
332
+ }
333
+
334
+ // ECharts' own default series colours, for the mark fallback when the caller
335
+ // carries no palette. Getting this wrong means marks with no fill.
336
+ var DEFAULT_PALETTE = [
337
+ '#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de',
338
+ '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'
339
+ ];
340
+
341
+ /**
342
+ * Apply every rule on `co` to the option being built.
343
+ *
344
+ * Called after the series exist, because a rule is scoped to one of them and
345
+ * needs its index, its palette colour and the field it plots.
346
+ */
347
+ function applyRules(co, option, ctx) {
348
+ if (!Array.isArray(co.rules) || !co.rules.length) return;
349
+ if (!Array.isArray(option.series) || !option.series.length) return;
350
+
351
+ var palette = (Array.isArray(co.palette) && co.palette.length) ? co.palette : DEFAULT_PALETTE;
352
+ var sorted = sortRules(co, option, ctx);
353
+
354
+ applyMarkRules(sorted.mark, option, palette);
355
+ applyLabelRules(sorted.dataLabel, option, co, ctx);
356
+ applyAxisRules(sorted.axisX, option, 'axisX', ctx);
357
+ applyAxisRules(sorted.axisY, option, 'axisY', ctx);
358
+ }
359
+
360
+ module.exports = applyRules;
361
+ // Re-exported so an existing caller keeps working; the source of truth is
362
+ // @xeplr/rules.
363
+ module.exports.matches = matches;
364
+ module.exports.formatNumber = formatNumber;
365
+ module.exports.richSafe = richSafe;
366
+ module.exports.MARK_KEYS = MARK_KEYS;
367
+ module.exports.TEXT_KEYS = TEXT_KEYS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xeplr/ui-charts",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Organized ECharts spec → option builder + React <Chart> for direct Apache ECharts (no wrapper). Premium optimizers plug in at the option level.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -33,5 +33,8 @@
33
33
  },
34
34
  "devDependencies": {
35
35
  "echarts": "^6.1.0"
36
+ },
37
+ "dependencies": {
38
+ "@xeplr/rules": "^1.0.0"
36
39
  }
37
40
  }