@pacem/pacem-charts 1.0.0-abel

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.
@@ -0,0 +1,437 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { CustomElement, Watch, PropertyConverters, P, PCSS, Utils, CustomElementUtils, Logging } from '@pacem/pacem-core';
8
+ import { PacemSeriesChartElement } from './types';
9
+ //namespace Pacem.Components.Charts {
10
+ /**
11
+ * Returns the anchor points for a Bézier curve.
12
+ * @param p The actual point.
13
+ * @param p0 The previous point, if any.
14
+ * @param p1 The next point, if any.
15
+ */
16
+ function getSplineCtrlPoints(p, p0, p1) {
17
+ let c0 = p, c1 = p;
18
+ const p0Null = Utils.isNull(p0), p1Null = Utils.isNull(p1);
19
+ if (!(p0Null && p1Null)) {
20
+ const portion = 3;
21
+ let m0, m1;
22
+ if (!p0Null) {
23
+ m0 = (p.y - p0.y) / (p.x - p0.x);
24
+ }
25
+ if (!p1Null) {
26
+ m1 = (p1.y - p.y) / (p1.x - p.x);
27
+ }
28
+ // average slope
29
+ const m = ((m1 || (m0 || 0)) + (m0 || (m1 || 0))) / 2;
30
+ const dx0 = p0Null ? 0 : (p.x - p0.x) / portion;
31
+ const dx1 = p1Null ? 0 : (p1.x - p.x) / portion;
32
+ c0 = { x: p.x - dx0, y: p.y - dx0 * m };
33
+ c1 = { x: p.x + dx1, y: p.y + dx1 * m };
34
+ }
35
+ return { c0: c0, c1: c1 };
36
+ }
37
+ const GET_VAL = CustomElementUtils.getAttachedPropertyValue;
38
+ const SET_VAL = CustomElementUtils.setAttachedPropertyValue;
39
+ const DEL_VAL = CustomElementUtils.deleteAttachedPropertyValue;
40
+ const PADDING_PIXELS = 24;
41
+ const SERIES_MAGNITUDE = 'pacem:chart-series:area';
42
+ const SVG_NS = "http://www.w3.org/2000/svg";
43
+ let PacemChartElement = class PacemChartElement extends PacemSeriesChartElement {
44
+ constructor() {
45
+ super(...arguments);
46
+ this.#hover = false;
47
+ this._enterHandler = (evt) => {
48
+ this.#hover = true;
49
+ };
50
+ this._leaveHandler = (evt) => {
51
+ this.#hover = false;
52
+ };
53
+ this._moveHandler = (evt) => {
54
+ if (!this.#hover) {
55
+ return;
56
+ }
57
+ };
58
+ this._chartFillSeries = [];
59
+ }
60
+ // TODO: implement
61
+ // @Watch({ converter: PropertyConverters.String }) hoverMode: 'abscissa' | 'point'; // 'point' assumed as default
62
+ _getVirtualGrid(items, minX, maxX, minY, maxY, xAxisType = this.xAxisType, steps, labels) {
63
+ return super.getVirtualGrid(items, minX, maxX, minY, maxY, xAxisType, steps, labels);
64
+ }
65
+ _wipe(series = this.chartSeries, startIndex = 0) {
66
+ super.wipeOut(series, startIndex);
67
+ }
68
+ viewActivatedCallback() {
69
+ super.viewActivatedCallback();
70
+ this._setupBehavior();
71
+ }
72
+ disconnectedCallback() {
73
+ this._dismantleBehavior();
74
+ super.disconnectedCallback();
75
+ }
76
+ propertyChangedCallback(name, old, val, first) {
77
+ super.propertyChangedCallback(name, old, val, first);
78
+ switch (name) {
79
+ case 'type':
80
+ case 'aspectRatio':
81
+ this.draw();
82
+ break;
83
+ }
84
+ }
85
+ _setupBehavior() {
86
+ this.addEventListener('mouseenter', this._enterHandler, false);
87
+ this.addEventListener('mouseleave', this._enterHandler, false);
88
+ this.addEventListener('mousemove', this._moveHandler, false);
89
+ }
90
+ _dismantleBehavior() {
91
+ this.removeEventListener('mouseenter', this._enterHandler, false);
92
+ this.removeEventListener('mouseleave', this._leaveHandler, false);
93
+ this.removeEventListener('mousemove', this._moveHandler, false);
94
+ }
95
+ #hover;
96
+ _buildLinearGradient(seriesIndex) {
97
+ return super.buildLinearGradient(seriesIndex);
98
+ }
99
+ _setGradientColor(grad, color) {
100
+ super.setGradientColor(grad, color);
101
+ }
102
+ drawSeries(datasource) {
103
+ if (!this.isReady || Utils.isNull(this.chartSize)) {
104
+ return;
105
+ }
106
+ this.ensureChartContainer();
107
+ const type = this.type || 'line';
108
+ const padding = PADDING_PIXELS;
109
+ // resize all
110
+ var size = this.chartSize;
111
+ if (size.height <= padding || size.width <= padding) {
112
+ return;
113
+ }
114
+ // items && labels?
115
+ if (Utils.isNullOrEmpty(datasource) || datasource.every(i => Utils.isNullOrEmpty(i.values))) {
116
+ this._wipe();
117
+ return;
118
+ }
119
+ const body = this.ensureChartBody('line', size.width, size.height);
120
+ // from now on, drawing...
121
+ this.log(Logging.LogLevel.Debug, `Drawing ${type} chart.`);
122
+ // #region computations
123
+ const xAxisType = this.xAxisType || 'string';
124
+ let minY = this.yAxisMin ?? Number.NaN, maxY = this.yAxisMax ?? Number.NaN, minX = Number.NaN, maxX = Number.NaN;
125
+ let stretch = 1;
126
+ // individuate the min/max x & y in order correctly apply aspect ratio...
127
+ for (let series of datasource) {
128
+ let data = series.values;
129
+ if (data && data.length) {
130
+ let j = 0;
131
+ for (let item of data) {
132
+ let pt = this.chartDataItemToPoint(item, data);
133
+ if (Utils.isNull(this.yAxisMin)) {
134
+ minY = isNaN(minY) ? pt.y : Math.min(minY, pt.y);
135
+ }
136
+ if (Utils.isNull(this.yAxisMax)) {
137
+ maxY = isNaN(maxY) ? pt.y : Math.max(maxY, pt.y);
138
+ }
139
+ if (j === 0)
140
+ minX = isNaN(minX) ? pt.x : Math.min(minX, pt.x);
141
+ else if (j === data.length - 1)
142
+ maxX = isNaN(maxX) ? pt.x : Math.max(maxX, pt.x);
143
+ j++;
144
+ }
145
+ }
146
+ }
147
+ /** virtual grid made of x- and y-axis labels */
148
+ const withValues = datasource.find(i => !Utils.isNullOrEmpty(i.values));
149
+ const grid = this._getVirtualGrid(withValues.values, minX, maxX, minY, maxY, xAxisType, this.yAxisDensity, this.xAxisDensity);
150
+ const topGrid = grid.y[grid.y.length - 1], bottomGrid = grid.y[0];
151
+ if (topGrid === bottomGrid)
152
+ return;
153
+ // estimate y-axis max width
154
+ const halfPad = padding * .5;
155
+ const maxYLabelLength = grid.y.reduce((max, i) => Math.max(max, this.estimateYAxisLabelWidth(i) + halfPad), padding);
156
+ const maxXLabelHalfLength = grid.x.reduce((max, i) => Math.max(max, this.estimateXAxisLabelWidth(i)), padding) * .5;
157
+ const paddingYAxis = Math.max(maxXLabelHalfLength, maxYLabelLength), paddingYAxisEnd = maxXLabelHalfLength; // Math.max(padding, this.estimateXAxisLabelWidth(minX) * .5 + PADDING_PIXELS);
158
+ /** width of the WHOLE series dedicated area */
159
+ const seriesWidth = size.width - paddingYAxis - paddingYAxisEnd;
160
+ /** height of the WHOLE series dedicated area */
161
+ const gridHeight = (size.height - 2 * padding);
162
+ const seriesHeight = gridHeight * (1 - (topGrid - bottomGrid - (maxY - minY)) / (topGrid - bottomGrid));
163
+ const seriesY = gridHeight * (topGrid - maxY) / (topGrid - bottomGrid);
164
+ // const seriesY2 = gridHeight * (minY - bottomGrid) / (topGrid - bottomGrid);
165
+ // if not a `graph`
166
+ if (xAxisType !== 'number' || (this.aspectRatio !== 'monometric' && this.aspectRatio !== 'logaritmic')) {
167
+ // consider padding
168
+ stretch = seriesWidth / seriesHeight;
169
+ body.setAttribute('height', size.height.toString());
170
+ body.setAttribute('width', size.width.toString());
171
+ }
172
+ body.setAttribute('viewBox', `0 0 ${size.width} ${size.height}`);
173
+ // #endregion
174
+ // #region render series
175
+ let iter = 0;
176
+ // normalize
177
+ const spanX = maxX - minX, spanY = maxY - minY;
178
+ const normX = 100 * stretch / spanX, normY = 100 / spanY;
179
+ const normPadding = 100 * padding / seriesHeight;
180
+ const buildPoint = (it, series) => {
181
+ let p = this.chartDataItemToPoint(it, series);
182
+ //pt.x *= normX;
183
+ p.y *= normY;
184
+ // Limit as much as possible the magnitude of a number inclued in the svg.
185
+ // Big numbers "overflow the renderer".
186
+ p.x = (p.x - minX) * normX;
187
+ //pt.y = (pt.y - minY) * normY;
188
+ return p;
189
+ };
190
+ const chartSeries = this.chartSeries;
191
+ const chartFillSeries = this._chartFillSeries;
192
+ const chartGrid = this.chartGrid;
193
+ const splineHere = type === 'spline' || type === 'splinearea';
194
+ for (let series of datasource) {
195
+ // does the series svg exist?
196
+ let svg, svgFill;
197
+ let grad;
198
+ if (chartSeries.length > iter) {
199
+ svg = chartSeries[iter];
200
+ svgFill = chartFillSeries[iter];
201
+ grad = svgFill.firstElementChild.firstElementChild;
202
+ }
203
+ else {
204
+ svg = document.createElementNS(SVG_NS, 'svg');
205
+ svg.setAttribute('pacem', '');
206
+ body.appendChild(svg);
207
+ svgFill = document.createElementNS(SVG_NS, 'svg');
208
+ svgFill.setAttribute('pacem', '');
209
+ body.insertBefore(svgFill, body.children.item(iter + /* <defs> should remain the very first child and 'grid' the very second */ 2));
210
+ // 1st child - defs (gradient)
211
+ const defs = document.createElementNS(SVG_NS, 'defs');
212
+ defs.appendChild(grad = this._buildLinearGradient(iter));
213
+ svgFill.appendChild(defs);
214
+ // 2nd child - fill path
215
+ svgFill.appendChild(document.createElementNS(SVG_NS, 'path'));
216
+ chartFillSeries.push(svgFill);
217
+ // 1st child stroke path
218
+ svg.appendChild(document.createElementNS(SVG_NS, 'path'));
219
+ // 2nd + nth+1 child will be point circles
220
+ chartSeries.push(svg);
221
+ }
222
+ // series positioning:
223
+ /*
224
+ --------------------------------------
225
+ | |--------------------------------|
226
+ |pad| series here |
227
+ | |--------------------------------|
228
+ --------------------------------------
229
+ | | pad |
230
+ --------------------------------------
231
+ */
232
+ let className = 'chart-series';
233
+ const fill = type === 'area' || type === 'splinearea';
234
+ if (!Utils.isNullOrEmpty(series.className)) {
235
+ className += ' ' + series.className;
236
+ }
237
+ [svg, svgFill].forEach(s => {
238
+ s.setAttribute('class', className);
239
+ s.setAttribute('x', paddingYAxis.toString());
240
+ s.setAttribute('y', seriesY.toString());
241
+ s.setAttribute('width', seriesWidth.toString());
242
+ s.setAttribute('height', (seriesHeight + 2 * padding).toString());
243
+ });
244
+ Utils.addClass(svgFill, PCSS + '-inert');
245
+ // pick path as single child element for the series.
246
+ let pathFill = svgFill.children.item(1);
247
+ let path = svg.firstElementChild;
248
+ path.style.stroke = series.color;
249
+ pathFill.style.stroke =
250
+ path.style.fill = 'none';
251
+ this._setGradientColor(grad, series.color);
252
+ pathFill.style.fill = `url(#${grad.id})`;
253
+ pathFill.style.display = fill ? '' : 'none';
254
+ let d = '';
255
+ let data = series.values;
256
+ if (data && data.length) {
257
+ const addInteractivePoint = (center, j, series, dataItem, color) => {
258
+ const ndx = j + /* count the <path> child */ 1;
259
+ let circle;
260
+ if (svg.children.length > ndx) {
261
+ circle = svg.children.item(ndx);
262
+ }
263
+ else {
264
+ circle = document.createElementNS(SVG_NS, 'line');
265
+ circle.setAttribute('class', 'circle');
266
+ svg.appendChild(circle);
267
+ }
268
+ const x1 = center.x.toString(), y1 = (-center.y).toString();
269
+ circle.setAttribute('x1', x1);
270
+ circle.setAttribute('y1', y1);
271
+ circle.setAttribute('x2', x1);
272
+ circle.setAttribute('y2', y1);
273
+ circle.style.stroke = color;
274
+ this.assignUiBehaviors(circle, series, dataItem, color);
275
+ };
276
+ const wipeExceedingPoints = () => {
277
+ const remaining = series.datapoints ? (data.length + 1) : 1;
278
+ for (let k = svg.children.length - 1; k >= remaining; k--) {
279
+ var circle = svg.children.item(k);
280
+ this.disposeUiBehaviors(circle);
281
+ circle.remove();
282
+ }
283
+ };
284
+ if (splineHere) {
285
+ // #region SPLINE
286
+ let pt0, pt, c0;
287
+ for (let j = 0; j < data.length; j++) {
288
+ const item = data[j];
289
+ if (isNaN(item.value)) {
290
+ continue;
291
+ }
292
+ if (!pt) {
293
+ pt = buildPoint(item, data);
294
+ }
295
+ let pt1;
296
+ if (splineHere && j < (data.length - 1)) {
297
+ pt1 = buildPoint(data[j + 1], data);
298
+ }
299
+ // accumulate `area` for sorting
300
+ let areaSoFar = GET_VAL(series, SERIES_MAGNITUDE, 0);
301
+ areaSoFar += item.value;
302
+ SET_VAL(series, SERIES_MAGNITUDE, areaSoFar);
303
+ var c = getSplineCtrlPoints(pt, pt0, pt1);
304
+ d += Utils.isNullOrEmpty(d) ? `M${pt.x},${-pt.y} ` : `C${c0.x},${-c0.y} ${c.c0.x},${-c.c0.y} ${pt.x},${-pt.y} `;
305
+ if (series.datapoints) {
306
+ addInteractivePoint(pt, j, series, item, series.color);
307
+ }
308
+ // step next
309
+ pt0 = pt;
310
+ pt = pt1, c0 = c.c1;
311
+ }
312
+ // #endregion
313
+ }
314
+ else {
315
+ // #region LINE
316
+ let j = 0;
317
+ for (let item of data) {
318
+ if (isNaN(item.value)) {
319
+ continue;
320
+ }
321
+ const pt = buildPoint(item, data);
322
+ // accumulate `area` for sorting
323
+ let areaSoFar = GET_VAL(series, SERIES_MAGNITUDE, 0);
324
+ areaSoFar += item.value;
325
+ SET_VAL(series, SERIES_MAGNITUDE, areaSoFar);
326
+ d += Utils.isNullOrEmpty(d) ? `M${pt.x},${-pt.y} ` : `L${pt.x},${-pt.y} `;
327
+ if (series.datapoints) {
328
+ addInteractivePoint(pt, j, series, item, series.color);
329
+ }
330
+ j++;
331
+ }
332
+ // #endregion
333
+ }
334
+ wipeExceedingPoints();
335
+ }
336
+ path.setAttribute('d', d);
337
+ pathFill.setAttribute('d', d + `V${(-minY * normY)} H0 Z`);
338
+ // tick
339
+ iter++;
340
+ }
341
+ // remove exceeding series
342
+ this._wipe(chartFillSeries, iter);
343
+ this._wipe(chartSeries, iter);
344
+ const w0 = 100 * stretch, h0 = 100 + 2 * normPadding, x0 = 0, //minX * normX,
345
+ y0 = maxY * normY + normPadding;
346
+ const svbox = `${x0} ${-y0} ${w0} ${h0}`;
347
+ for (var svg of chartSeries.concat(chartFillSeries)) {
348
+ svg.setAttribute('viewBox', svbox);
349
+ }
350
+ const chartMask = this.chartMask;
351
+ let mask = chartMask.children.item(1);
352
+ mask.setAttribute('x', x0.toString());
353
+ mask.setAttribute('height', (size.height - padding).toString());
354
+ // #endregion
355
+ // #region grid
356
+ chartGrid.setAttribute('viewBox', `0 0 ${size.width} ${size.height}`);
357
+ if (grid.x.length <= 1 || grid.y.length <= 1) {
358
+ for (let j = chartGrid.children.length - 1; j >= 0; j--) {
359
+ chartGrid.children.item(j).remove();
360
+ }
361
+ return;
362
+ }
363
+ let pgrid;
364
+ if (chartGrid.children.length > 0) {
365
+ pgrid = chartGrid.children.item(0);
366
+ }
367
+ else {
368
+ pgrid = document.createElementNS(SVG_NS, 'path');
369
+ chartGrid.appendChild(pgrid);
370
+ }
371
+ const tick = padding * .25;
372
+ let lblCounter = 0;
373
+ let ensureLabel = (index, x, y, txt) => {
374
+ const ndx = index + /* <path> is the first child element */ 1;
375
+ let lbl;
376
+ if (chartGrid.children.length <= ndx) {
377
+ lbl = document.createElementNS(SVG_NS, 'text');
378
+ chartGrid.appendChild(lbl);
379
+ }
380
+ else {
381
+ lbl = chartGrid.children.item(ndx);
382
+ }
383
+ lbl.textContent = txt;
384
+ lbl.setAttribute('x', x.toString());
385
+ lbl.setAttribute('y', y.toString());
386
+ return lbl;
387
+ };
388
+ let dgrid = `M${paddingYAxis},${padding} v${gridHeight}`; //H${w}
389
+ // x
390
+ let j = 0;
391
+ const xincr = seriesWidth / (grid.x.length - 1), yincr = gridHeight / (grid.y.length - 1);
392
+ if (this.xAxisPosition !== 'none') {
393
+ for (var x of grid.x) {
394
+ const xcoord = paddingYAxis + j * xincr;
395
+ if (this.xAxisPosition === 'top') {
396
+ dgrid += ` M${xcoord},${padding} v${-tick}`;
397
+ let lbl = ensureLabel(lblCounter++, xcoord, 0, x);
398
+ lbl.setAttribute('text-anchor', 'middle');
399
+ lbl.setAttribute('alignment-baseline', 'hanging');
400
+ }
401
+ else {
402
+ let ycoord = gridHeight + padding;
403
+ dgrid += ` M${xcoord},${ycoord} v${tick}`;
404
+ ensureLabel(lblCounter++, xcoord, ycoord + padding, x).setAttribute('text-anchor', 'middle');
405
+ }
406
+ j++;
407
+ }
408
+ }
409
+ // y
410
+ j = 0;
411
+ for (var y of grid.y) {
412
+ const ycoord = gridHeight + padding - j * yincr, xcoord = paddingYAxis - tick;
413
+ dgrid += ` M${xcoord},${ycoord} H${(seriesWidth + paddingYAxis)}`;
414
+ let txt = this.formatYAxisLabel(y);
415
+ ensureLabel(lblCounter++, xcoord - tick, ycoord, txt).setAttribute('text-anchor', 'end');
416
+ j++;
417
+ }
418
+ pgrid.setAttribute('d', dgrid);
419
+ // exceeding labels?
420
+ for (let j = chartGrid.children.length - 1; j > lblCounter; j--) {
421
+ chartGrid.children.item(j).remove();
422
+ }
423
+ // #endregion
424
+ // #region abscissa cursor
425
+ // #endregion
426
+ }
427
+ };
428
+ __decorate([
429
+ Watch({ emit: false, converter: PropertyConverters.String })
430
+ ], PacemChartElement.prototype, "type", void 0);
431
+ __decorate([
432
+ Watch({ converter: PropertyConverters.String })
433
+ ], PacemChartElement.prototype, "aspectRatio", void 0);
434
+ PacemChartElement = __decorate([
435
+ CustomElement({ tagName: P + '-chart' })
436
+ ], PacemChartElement);
437
+ export { PacemChartElement };
@@ -0,0 +1,4 @@
1
+ export * from './types';
2
+ export * from './generic';
3
+ export * from './column';
4
+ export * from './pie';
@@ -0,0 +1 @@
1
+ export * as Charts from './index-components-charts';
@@ -0,0 +1,3 @@
1
+ import { DeepMerger } from '@pacem/pacem-foundation';
2
+ import * as Output from './index';
3
+ DeepMerger.merge(Output);
@@ -0,0 +1 @@
1
+ export * as Components from './index-components';