@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,504 @@
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, Components, CustomUIEvent, CustomElementUtils, Utils, Throttle, PropertyChangeEventName } from '@pacem/pacem-core';
8
+ //namespace Pacem.Components.Charts {
9
+ export const MANAGED_EVENTS = ['keydown', 'keyup', 'click', 'dblclick', 'mouseover', 'mouseout', 'mouseenter', 'mouseleave', 'mousedown', 'mouseup', 'mousemove', 'contextmenu'];
10
+ ;
11
+ //function logN(N, x) {
12
+ // return Math.log10(x) / Math.log10(N);
13
+ //}
14
+ const MSECS_PER_DAY = 1000 * 60 * 60 * 24;
15
+ const MAX_X_LABELS = 10;
16
+ const MAX_Y_LABELS = 10;
17
+ const SERIES_DATAITEM = 'pacem:chart-series:dataitem';
18
+ const SERIES_SERIES = 'pacem:chart-series:series';
19
+ function getEvenlySpaced(items, type, min, max, formatOrLang, labels = MAX_X_LABELS) {
20
+ const length = (type === 'number' || type === 'date') ? (max - min) : Math.max(1, items.length - 1);
21
+ const gaps = labels - 1;
22
+ var retval = [];
23
+ var step = 1;
24
+ for (var l = gaps; l > 1; l--) {
25
+ if ((length % l) === 0) {
26
+ step = length / l;
27
+ break;
28
+ }
29
+ }
30
+ if (step === 1 /* coprime number? */ && length > labels) {
31
+ step = length;
32
+ }
33
+ const fnDate = typeof formatOrLang === 'function' ? formatOrLang : (step >= MSECS_PER_DAY ? ((d) => d.toLocaleDateString(formatOrLang)) : ((d) => d.toLocaleTimeString(formatOrLang)));
34
+ const fnDateFirst = typeof formatOrLang === 'function' ? formatOrLang : (step < MSECS_PER_DAY ? ((d) => d.toLocaleString(formatOrLang)) : fnDate);
35
+ const fnNum = typeof formatOrLang === 'function' ? formatOrLang : (n) => n.toLocaleString(formatOrLang);
36
+ for (var j = 0; j <= length; j += step) {
37
+ let label;
38
+ switch (type) {
39
+ case 'string':
40
+ if (j >= items.length) {
41
+ continue;
42
+ }
43
+ label = items[j].label;
44
+ break;
45
+ case 'number':
46
+ label = fnNum(min + j);
47
+ break;
48
+ case 'date':
49
+ let date = Utils.parseDate(min + j);
50
+ let fn = (j == 0) ? fnDateFirst : fnDate;
51
+ label = fn(date);
52
+ break;
53
+ default:
54
+ throw 'Not supported.';
55
+ }
56
+ retval.push(label);
57
+ }
58
+ return retval;
59
+ }
60
+ function getRoundedBoundaries(min, max, desiredSteps) {
61
+ desiredSteps ??= Math.min(bestGuessedDensity(min, max), MAX_Y_LABELS);
62
+ const roughStep = (max - min) / desiredSteps;
63
+ const magnitude = Math.pow(10, Math.floor(Math.log10(roughStep)));
64
+ const step = [1, 2, 5, 10].map(n => n * magnitude).filter(s => s >= roughStep).reduce((min, s) => Math.min(min, s), Number.POSITIVE_INFINITY);
65
+ return {
66
+ min: Math.floor(min / step) * step,
67
+ max: Math.ceil(max / step) * step,
68
+ round: step
69
+ };
70
+ }
71
+ // Deprecated
72
+ //function getRoundedBoundariesOld(a0: number, a1: number, steps?: number) {
73
+ // if (!(steps > 0)) {
74
+ // steps = bestGuessedDensity(a0, a1);
75
+ // }
76
+ // const magn = logN(steps, a1 - a0),
77
+ // exp = magn % 1 == 0 ? magn - 1 : magn,
78
+ // rounder = Math.pow(steps, Math.floor(exp));
79
+ // return {
80
+ // min: Math.floor(a0 / rounder) * rounder,
81
+ // max: Math.ceil(a1 / rounder) * rounder,
82
+ // round: rounder
83
+ // };
84
+ //}
85
+ function bestGuessedDensity(min, max) {
86
+ const delta = max - min;
87
+ if (!(delta > 0)) {
88
+ return 2;
89
+ }
90
+ const oom = Math.floor(Math.log10(delta));
91
+ const exp = Math.pow(10, -oom);
92
+ // bring everything to a single digit format
93
+ if (oom != 0) {
94
+ return bestGuessedDensity(min * exp, max * exp);
95
+ }
96
+ const capFn = (n) => {
97
+ const norm = Math.floor(Math.abs(n) * 10), limit = norm + 10 - (norm % 10);
98
+ return limit.roundoff();
99
+ };
100
+ const b = capFn(delta);
101
+ let a = 10;
102
+ if (min !== 0) {
103
+ a = capFn(Math.abs(max));
104
+ }
105
+ const ret = gcd(a, b);
106
+ return ret === 1 ? b : Math.max(ret, 10 - ret);
107
+ }
108
+ function gcd(a, b) {
109
+ if (a <= 0) {
110
+ return b;
111
+ }
112
+ return gcd(b % a, a);
113
+ }
114
+ export class ChartEvent extends CustomUIEvent {
115
+ constructor(type, eventInit, originalEvent) {
116
+ super(type, eventInit, originalEvent);
117
+ }
118
+ }
119
+ let PacemChartSeriesElement = class PacemChartSeriesElement extends Components.PacemItemElement {
120
+ get values() {
121
+ return this.datasource;
122
+ }
123
+ };
124
+ __decorate([
125
+ Watch({ emit: true, converter: PropertyConverters.Json })
126
+ ], PacemChartSeriesElement.prototype, "datasource", void 0);
127
+ __decorate([
128
+ Watch({ emit: true, converter: PropertyConverters.String })
129
+ ], PacemChartSeriesElement.prototype, "label", void 0);
130
+ __decorate([
131
+ Watch({ emit: true, converter: PropertyConverters.String })
132
+ ], PacemChartSeriesElement.prototype, "color", void 0);
133
+ __decorate([
134
+ Watch({ emit: true, converter: PropertyConverters.Boolean })
135
+ ], PacemChartSeriesElement.prototype, "datapoints", void 0);
136
+ __decorate([
137
+ Watch( /* to be set programmatically (internally) */)
138
+ ], PacemChartSeriesElement.prototype, "_uiElements", void 0);
139
+ PacemChartSeriesElement = __decorate([
140
+ CustomElement({ tagName: P + '-chart-series' })
141
+ ], PacemChartSeriesElement);
142
+ export { PacemChartSeriesElement };
143
+ const SVG_NS = "http://www.w3.org/2000/svg";
144
+ export class PacemSeriesChartElement extends Components.PacemItemsContainerElement {
145
+ constructor() {
146
+ super();
147
+ this._itemPropertyChangedCallback = (evt) => {
148
+ const propName = evt.detail.propertyName;
149
+ if (propName === 'datasource'
150
+ || propName === 'label'
151
+ || propName === 'datapoints'
152
+ || propName === 'cssClass'
153
+ || propName === 'color') {
154
+ this.draw();
155
+ }
156
+ };
157
+ this._series = [];
158
+ this._resizeHandler = (evt) => {
159
+ this._size = evt.detail;
160
+ this.draw();
161
+ };
162
+ // #endregion
163
+ this._broadcastHandler = (e) => {
164
+ /* Don't mess with the events:
165
+ * keep the original (trusted) events with their well-known names
166
+ * and provide a method to retrieve the 'ChartDataItem' based on the the eventTarget. */
167
+ const element = e.currentTarget;
168
+ const dataItem = CustomElementUtils.getAttachedPropertyValue(element, SERIES_DATAITEM);
169
+ if (Utils.isNull(dataItem)) {
170
+ return;
171
+ }
172
+ const chart = this, anchorPoint = chart.getAnchorPoint(element);
173
+ const evt = new ChartEvent('item' + e.type, { detail: { dataItem, anchorPoint } }, e);
174
+ const series = CustomElementUtils.getAttachedPropertyValue(element, SERIES_SERIES);
175
+ if (series instanceof PacemChartSeriesElement) {
176
+ series.dispatchEvent(evt);
177
+ }
178
+ chart.dispatchEvent(evt);
179
+ };
180
+ this._key = Utils.uniqueCode();
181
+ }
182
+ validate(item) {
183
+ return item instanceof PacemChartSeriesElement;
184
+ }
185
+ register(item) {
186
+ if (super.register(item)) {
187
+ item.addEventListener(PropertyChangeEventName, this._itemPropertyChangedCallback, false);
188
+ return true;
189
+ }
190
+ return false;
191
+ }
192
+ unregister(item) {
193
+ if (super.unregister(item)) {
194
+ item.removeEventListener(PropertyChangeEventName, this._itemPropertyChangedCallback, false);
195
+ return true;
196
+ }
197
+ return false;
198
+ }
199
+ propertyChangedCallback(name, old, val, first) {
200
+ super.propertyChangedCallback(name, old, val, first);
201
+ if (name === 'target') {
202
+ if (this._div != old) {
203
+ this._div && this._div.remove();
204
+ }
205
+ // reset
206
+ this._div = null;
207
+ this._body = null; // <- get checked on next draw
208
+ const eold = old;
209
+ if (eold) {
210
+ eold.classList.remove(PCSS + '-chart-area');
211
+ eold.innerHTML = '';
212
+ }
213
+ this._ensureResizer();
214
+ this.draw();
215
+ }
216
+ else if (!first) {
217
+ switch (name) {
218
+ case 'items':
219
+ this._databindAndDrawDebounced();
220
+ break;
221
+ case 'datasource':
222
+ this._databind();
223
+ // fall down to draw() call.
224
+ case 'yAxisDensity':
225
+ case 'xAxisDensity':
226
+ case 'xAxisType':
227
+ case 'yAxisMax':
228
+ case 'xAxisPosition':
229
+ case 'yAxisFormat':
230
+ this.draw();
231
+ break;
232
+ }
233
+ }
234
+ }
235
+ _databindAndDrawDebounced() {
236
+ cancelAnimationFrame(this._handle);
237
+ this._handle = requestAnimationFrame(() => {
238
+ this._databind();
239
+ this.draw();
240
+ });
241
+ }
242
+ _databind() {
243
+ this._datasource = Utils.isNullOrEmpty(this.datasource) ? this.items : this.datasource;
244
+ }
245
+ disconnectedCallback() {
246
+ this._div && this._div.remove();
247
+ const resizer = this._resizer;
248
+ if (!Utils.isNull(resizer)) {
249
+ resizer.removeEventListener(Components.ResizeEventName, this._resizeHandler, false);
250
+ resizer.remove();
251
+ this._resizer = null;
252
+ }
253
+ super.disconnectedCallback();
254
+ }
255
+ viewActivatedCallback() {
256
+ super.viewActivatedCallback();
257
+ this._ensureResizer();
258
+ this._databind();
259
+ this.draw();
260
+ }
261
+ _ensureResizer(target = this.target || this._div) {
262
+ if (Utils.isNull(this._resizer)) {
263
+ const shell = CustomElementUtils.findAncestorShell(this);
264
+ const resizer = this._resizer = shell.appendChild(new Components.PacemResizeElement());
265
+ resizer.addEventListener(Components.ResizeEventName, this._resizeHandler, false);
266
+ }
267
+ this._resizer.target = target;
268
+ }
269
+ // #region OO-scoped props
270
+ get chartSize() {
271
+ return this._size;
272
+ }
273
+ get chartSeries() {
274
+ return this._series;
275
+ }
276
+ get chartBody() {
277
+ return this._body;
278
+ }
279
+ get chartGrid() {
280
+ return this._grid;
281
+ }
282
+ get chartMask() {
283
+ return this._mask;
284
+ }
285
+ get chartKey() {
286
+ return this._key;
287
+ }
288
+ get chartContainer() {
289
+ return this._div;
290
+ }
291
+ assignUiBehaviors(ui, series, data, color) {
292
+ const alreadyRegistered = !Utils.isNull(CustomElementUtils.getAttachedPropertyValue(ui, SERIES_DATAITEM));
293
+ const item = Utils.extend({ series: series.label, color: color || series.color }, data);
294
+ CustomElementUtils.setAttachedPropertyValue(ui, SERIES_DATAITEM, item);
295
+ CustomElementUtils.setAttachedPropertyValue(ui, SERIES_SERIES, series);
296
+ if (!alreadyRegistered) {
297
+ MANAGED_EVENTS.forEach(type => {
298
+ ui.addEventListener(type, this._broadcastHandler);
299
+ });
300
+ }
301
+ }
302
+ disposeUiBehaviors(ui) {
303
+ CustomElementUtils.deleteAttachedPropertyValue(ui, SERIES_DATAITEM);
304
+ CustomElementUtils.deleteAttachedPropertyValue(ui, SERIES_SERIES);
305
+ MANAGED_EVENTS.forEach(type => {
306
+ ui.removeEventListener(type, this._broadcastHandler);
307
+ });
308
+ }
309
+ ensureChartContainer() {
310
+ if (Utils.isNull(this._div)) {
311
+ let div = this._div = this.target || document.createElement('div');
312
+ div.classList.add(PCSS + '-chart-area');
313
+ if (div != this.target) {
314
+ this.parentElement.insertBefore(div, this);
315
+ }
316
+ }
317
+ return this._div;
318
+ }
319
+ ensureChartBody(type, w, h) {
320
+ if (Utils.isNull(this._body)) {
321
+ let svg = this._body = document.createElementNS(SVG_NS, 'svg');
322
+ svg.setAttribute('pacem', '');
323
+ svg.setAttribute('class', PCSS + '-' + type + '-chart');
324
+ svg.setAttribute('preserveAspectRatio', 'xMinYMax slice');
325
+ const g = this._grid = document.createElementNS(SVG_NS, 'svg');
326
+ g.setAttribute('pacem', '');
327
+ g.setAttribute('class', 'chart-grid');
328
+ // put mask on series
329
+ let defs = document.createElementNS(SVG_NS, 'defs');
330
+ let mask = this._mask = document.createElementNS(SVG_NS, 'mask');
331
+ // black (hides)
332
+ let rect = document.createElementNS(SVG_NS, 'rect');
333
+ rect.setAttribute('x', '0');
334
+ rect.setAttribute('y', '0');
335
+ rect.setAttribute('width', '100%');
336
+ rect.setAttribute('height', '100%');
337
+ // white (shows)
338
+ let rect0 = document.createElementNS(SVG_NS, 'rect');
339
+ rect0.setAttribute('y', '0');
340
+ rect0.setAttribute('width', '100%');
341
+ rect0.setAttribute('fill', '#fff');
342
+ mask.id = 'gnrc_mask_' + this._key;
343
+ mask.appendChild(rect);
344
+ mask.appendChild(rect0);
345
+ // place mask on series so that lines won't be rendered outside the grid borders
346
+ //g2.setAttribute('mask', 'url(#' + mask.id + ')');
347
+ defs.appendChild(mask);
348
+ svg.appendChild(defs);
349
+ svg.appendChild(g);
350
+ this._div.appendChild(svg);
351
+ }
352
+ return this._body;
353
+ }
354
+ chartDataItemToPoint(input, source) {
355
+ switch (this.xAxisType || 'string') {
356
+ case 'number':
357
+ return { x: parseFloat(input.label), y: input.value };
358
+ case 'date':
359
+ return { x: Utils.parseDate(input.label).valueOf(), y: input.value };
360
+ case 'string':
361
+ return { x: source.findIndex(e => e.label == input.label), y: input.value };
362
+ default:
363
+ throw 'Not supported.';
364
+ }
365
+ }
366
+ getVirtualGrid(items, minX, maxX, minY, maxY, xAxisType = this.xAxisType, steps, labels) {
367
+ const rangeY = getRoundedBoundaries(minY, maxY, steps), stepY = rangeY.round;
368
+ // fill y-axis array
369
+ var y = [];
370
+ for (let j = rangeY.min; j <= rangeY.max; j += stepY) {
371
+ y.push(j.roundoff());
372
+ }
373
+ // fill x-axis array;
374
+ let lang = Utils.lang(this);
375
+ let format = null;
376
+ const intl = this.xAxisFormat;
377
+ if (!Utils.isNullOrEmpty(intl)) {
378
+ switch (xAxisType) {
379
+ case 'number':
380
+ format = (n) => Intl.NumberFormat(lang, intl).format(n);
381
+ break;
382
+ case 'date':
383
+ format = (n) => Intl.DateTimeFormat(lang, intl).format(Utils.parseDate(n));
384
+ break;
385
+ }
386
+ }
387
+ const x = getEvenlySpaced(items, xAxisType, minX, maxX, format ?? lang, labels);
388
+ return { x, y };
389
+ }
390
+ wipeOut(series = this._series, startIndex = 0) {
391
+ for (var j = series.length - 1; j >= startIndex; j--) {
392
+ series[j].remove();
393
+ }
394
+ series.splice(startIndex);
395
+ }
396
+ buildLinearGradient(seriesIndex, invert = false) {
397
+ const grad = document.createElementNS(SVG_NS, 'linearGradient');
398
+ grad.id = this.chartKey + '_grad' + seriesIndex + (invert ? '_inverted' : '');
399
+ if (invert) {
400
+ Utils.addClass(grad, 'bottom-up');
401
+ }
402
+ grad.setAttribute('x1', '0%');
403
+ grad.setAttribute('x2', '0%');
404
+ grad.setAttribute('y1', '0%');
405
+ grad.setAttribute('y2', '100%');
406
+ grad.setAttribute('spreadMethod', 'pad');
407
+ const stop1 = document.createElementNS(SVG_NS, 'stop');
408
+ stop1.setAttribute('offset', '0%');
409
+ const stop2 = document.createElementNS(SVG_NS, 'stop');
410
+ stop2.setAttribute('offset', '100%');
411
+ grad.appendChild(stop1);
412
+ grad.appendChild(stop2);
413
+ return grad;
414
+ }
415
+ setGradientColor(grad, color) {
416
+ for (let j = 0; j < grad.children.length; j++) {
417
+ const stop = grad.children.item(j);
418
+ if (stop instanceof SVGStopElement) {
419
+ stop.style.stopColor = color;
420
+ }
421
+ }
422
+ }
423
+ estimateYAxisLabelWidth(max) {
424
+ const txt = this.formatYAxisLabel(max);
425
+ return this.estimateLabelWidth(txt);
426
+ }
427
+ estimateLabelWidth(txt) {
428
+ const grid = this._grid;
429
+ if (Utils.isNull(grid)) {
430
+ throw new Error('Unable to estimate label without an underlying grid available.');
431
+ }
432
+ const lbl = document.createElementNS(SVG_NS, 'text');
433
+ lbl.textContent = txt;
434
+ grid.appendChild(lbl);
435
+ const size = Utils.offset(lbl);
436
+ lbl.remove();
437
+ return size.width;
438
+ }
439
+ formatYAxisLabel(y) {
440
+ const intl = this.yAxisFormat;
441
+ return Utils.isNullOrEmpty(intl) ? y.toString() : Intl.NumberFormat(Utils.lang(this), intl).format(y);
442
+ }
443
+ estimateXAxisLabelWidth(max) {
444
+ const txt = this.formatXAxisLabel(max);
445
+ return this.estimateLabelWidth(txt);
446
+ }
447
+ formatXAxisLabel(x) {
448
+ const intl = this.xAxisFormat, lang = Utils.lang(this);
449
+ switch (this.xAxisType) {
450
+ case 'date':
451
+ const date = Utils.Dates.parse(x);
452
+ return Utils.isNullOrEmpty(intl) ? date.toLocaleString(lang) : Intl.DateTimeFormat(lang, intl).format(date);
453
+ case 'number':
454
+ const num = x;
455
+ return Utils.isNullOrEmpty(intl) ? num.toLocaleString(lang) : Intl.NumberFormat(lang, intl).format(num);
456
+ default:
457
+ return x;
458
+ }
459
+ }
460
+ draw() {
461
+ this.drawSeries(this._datasource);
462
+ }
463
+ /**
464
+ * Default implementation.
465
+ * @param element
466
+ */
467
+ getAnchorPoint(element) {
468
+ const rect = Utils.offset(element);
469
+ return { x: rect.left + rect.width * .5, y: rect.top + rect.height * .5 };
470
+ }
471
+ }
472
+ __decorate([
473
+ Watch({ converter: PropertyConverters.Element })
474
+ ], PacemSeriesChartElement.prototype, "target", void 0);
475
+ __decorate([
476
+ Watch({ emit: false, converter: PropertyConverters.String })
477
+ ], PacemSeriesChartElement.prototype, "xAxisType", void 0);
478
+ __decorate([
479
+ Watch({ emit: false, converter: PropertyConverters.String })
480
+ ], PacemSeriesChartElement.prototype, "xAxisPosition", void 0);
481
+ __decorate([
482
+ Watch({ emit: false, converter: PropertyConverters.Json })
483
+ ], PacemSeriesChartElement.prototype, "datasource", void 0);
484
+ __decorate([
485
+ Watch({ emit: false, converter: PropertyConverters.Number })
486
+ ], PacemSeriesChartElement.prototype, "yAxisDensity", void 0);
487
+ __decorate([
488
+ Watch({ emit: false, converter: PropertyConverters.Number })
489
+ ], PacemSeriesChartElement.prototype, "xAxisDensity", void 0);
490
+ __decorate([
491
+ Watch({ emit: false, converter: PropertyConverters.Number })
492
+ ], PacemSeriesChartElement.prototype, "yAxisMin", void 0);
493
+ __decorate([
494
+ Watch({ emit: false, converter: PropertyConverters.Number })
495
+ ], PacemSeriesChartElement.prototype, "yAxisMax", void 0);
496
+ __decorate([
497
+ Watch({ emit: false, converter: PropertyConverters.Json })
498
+ ], PacemSeriesChartElement.prototype, "yAxisFormat", void 0);
499
+ __decorate([
500
+ Watch({ emit: false, converter: PropertyConverters.Json })
501
+ ], PacemSeriesChartElement.prototype, "xAxisFormat", void 0);
502
+ __decorate([
503
+ Throttle(true)
504
+ ], PacemSeriesChartElement.prototype, "draw", null);
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@pacem/pacem-charts",
3
+ "files": [
4
+ "dist/browser/*.*",
5
+ "dist/bundle/*.*",
6
+ "dist/esm/*.*",
7
+ "typings/*.*",
8
+ "README.md",
9
+ "LICENSE",
10
+ "NOTICE",
11
+ "package.json"
12
+ ],
13
+ "type": "module",
14
+ "dependencies": {
15
+ "@pacem/pacem-foundation": "*",
16
+ "@pacem/pacem-core": "*"
17
+ },
18
+ "main": "./dist/esm/index.js",
19
+ "module": "./dist/esm/index.js",
20
+ "typings": "./typings/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "import": "./dist/esm/index.js",
24
+ "types": "./typings/index.d.ts"
25
+ }
26
+ },
27
+ "version": "1.0.0-abel",
28
+ "author": {
29
+ "name": "Pacem",
30
+ "url": "https://pacem.it"
31
+ },
32
+ "license": "Apache-2.0",
33
+ "homepage": "https://js.pacem.it",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/pacem-it/pacem.git"
37
+ }
38
+ }