UNPKG

98.4 kBJavaScriptView Raw
1function $(expr, con) {
2 return typeof expr === "string"? (con || document).querySelector(expr) : expr || null;
3}
4
5
6
7$.create = (tag, o) => {
8 var element = document.createElement(tag);
9
10 for (var i in o) {
11 var val = o[i];
12
13 if (i === "inside") {
14 $(val).appendChild(element);
15 }
16 else if (i === "around") {
17 var ref = $(val);
18 ref.parentNode.insertBefore(element, ref);
19 element.appendChild(ref);
20
21 } else if (i === "styles") {
22 if(typeof val === "object") {
23 Object.keys(val).map(prop => {
24 element.style[prop] = val[prop];
25 });
26 }
27 } else if (i in element ) {
28 element[i] = val;
29 }
30 else {
31 element.setAttribute(i, val);
32 }
33 }
34
35 return element;
36};
37
38function getOffset(element) {
39 let rect = element.getBoundingClientRect();
40 return {
41 // https://stackoverflow.com/a/7436602/6495043
42 // rect.top varies with scroll, so we add whatever has been
43 // scrolled to it to get absolute distance from actual page top
44 top: rect.top + (document.documentElement.scrollTop || document.body.scrollTop),
45 left: rect.left + (document.documentElement.scrollLeft || document.body.scrollLeft)
46 };
47}
48
49function isElementInViewport(el) {
50 // Although straightforward: https://stackoverflow.com/a/7557433/6495043
51 var rect = el.getBoundingClientRect();
52
53 return (
54 rect.top >= 0 &&
55 rect.left >= 0 &&
56 rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
57 rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
58 );
59}
60
61function getElementContentWidth(element) {
62 var styles = window.getComputedStyle(element);
63 var padding = parseFloat(styles.paddingLeft) +
64 parseFloat(styles.paddingRight);
65
66 return element.clientWidth - padding;
67}
68
69
70
71
72
73function fire(target, type, properties) {
74 var evt = document.createEvent("HTMLEvents");
75
76 evt.initEvent(type, true, true );
77
78 for (var j in properties) {
79 evt[j] = properties[j];
80 }
81
82 return target.dispatchEvent(evt);
83}
84
85// https://css-tricks.com/snippets/javascript/loop-queryselectorall-matches/
86
87const BASE_MEASURES = {
88 margins: {
89 top: 10,
90 bottom: 10,
91 left: 20,
92 right: 20
93 },
94 paddings: {
95 top: 20,
96 bottom: 40,
97 left: 30,
98 right: 10
99 },
100
101 baseHeight: 240,
102 titleHeight: 20,
103 legendHeight: 30,
104
105 titleFontSize: 12,
106};
107
108function getTopOffset(m) {
109 return m.titleHeight + m.margins.top + m.paddings.top;
110}
111
112function getLeftOffset(m) {
113 return m.margins.left + m.paddings.left;
114}
115
116function getExtraHeight(m) {
117 let totalExtraHeight = m.margins.top + m.margins.bottom
118 + m.paddings.top + m.paddings.bottom
119 + m.titleHeight + m.legendHeight;
120 return totalExtraHeight;
121}
122
123function getExtraWidth(m) {
124 let totalExtraWidth = m.margins.left + m.margins.right
125 + m.paddings.left + m.paddings.right;
126
127 return totalExtraWidth;
128}
129
130const INIT_CHART_UPDATE_TIMEOUT = 700;
131const CHART_POST_ANIMATE_TIMEOUT = 400;
132
133const DEFAULT_AXIS_CHART_TYPE = 'line';
134const AXIS_DATASET_CHART_TYPES = ['line', 'bar'];
135
136const AXIS_LEGEND_BAR_SIZE = 100;
137
138const BAR_CHART_SPACE_RATIO = 0.5;
139const MIN_BAR_PERCENT_HEIGHT = 0.00;
140
141const LINE_CHART_DOT_SIZE = 4;
142const DOT_OVERLAY_SIZE_INCR = 4;
143
144const PERCENTAGE_BAR_DEFAULT_HEIGHT = 20;
145const PERCENTAGE_BAR_DEFAULT_DEPTH = 2;
146
147// Fixed 5-color theme,
148// More colors are difficult to parse visually
149const HEATMAP_DISTRIBUTION_SIZE = 5;
150
151const HEATMAP_SQUARE_SIZE = 10;
152const HEATMAP_GUTTER_SIZE = 2;
153
154const DEFAULT_CHAR_WIDTH = 7;
155
156const TOOLTIP_POINTER_TRIANGLE_HEIGHT = 5;
157
158const DEFAULT_CHART_COLORS = ['light-blue', 'blue', 'violet', 'red', 'orange',
159 'yellow', 'green', 'light-green', 'purple', 'magenta', 'light-grey', 'dark-grey'];
160const HEATMAP_COLORS_GREEN = ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'];
161
162
163
164const DEFAULT_COLORS = {
165 bar: DEFAULT_CHART_COLORS,
166 line: DEFAULT_CHART_COLORS,
167 pie: DEFAULT_CHART_COLORS,
168 percentage: DEFAULT_CHART_COLORS,
169 heatmap: HEATMAP_COLORS_GREEN,
170 donut: DEFAULT_CHART_COLORS
171};
172
173// Universal constants
174const ANGLE_RATIO = Math.PI / 180;
175const FULL_ANGLE = 360;
176
177class SvgTip {
178 constructor({
179 parent = null,
180 colors = []
181 }) {
182 this.parent = parent;
183 this.colors = colors;
184 this.titleName = '';
185 this.titleValue = '';
186 this.listValues = [];
187 this.titleValueFirst = 0;
188
189 this.x = 0;
190 this.y = 0;
191
192 this.top = 0;
193 this.left = 0;
194
195 this.setup();
196 }
197
198 setup() {
199 this.makeTooltip();
200 }
201
202 refresh() {
203 this.fill();
204 this.calcPosition();
205 }
206
207 makeTooltip() {
208 this.container = $.create('div', {
209 inside: this.parent,
210 className: 'graph-svg-tip comparison',
211 innerHTML: `<span class="title"></span>
212 <ul class="data-point-list"></ul>
213 <div class="svg-pointer"></div>`
214 });
215 this.hideTip();
216
217 this.title = this.container.querySelector('.title');
218 this.dataPointList = this.container.querySelector('.data-point-list');
219
220 this.parent.addEventListener('mouseleave', () => {
221 this.hideTip();
222 });
223 }
224
225 fill() {
226 let title;
227 if(this.index) {
228 this.container.setAttribute('data-point-index', this.index);
229 }
230 if(this.titleValueFirst) {
231 title = `<strong>${this.titleValue}</strong>${this.titleName}`;
232 } else {
233 title = `${this.titleName}<strong>${this.titleValue}</strong>`;
234 }
235 this.title.innerHTML = title;
236 this.dataPointList.innerHTML = '';
237
238 this.listValues.map((set, i) => {
239 const color = this.colors[i] || 'black';
240 let value = set.formatted === 0 || set.formatted ? set.formatted : set.value;
241
242 let li = $.create('li', {
243 styles: {
244 'border-top': `3px solid ${color}`
245 },
246 innerHTML: `<strong style="display: block;">${ value === 0 || value ? value : '' }</strong>
247 ${set.title ? set.title : '' }`
248 });
249
250 this.dataPointList.appendChild(li);
251 });
252 }
253
254 calcPosition() {
255 let width = this.container.offsetWidth;
256
257 this.top = this.y - this.container.offsetHeight
258 - TOOLTIP_POINTER_TRIANGLE_HEIGHT;
259 this.left = this.x - width/2;
260 let maxLeft = this.parent.offsetWidth - width;
261
262 let pointer = this.container.querySelector('.svg-pointer');
263
264 if(this.left < 0) {
265 pointer.style.left = `calc(50% - ${-1 * this.left}px)`;
266 this.left = 0;
267 } else if(this.left > maxLeft) {
268 let delta = this.left - maxLeft;
269 let pointerOffset = `calc(50% + ${delta}px)`;
270 pointer.style.left = pointerOffset;
271
272 this.left = maxLeft;
273 } else {
274 pointer.style.left = `50%`;
275 }
276 }
277
278 setValues(x, y, title = {}, listValues = [], index = -1) {
279 this.titleName = title.name;
280 this.titleValue = title.value;
281 this.listValues = listValues;
282 this.x = x;
283 this.y = y;
284 this.titleValueFirst = title.valueFirst || 0;
285 this.index = index;
286 this.refresh();
287 }
288
289 hideTip() {
290 this.container.style.top = '0px';
291 this.container.style.left = '0px';
292 this.container.style.opacity = '0';
293 }
294
295 showTip() {
296 this.container.style.top = this.top + 'px';
297 this.container.style.left = this.left + 'px';
298 this.container.style.opacity = '1';
299 }
300}
301
302/**
303 * Returns the value of a number upto 2 decimal places.
304 * @param {Number} d Any number
305 */
306function floatTwo(d) {
307 return parseFloat(d.toFixed(2));
308}
309
310/**
311 * Returns whether or not two given arrays are equal.
312 * @param {Array} arr1 First array
313 * @param {Array} arr2 Second array
314 */
315
316
317/**
318 * Shuffles array in place. ES6 version
319 * @param {Array} array An array containing the items.
320 */
321
322
323/**
324 * Fill an array with extra points
325 * @param {Array} array Array
326 * @param {Number} count number of filler elements
327 * @param {Object} element element to fill with
328 * @param {Boolean} start fill at start?
329 */
330function fillArray(array, count, element, start=false) {
331 if(!element) {
332 element = start ? array[0] : array[array.length - 1];
333 }
334 let fillerArray = new Array(Math.abs(count)).fill(element);
335 array = start ? fillerArray.concat(array) : array.concat(fillerArray);
336 return array;
337}
338
339/**
340 * Returns pixel width of string.
341 * @param {String} string
342 * @param {Number} charWidth Width of single char in pixels
343 */
344function getStringWidth(string, charWidth) {
345 return (string+"").length * charWidth;
346}
347
348
349
350// https://stackoverflow.com/a/29325222
351
352
353function getPositionByAngle(angle, radius) {
354 return {
355 x: Math.sin(angle * ANGLE_RATIO) * radius,
356 y: Math.cos(angle * ANGLE_RATIO) * radius,
357 };
358}
359
360function getBarHeightAndYAttr(yTop, zeroLine) {
361 let height, y;
362 if (yTop <= zeroLine) {
363 height = zeroLine - yTop;
364 y = yTop;
365 } else {
366 height = yTop - zeroLine;
367 y = zeroLine;
368 }
369
370 return [height, y];
371}
372
373function equilizeNoOfElements(array1, array2,
374 extraCount = array2.length - array1.length) {
375
376 // Doesn't work if either has zero elements.
377 if(extraCount > 0) {
378 array1 = fillArray(array1, extraCount);
379 } else {
380 array2 = fillArray(array2, extraCount);
381 }
382 return [array1, array2];
383}
384
385function truncateString(txt, len) {
386 if (!txt) {
387 return;
388 }
389 if (txt.length > len) {
390 return txt.slice(0, len-3) + '...';
391 } else {
392 return txt;
393 }
394}
395
396function shortenLargeNumber(label) {
397 let number;
398 if (typeof label === 'number') number = label;
399 else if (typeof label === 'string') {
400 number = Number(label);
401 if (Number.isNaN(number)) return label;
402 }
403
404 // Using absolute since log wont work for negative numbers
405 let p = Math.floor(Math.log10(Math.abs(number)));
406 if (p <= 2) return number; // Return as is for a 3 digit number of less
407 let l = Math.floor(p / 3);
408 let shortened = (Math.pow(10, p - l * 3) * +(number / Math.pow(10, p)).toFixed(1));
409
410 // Correct for floating point error upto 2 decimal places
411 return Math.round(shortened*100)/100 + ' ' + ['', 'K', 'M', 'B', 'T'][l];
412}
413
414// cubic bezier curve calculation (from example by François Romain)
415function getSplineCurvePointsStr(xList, yList) {
416
417 let points=[];
418 for(let i=0;i<xList.length;i++){
419 points.push([xList[i], yList[i]]);
420 }
421
422 let smoothing = 0.2;
423 let line = (pointA, pointB) => {
424 let lengthX = pointB[0] - pointA[0];
425 let lengthY = pointB[1] - pointA[1];
426 return {
427 length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),
428 angle: Math.atan2(lengthY, lengthX)
429 };
430 };
431
432 let controlPoint = (current, previous, next, reverse) => {
433 let p = previous || current;
434 let n = next || current;
435 let o = line(p, n);
436 let angle = o.angle + (reverse ? Math.PI : 0);
437 let length = o.length * smoothing;
438 let x = current[0] + Math.cos(angle) * length;
439 let y = current[1] + Math.sin(angle) * length;
440 return [x, y];
441 };
442
443 let bezierCommand = (point, i, a) => {
444 let cps = controlPoint(a[i - 1], a[i - 2], point);
445 let cpe = controlPoint(point, a[i - 1], a[i + 1], true);
446 return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}`;
447 };
448
449 let pointStr = (points, command) => {
450 return points.reduce((acc, point, i, a) => i === 0
451 ? `${point[0]},${point[1]}`
452 : `${acc} ${command(point, i, a)}`, '');
453 };
454
455 return pointStr(points, bezierCommand);
456}
457
458const PRESET_COLOR_MAP = {
459 'light-blue': '#7cd6fd',
460 'blue': '#5e64ff',
461 'violet': '#743ee2',
462 'red': '#ff5858',
463 'orange': '#ffa00a',
464 'yellow': '#feef72',
465 'green': '#28a745',
466 'light-green': '#98d85b',
467 'purple': '#b554ff',
468 'magenta': '#ffa3ef',
469 'black': '#36114C',
470 'grey': '#bdd3e6',
471 'light-grey': '#f0f4f7',
472 'dark-grey': '#b8c2cc'
473};
474
475function limitColor(r){
476 if (r > 255) return 255;
477 else if (r < 0) return 0;
478 return r;
479}
480
481function lightenDarkenColor(color, amt) {
482 let col = getColor(color);
483 let usePound = false;
484 if (col[0] == "#") {
485 col = col.slice(1);
486 usePound = true;
487 }
488 let num = parseInt(col,16);
489 let r = limitColor((num >> 16) + amt);
490 let b = limitColor(((num >> 8) & 0x00FF) + amt);
491 let g = limitColor((num & 0x0000FF) + amt);
492 return (usePound?"#":"") + (g | (b << 8) | (r << 16)).toString(16);
493}
494
495function isValidColor(string) {
496 // https://stackoverflow.com/a/8027444/6495043
497 return /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(string);
498}
499
500const getColor = (color) => {
501 return PRESET_COLOR_MAP[color] || color;
502};
503
504const AXIS_TICK_LENGTH = 6;
505const LABEL_MARGIN = 4;
506const LABEL_MAX_CHARS = 15;
507const FONT_SIZE = 10;
508const BASE_LINE_COLOR = '#dadada';
509const FONT_FILL = '#555b51';
510
511function $$1(expr, con) {
512 return typeof expr === "string"? (con || document).querySelector(expr) : expr || null;
513}
514
515function createSVG(tag, o) {
516 var element = document.createElementNS("http://www.w3.org/2000/svg", tag);
517
518 for (var i in o) {
519 var val = o[i];
520
521 if (i === "inside") {
522 $$1(val).appendChild(element);
523 }
524 else if (i === "around") {
525 var ref = $$1(val);
526 ref.parentNode.insertBefore(element, ref);
527 element.appendChild(ref);
528
529 } else if (i === "styles") {
530 if(typeof val === "object") {
531 Object.keys(val).map(prop => {
532 element.style[prop] = val[prop];
533 });
534 }
535 } else {
536 if(i === "className") { i = "class"; }
537 if(i === "innerHTML") {
538 element['textContent'] = val;
539 } else {
540 element.setAttribute(i, val);
541 }
542 }
543 }
544
545 return element;
546}
547
548function renderVerticalGradient(svgDefElem, gradientId) {
549 return createSVG('linearGradient', {
550 inside: svgDefElem,
551 id: gradientId,
552 x1: 0,
553 x2: 0,
554 y1: 0,
555 y2: 1
556 });
557}
558
559function setGradientStop(gradElem, offset, color, opacity) {
560 return createSVG('stop', {
561 'inside': gradElem,
562 'style': `stop-color: ${color}`,
563 'offset': offset,
564 'stop-opacity': opacity
565 });
566}
567
568function makeSVGContainer(parent, className, width, height) {
569 return createSVG('svg', {
570 className: className,
571 inside: parent,
572 width: width,
573 height: height
574 });
575}
576
577function makeSVGDefs(svgContainer) {
578 return createSVG('defs', {
579 inside: svgContainer,
580 });
581}
582
583function makeSVGGroup(className, transform='', parent=undefined) {
584 let args = {
585 className: className,
586 transform: transform
587 };
588 if(parent) args.inside = parent;
589 return createSVG('g', args);
590}
591
592
593
594function makePath(pathStr, className='', stroke='none', fill='none', strokeWidth=2) {
595 return createSVG('path', {
596 className: className,
597 d: pathStr,
598 styles: {
599 stroke: stroke,
600 fill: fill,
601 'stroke-width': strokeWidth
602 }
603 });
604}
605
606function makeArcPathStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
607 let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
608 let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y];
609 return `M${center.x} ${center.y}
610 L${arcStartX} ${arcStartY}
611 A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
612 ${arcEndX} ${arcEndY} z`;
613}
614
615function makeCircleStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
616 let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
617 let [arcEndX, midArc, arcEndY] = [center.x + endPosition.x, center.y * 2, center.y + endPosition.y];
618 return `M${center.x} ${center.y}
619 L${arcStartX} ${arcStartY}
620 A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
621 ${arcEndX} ${midArc} z
622 L${arcStartX} ${midArc}
623 A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
624 ${arcEndX} ${arcEndY} z`;
625}
626
627function makeArcStrokePathStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
628 let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
629 let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y];
630
631 return `M${arcStartX} ${arcStartY}
632 A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
633 ${arcEndX} ${arcEndY}`;
634}
635
636function makeStrokeCircleStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){
637 let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
638 let [arcEndX, midArc, arcEndY] = [center.x + endPosition.x, radius * 2 + arcStartY, center.y + startPosition.y];
639
640 return `M${arcStartX} ${arcStartY}
641 A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
642 ${arcEndX} ${midArc}
643 M${arcStartX} ${midArc}
644 A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
645 ${arcEndX} ${arcEndY}`;
646}
647
648function makeGradient(svgDefElem, color, lighter = false) {
649 let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default');
650 let gradientDef = renderVerticalGradient(svgDefElem, gradientId);
651 let opacities = [1, 0.6, 0.2];
652 if(lighter) {
653 opacities = [0.4, 0.2, 0];
654 }
655
656 setGradientStop(gradientDef, "0%", color, opacities[0]);
657 setGradientStop(gradientDef, "50%", color, opacities[1]);
658 setGradientStop(gradientDef, "100%", color, opacities[2]);
659
660 return gradientId;
661}
662
663function percentageBar(x, y, width, height,
664 depth=PERCENTAGE_BAR_DEFAULT_DEPTH, fill='none') {
665
666 let args = {
667 className: 'percentage-bar',
668 x: x,
669 y: y,
670 width: width,
671 height: height,
672 fill: fill,
673 styles: {
674 'stroke': lightenDarkenColor(fill, -25),
675 // Diabolically good: https://stackoverflow.com/a/9000859
676 // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray
677 'stroke-dasharray': `0, ${height + width}, ${width}, ${height}`,
678 'stroke-width': depth
679 },
680 };
681
682 return createSVG("rect", args);
683}
684
685function heatSquare(className, x, y, size, fill='none', data={}) {
686 let args = {
687 className: className,
688 x: x,
689 y: y,
690 width: size,
691 height: size,
692 fill: fill
693 };
694
695 Object.keys(data).map(key => {
696 args[key] = data[key];
697 });
698
699 return createSVG("rect", args);
700}
701
702function legendBar(x, y, size, fill='none', label, truncate=false) {
703 label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label;
704
705 let args = {
706 className: 'legend-bar',
707 x: 0,
708 y: 0,
709 width: size,
710 height: '2px',
711 fill: fill
712 };
713 let text = createSVG('text', {
714 className: 'legend-dataset-text',
715 x: 0,
716 y: 0,
717 dy: (FONT_SIZE * 2) + 'px',
718 'font-size': (FONT_SIZE * 1.2) + 'px',
719 'text-anchor': 'start',
720 fill: FONT_FILL,
721 innerHTML: label
722 });
723
724 let group = createSVG('g', {
725 transform: `translate(${x}, ${y})`
726 });
727 group.appendChild(createSVG("rect", args));
728 group.appendChild(text);
729
730 return group;
731}
732
733function legendDot(x, y, size, fill='none', label, truncate=false) {
734 label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label;
735
736 let args = {
737 className: 'legend-dot',
738 cx: 0,
739 cy: 0,
740 r: size,
741 fill: fill
742 };
743 let text = createSVG('text', {
744 className: 'legend-dataset-text',
745 x: 0,
746 y: 0,
747 dx: (FONT_SIZE) + 'px',
748 dy: (FONT_SIZE/3) + 'px',
749 'font-size': (FONT_SIZE * 1.2) + 'px',
750 'text-anchor': 'start',
751 fill: FONT_FILL,
752 innerHTML: label
753 });
754
755 let group = createSVG('g', {
756 transform: `translate(${x}, ${y})`
757 });
758 group.appendChild(createSVG("circle", args));
759 group.appendChild(text);
760
761 return group;
762}
763
764function makeText(className, x, y, content, options = {}) {
765 let fontSize = options.fontSize || FONT_SIZE;
766 let dy = options.dy !== undefined ? options.dy : (fontSize / 2);
767 let fill = options.fill || FONT_FILL;
768 let textAnchor = options.textAnchor || 'start';
769 return createSVG('text', {
770 className: className,
771 x: x,
772 y: y,
773 dy: dy + 'px',
774 'font-size': fontSize + 'px',
775 fill: fill,
776 'text-anchor': textAnchor,
777 innerHTML: content
778 });
779}
780
781function makeVertLine(x, label, y1, y2, options={}) {
782 if(!options.stroke) options.stroke = BASE_LINE_COLOR;
783 let l = createSVG('line', {
784 className: 'line-vertical ' + options.className,
785 x1: 0,
786 x2: 0,
787 y1: y1,
788 y2: y2,
789 styles: {
790 stroke: options.stroke
791 }
792 });
793
794 let text = createSVG('text', {
795 x: 0,
796 y: y1 > y2 ? y1 + LABEL_MARGIN : y1 - LABEL_MARGIN - FONT_SIZE,
797 dy: FONT_SIZE + 'px',
798 'font-size': FONT_SIZE + 'px',
799 'text-anchor': 'middle',
800 innerHTML: label + ""
801 });
802
803 let line = createSVG('g', {
804 transform: `translate(${ x }, 0)`
805 });
806
807 line.appendChild(l);
808 line.appendChild(text);
809
810 return line;
811}
812
813function makeHoriLine(y, label, x1, x2, options={}) {
814 if(!options.stroke) options.stroke = BASE_LINE_COLOR;
815 if(!options.lineType) options.lineType = '';
816 if (options.shortenNumbers) label = shortenLargeNumber(label);
817
818 let className = 'line-horizontal ' + options.className +
819 (options.lineType === "dashed" ? "dashed": "");
820
821 let l = createSVG('line', {
822 className: className,
823 x1: x1,
824 x2: x2,
825 y1: 0,
826 y2: 0,
827 styles: {
828 stroke: options.stroke
829 }
830 });
831
832 let text = createSVG('text', {
833 x: x1 < x2 ? x1 - LABEL_MARGIN : x1 + LABEL_MARGIN,
834 y: 0,
835 dy: (FONT_SIZE / 2 - 2) + 'px',
836 'font-size': FONT_SIZE + 'px',
837 'text-anchor': x1 < x2 ? 'end' : 'start',
838 innerHTML: label+""
839 });
840
841 let line = createSVG('g', {
842 transform: `translate(0, ${y})`,
843 'stroke-opacity': 1
844 });
845
846 if(text === 0 || text === '0') {
847 line.style.stroke = "rgba(27, 31, 35, 0.6)";
848 }
849
850 line.appendChild(l);
851 line.appendChild(text);
852
853 return line;
854}
855
856function yLine(y, label, width, options={}) {
857 if(!options.pos) options.pos = 'left';
858 if(!options.offset) options.offset = 0;
859 if(!options.mode) options.mode = 'span';
860 if(!options.stroke) options.stroke = BASE_LINE_COLOR;
861 if(!options.className) options.className = '';
862
863 let x1 = -1 * AXIS_TICK_LENGTH;
864 let x2 = options.mode === 'span' ? width + AXIS_TICK_LENGTH : 0;
865
866 if(options.mode === 'tick' && options.pos === 'right') {
867 x1 = width + AXIS_TICK_LENGTH;
868 x2 = width;
869 }
870
871 // let offset = options.pos === 'left' ? -1 * options.offset : options.offset;
872
873 x1 += options.offset;
874 x2 += options.offset;
875
876 return makeHoriLine(y, label, x1, x2, {
877 stroke: options.stroke,
878 className: options.className,
879 lineType: options.lineType,
880 shortenNumbers: options.shortenNumbers
881 });
882}
883
884function xLine(x, label, height, options={}) {
885 if(!options.pos) options.pos = 'bottom';
886 if(!options.offset) options.offset = 0;
887 if(!options.mode) options.mode = 'span';
888 if(!options.stroke) options.stroke = BASE_LINE_COLOR;
889 if(!options.className) options.className = '';
890
891 // Draw X axis line in span/tick mode with optional label
892 // y2(span)
893 // |
894 // |
895 // x line |
896 // |
897 // |
898 // ---------------------+-- y2(tick)
899 // |
900 // y1
901
902 let y1 = height + AXIS_TICK_LENGTH;
903 let y2 = options.mode === 'span' ? -1 * AXIS_TICK_LENGTH : height;
904
905 if(options.mode === 'tick' && options.pos === 'top') {
906 // top axis ticks
907 y1 = -1 * AXIS_TICK_LENGTH;
908 y2 = 0;
909 }
910
911 return makeVertLine(x, label, y1, y2, {
912 stroke: options.stroke,
913 className: options.className,
914 lineType: options.lineType
915 });
916}
917
918function yMarker(y, label, width, options={}) {
919 if(!options.labelPos) options.labelPos = 'right';
920 let x = options.labelPos === 'left' ? LABEL_MARGIN
921 : width - getStringWidth(label, 5) - LABEL_MARGIN;
922
923 let labelSvg = createSVG('text', {
924 className: 'chart-label',
925 x: x,
926 y: 0,
927 dy: (FONT_SIZE / -2) + 'px',
928 'font-size': FONT_SIZE + 'px',
929 'text-anchor': 'start',
930 innerHTML: label+""
931 });
932
933 let line = makeHoriLine(y, '', 0, width, {
934 stroke: options.stroke || BASE_LINE_COLOR,
935 className: options.className || '',
936 lineType: options.lineType
937 });
938
939 line.appendChild(labelSvg);
940
941 return line;
942}
943
944function yRegion(y1, y2, width, label, options={}) {
945 // return a group
946 let height = y1 - y2;
947
948 let rect = createSVG('rect', {
949 className: `bar mini`, // remove class
950 styles: {
951 fill: `rgba(228, 234, 239, 0.49)`,
952 stroke: BASE_LINE_COLOR,
953 'stroke-dasharray': `${width}, ${height}`
954 },
955 // 'data-point-index': index,
956 x: 0,
957 y: 0,
958 width: width,
959 height: height
960 });
961
962 if(!options.labelPos) options.labelPos = 'right';
963 let x = options.labelPos === 'left' ? LABEL_MARGIN
964 : width - getStringWidth(label+"", 4.5) - LABEL_MARGIN;
965
966 let labelSvg = createSVG('text', {
967 className: 'chart-label',
968 x: x,
969 y: 0,
970 dy: (FONT_SIZE / -2) + 'px',
971 'font-size': FONT_SIZE + 'px',
972 'text-anchor': 'start',
973 innerHTML: label+""
974 });
975
976 let region = createSVG('g', {
977 transform: `translate(0, ${y2})`
978 });
979
980 region.appendChild(rect);
981 region.appendChild(labelSvg);
982
983 return region;
984}
985
986function datasetBar(x, yTop, width, color, label='', index=0, offset=0, meta={}) {
987 let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine);
988 y -= offset;
989
990 if(height === 0) {
991 height = meta.minHeight;
992 y -= meta.minHeight;
993 }
994
995 let rect = createSVG('rect', {
996 className: `bar mini`,
997 style: `fill: ${color}`,
998 'data-point-index': index,
999 x: x,
1000 y: y,
1001 width: width,
1002 height: height
1003 });
1004
1005 label += "";
1006
1007 if(!label && !label.length) {
1008 return rect;
1009 } else {
1010 rect.setAttribute('y', 0);
1011 rect.setAttribute('x', 0);
1012 let text = createSVG('text', {
1013 className: 'data-point-value',
1014 x: width/2,
1015 y: 0,
1016 dy: (FONT_SIZE / 2 * -1) + 'px',
1017 'font-size': FONT_SIZE + 'px',
1018 'text-anchor': 'middle',
1019 innerHTML: label
1020 });
1021
1022 let group = createSVG('g', {
1023 'data-point-index': index,
1024 transform: `translate(${x}, ${y})`
1025 });
1026 group.appendChild(rect);
1027 group.appendChild(text);
1028
1029 return group;
1030 }
1031}
1032
1033function datasetDot(x, y, radius, color, label='', index=0) {
1034 let dot = createSVG('circle', {
1035 style: `fill: ${color}`,
1036 'data-point-index': index,
1037 cx: x,
1038 cy: y,
1039 r: radius
1040 });
1041
1042 label += "";
1043
1044 if(!label && !label.length) {
1045 return dot;
1046 } else {
1047 dot.setAttribute('cy', 0);
1048 dot.setAttribute('cx', 0);
1049
1050 let text = createSVG('text', {
1051 className: 'data-point-value',
1052 x: 0,
1053 y: 0,
1054 dy: (FONT_SIZE / 2 * -1 - radius) + 'px',
1055 'font-size': FONT_SIZE + 'px',
1056 'text-anchor': 'middle',
1057 innerHTML: label
1058 });
1059
1060 let group = createSVG('g', {
1061 'data-point-index': index,
1062 transform: `translate(${x}, ${y})`
1063 });
1064 group.appendChild(dot);
1065 group.appendChild(text);
1066
1067 return group;
1068 }
1069}
1070
1071function getPaths(xList, yList, color, options={}, meta={}) {
1072 let pointsList = yList.map((y, i) => (xList[i] + ',' + y));
1073 let pointsStr = pointsList.join("L");
1074
1075 // Spline
1076 if (options.spline)
1077 pointsStr = getSplineCurvePointsStr(xList, yList);
1078
1079 let path = makePath("M"+pointsStr, 'line-graph-path', color);
1080
1081 // HeatLine
1082 if(options.heatline) {
1083 let gradient_id = makeGradient(meta.svgDefs, color);
1084 path.style.stroke = `url(#${gradient_id})`;
1085 }
1086
1087 let paths = {
1088 path: path
1089 };
1090
1091 // Region
1092 if(options.regionFill) {
1093 let gradient_id_region = makeGradient(meta.svgDefs, color, true);
1094
1095 let pathStr = "M" + `${xList[0]},${meta.zeroLine}L` + pointsStr + `L${xList.slice(-1)[0]},${meta.zeroLine}`;
1096 paths.region = makePath(pathStr, `region-fill`, 'none', `url(#${gradient_id_region})`);
1097 }
1098
1099 return paths;
1100}
1101
1102let makeOverlay = {
1103 'bar': (unit) => {
1104 let transformValue;
1105 if(unit.nodeName !== 'rect') {
1106 transformValue = unit.getAttribute('transform');
1107 unit = unit.childNodes[0];
1108 }
1109 let overlay = unit.cloneNode();
1110 overlay.style.fill = '#000000';
1111 overlay.style.opacity = '0.4';
1112
1113 if(transformValue) {
1114 overlay.setAttribute('transform', transformValue);
1115 }
1116 return overlay;
1117 },
1118
1119 'dot': (unit) => {
1120 let transformValue;
1121 if(unit.nodeName !== 'circle') {
1122 transformValue = unit.getAttribute('transform');
1123 unit = unit.childNodes[0];
1124 }
1125 let overlay = unit.cloneNode();
1126 let radius = unit.getAttribute('r');
1127 let fill = unit.getAttribute('fill');
1128 overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR);
1129 overlay.setAttribute('fill', fill);
1130 overlay.style.opacity = '0.6';
1131
1132 if(transformValue) {
1133 overlay.setAttribute('transform', transformValue);
1134 }
1135 return overlay;
1136 },
1137
1138 'heat_square': (unit) => {
1139 let transformValue;
1140 if(unit.nodeName !== 'circle') {
1141 transformValue = unit.getAttribute('transform');
1142 unit = unit.childNodes[0];
1143 }
1144 let overlay = unit.cloneNode();
1145 let radius = unit.getAttribute('r');
1146 let fill = unit.getAttribute('fill');
1147 overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR);
1148 overlay.setAttribute('fill', fill);
1149 overlay.style.opacity = '0.6';
1150
1151 if(transformValue) {
1152 overlay.setAttribute('transform', transformValue);
1153 }
1154 return overlay;
1155 }
1156};
1157
1158let updateOverlay = {
1159 'bar': (unit, overlay) => {
1160 let transformValue;
1161 if(unit.nodeName !== 'rect') {
1162 transformValue = unit.getAttribute('transform');
1163 unit = unit.childNodes[0];
1164 }
1165 let attributes = ['x', 'y', 'width', 'height'];
1166 Object.values(unit.attributes)
1167 .filter(attr => attributes.includes(attr.name) && attr.specified)
1168 .map(attr => {
1169 overlay.setAttribute(attr.name, attr.nodeValue);
1170 });
1171
1172 if(transformValue) {
1173 overlay.setAttribute('transform', transformValue);
1174 }
1175 },
1176
1177 'dot': (unit, overlay) => {
1178 let transformValue;
1179 if(unit.nodeName !== 'circle') {
1180 transformValue = unit.getAttribute('transform');
1181 unit = unit.childNodes[0];
1182 }
1183 let attributes = ['cx', 'cy'];
1184 Object.values(unit.attributes)
1185 .filter(attr => attributes.includes(attr.name) && attr.specified)
1186 .map(attr => {
1187 overlay.setAttribute(attr.name, attr.nodeValue);
1188 });
1189
1190 if(transformValue) {
1191 overlay.setAttribute('transform', transformValue);
1192 }
1193 },
1194
1195 'heat_square': (unit, overlay) => {
1196 let transformValue;
1197 if(unit.nodeName !== 'circle') {
1198 transformValue = unit.getAttribute('transform');
1199 unit = unit.childNodes[0];
1200 }
1201 let attributes = ['cx', 'cy'];
1202 Object.values(unit.attributes)
1203 .filter(attr => attributes.includes(attr.name) && attr.specified)
1204 .map(attr => {
1205 overlay.setAttribute(attr.name, attr.nodeValue);
1206 });
1207
1208 if(transformValue) {
1209 overlay.setAttribute('transform', transformValue);
1210 }
1211 },
1212};
1213
1214const UNIT_ANIM_DUR = 350;
1215const PATH_ANIM_DUR = 350;
1216const MARKER_LINE_ANIM_DUR = UNIT_ANIM_DUR;
1217const REPLACE_ALL_NEW_DUR = 250;
1218
1219const STD_EASING = 'easein';
1220
1221function translate(unit, oldCoord, newCoord, duration) {
1222 let old = typeof oldCoord === 'string' ? oldCoord : oldCoord.join(', ');
1223 return [
1224 unit,
1225 {transform: newCoord.join(', ')},
1226 duration,
1227 STD_EASING,
1228 "translate",
1229 {transform: old}
1230 ];
1231}
1232
1233function translateVertLine(xLine, newX, oldX) {
1234 return translate(xLine, [oldX, 0], [newX, 0], MARKER_LINE_ANIM_DUR);
1235}
1236
1237function translateHoriLine(yLine, newY, oldY) {
1238 return translate(yLine, [0, oldY], [0, newY], MARKER_LINE_ANIM_DUR);
1239}
1240
1241function animateRegion(rectGroup, newY1, newY2, oldY2) {
1242 let newHeight = newY1 - newY2;
1243 let rect = rectGroup.childNodes[0];
1244 let width = rect.getAttribute("width");
1245 let rectAnim = [
1246 rect,
1247 { height: newHeight, 'stroke-dasharray': `${width}, ${newHeight}` },
1248 MARKER_LINE_ANIM_DUR,
1249 STD_EASING
1250 ];
1251
1252 let groupAnim = translate(rectGroup, [0, oldY2], [0, newY2], MARKER_LINE_ANIM_DUR);
1253 return [rectAnim, groupAnim];
1254}
1255
1256function animateBar(bar, x, yTop, width, offset=0, meta={}) {
1257 let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine);
1258 y -= offset;
1259 if(bar.nodeName !== 'rect') {
1260 let rect = bar.childNodes[0];
1261 let rectAnim = [
1262 rect,
1263 {width: width, height: height},
1264 UNIT_ANIM_DUR,
1265 STD_EASING
1266 ];
1267
1268 let oldCoordStr = bar.getAttribute("transform").split("(")[1].slice(0, -1);
1269 let groupAnim = translate(bar, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR);
1270 return [rectAnim, groupAnim];
1271 } else {
1272 return [[bar, {width: width, height: height, x: x, y: y}, UNIT_ANIM_DUR, STD_EASING]];
1273 }
1274 // bar.animate({height: args.newHeight, y: yTop}, UNIT_ANIM_DUR, mina.easein);
1275}
1276
1277function animateDot(dot, x, y) {
1278 if(dot.nodeName !== 'circle') {
1279 let oldCoordStr = dot.getAttribute("transform").split("(")[1].slice(0, -1);
1280 let groupAnim = translate(dot, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR);
1281 return [groupAnim];
1282 } else {
1283 return [[dot, {cx: x, cy: y}, UNIT_ANIM_DUR, STD_EASING]];
1284 }
1285 // dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein);
1286}
1287
1288function animatePath(paths, newXList, newYList, zeroLine, spline) {
1289 let pathComponents = [];
1290 let pointsStr = newYList.map((y, i) => (newXList[i] + ',' + y)).join("L");
1291
1292 if (spline)
1293 pointsStr = getSplineCurvePointsStr(newXList, newYList);
1294
1295 const animPath = [paths.path, {d:"M" + pointsStr}, PATH_ANIM_DUR, STD_EASING];
1296 pathComponents.push(animPath);
1297
1298 if(paths.region) {
1299 let regStartPt = `${newXList[0]},${zeroLine}L`;
1300 let regEndPt = `L${newXList.slice(-1)[0]}, ${zeroLine}`;
1301
1302 const animRegion = [
1303 paths.region,
1304 {d:"M" + regStartPt + pointsStr + regEndPt},
1305 PATH_ANIM_DUR,
1306 STD_EASING
1307 ];
1308 pathComponents.push(animRegion);
1309 }
1310
1311 return pathComponents;
1312}
1313
1314function animatePathStr(oldPath, pathStr) {
1315 return [oldPath, {d: pathStr}, UNIT_ANIM_DUR, STD_EASING];
1316}
1317
1318// Leveraging SMIL Animations
1319
1320const EASING = {
1321 ease: "0.25 0.1 0.25 1",
1322 linear: "0 0 1 1",
1323 // easein: "0.42 0 1 1",
1324 easein: "0.1 0.8 0.2 1",
1325 easeout: "0 0 0.58 1",
1326 easeinout: "0.42 0 0.58 1"
1327};
1328
1329function animateSVGElement(element, props, dur, easingType="linear", type=undefined, oldValues={}) {
1330
1331 let animElement = element.cloneNode(true);
1332 let newElement = element.cloneNode(true);
1333
1334 for(var attributeName in props) {
1335 let animateElement;
1336 if(attributeName === 'transform') {
1337 animateElement = document.createElementNS("http://www.w3.org/2000/svg", "animateTransform");
1338 } else {
1339 animateElement = document.createElementNS("http://www.w3.org/2000/svg", "animate");
1340 }
1341 let currentValue = oldValues[attributeName] || element.getAttribute(attributeName);
1342 let value = props[attributeName];
1343
1344 let animAttr = {
1345 attributeName: attributeName,
1346 from: currentValue,
1347 to: value,
1348 begin: "0s",
1349 dur: dur/1000 + "s",
1350 values: currentValue + ";" + value,
1351 keySplines: EASING[easingType],
1352 keyTimes: "0;1",
1353 calcMode: "spline",
1354 fill: 'freeze'
1355 };
1356
1357 if(type) {
1358 animAttr["type"] = type;
1359 }
1360
1361 for (var i in animAttr) {
1362 animateElement.setAttribute(i, animAttr[i]);
1363 }
1364
1365 animElement.appendChild(animateElement);
1366
1367 if(type) {
1368 newElement.setAttribute(attributeName, `translate(${value})`);
1369 } else {
1370 newElement.setAttribute(attributeName, value);
1371 }
1372 }
1373
1374 return [animElement, newElement];
1375}
1376
1377function transform(element, style) { // eslint-disable-line no-unused-vars
1378 element.style.transform = style;
1379 element.style.webkitTransform = style;
1380 element.style.msTransform = style;
1381 element.style.mozTransform = style;
1382 element.style.oTransform = style;
1383}
1384
1385function animateSVG(svgContainer, elements) {
1386 let newElements = [];
1387 let animElements = [];
1388
1389 elements.map(element => {
1390 let unit = element[0];
1391 let parent = unit.parentNode;
1392
1393 let animElement, newElement;
1394
1395 element[0] = unit;
1396 [animElement, newElement] = animateSVGElement(...element);
1397
1398 newElements.push(newElement);
1399 animElements.push([animElement, parent]);
1400
1401 parent.replaceChild(animElement, unit);
1402 });
1403
1404 let animSvg = svgContainer.cloneNode(true);
1405
1406 animElements.map((animElement, i) => {
1407 animElement[1].replaceChild(newElements[i], animElement[0]);
1408 elements[i][0] = newElements[i];
1409 });
1410
1411 return animSvg;
1412}
1413
1414function runSMILAnimation(parent, svgElement, elementsToAnimate) {
1415 if(elementsToAnimate.length === 0) return;
1416
1417 let animSvgElement = animateSVG(svgElement, elementsToAnimate);
1418 if(svgElement.parentNode == parent) {
1419 parent.removeChild(svgElement);
1420 parent.appendChild(animSvgElement);
1421
1422 }
1423
1424 // Replace the new svgElement (data has already been replaced)
1425 setTimeout(() => {
1426 if(animSvgElement.parentNode == parent) {
1427 parent.removeChild(animSvgElement);
1428 parent.appendChild(svgElement);
1429 }
1430 }, REPLACE_ALL_NEW_DUR);
1431}
1432
1433const CSSTEXT = ".chart-container{position:relative;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif}.chart-container .axis,.chart-container .chart-label{fill:#555b51}.chart-container .axis line,.chart-container .chart-label line{stroke:#dadada}.chart-container .dataset-units circle{stroke:#fff;stroke-width:2}.chart-container .dataset-units path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container .dataset-path{stroke-width:2px}.chart-container .path-group path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container line.dashed{stroke-dasharray:5,3}.chart-container .axis-line .specific-value{text-anchor:start}.chart-container .axis-line .y-line{text-anchor:end}.chart-container .axis-line .x-line{text-anchor:middle}.chart-container .legend-dataset-text{fill:#6c7680;font-weight:600}.graph-svg-tip{position:absolute;z-index:99999;padding:10px;font-size:12px;color:#959da5;text-align:center;background:rgba(0,0,0,.8);border-radius:3px}.graph-svg-tip ul{padding-left:0;display:flex}.graph-svg-tip ol{padding-left:0;display:flex}.graph-svg-tip ul.data-point-list li{min-width:90px;flex:1;font-weight:600}.graph-svg-tip strong{color:#dfe2e5;font-weight:600}.graph-svg-tip .svg-pointer{position:absolute;height:5px;margin:0 0 0 -5px;content:' ';border:5px solid transparent;border-top-color:rgba(0,0,0,.8)}.graph-svg-tip.comparison{padding:0;text-align:left;pointer-events:none}.graph-svg-tip.comparison .title{display:block;padding:10px;margin:0;font-weight:600;line-height:1;pointer-events:none}.graph-svg-tip.comparison ul{margin:0;white-space:nowrap;list-style:none}.graph-svg-tip.comparison li{display:inline-block;padding:5px 10px}";
1434
1435function downloadFile(filename, data) {
1436 var a = document.createElement('a');
1437 a.style = "display: none";
1438 var blob = new Blob(data, {type: "image/svg+xml; charset=utf-8"});
1439 var url = window.URL.createObjectURL(blob);
1440 a.href = url;
1441 a.download = filename;
1442 document.body.appendChild(a);
1443 a.click();
1444 setTimeout(function(){
1445 document.body.removeChild(a);
1446 window.URL.revokeObjectURL(url);
1447 }, 300);
1448}
1449
1450function prepareForExport(svg) {
1451 let clone = svg.cloneNode(true);
1452 clone.classList.add('chart-container');
1453 clone.setAttribute('xmlns', "http://www.w3.org/2000/svg");
1454 clone.setAttribute('xmlns:xlink', "http://www.w3.org/1999/xlink");
1455 let styleEl = $.create('style', {
1456 'innerHTML': CSSTEXT
1457 });
1458 clone.insertBefore(styleEl, clone.firstChild);
1459
1460 let container = $.create('div');
1461 container.appendChild(clone);
1462
1463 return container.innerHTML;
1464}
1465
1466class BaseChart {
1467 constructor(parent, options) {
1468
1469 this.parent = typeof parent === 'string'
1470 ? document.querySelector(parent)
1471 : parent;
1472
1473 if (!(this.parent instanceof HTMLElement)) {
1474 throw new Error('No `parent` element to render on was provided.');
1475 }
1476
1477 this.rawChartArgs = options;
1478
1479 this.title = options.title || '';
1480 this.type = options.type || '';
1481
1482 this.realData = this.prepareData(options.data);
1483 this.data = this.prepareFirstData(this.realData);
1484
1485 this.colors = this.validateColors(options.colors, this.type);
1486
1487 this.config = {
1488 showTooltip: 1, // calculate
1489 showLegend: 1, // calculate
1490 isNavigable: options.isNavigable || 0,
1491 animate: (typeof options.animate !== 'undefined') ? options.animate : 1,
1492 truncateLegends: options.truncateLegends || 0
1493 };
1494
1495 this.measures = JSON.parse(JSON.stringify(BASE_MEASURES));
1496 let m = this.measures;
1497 this.setMeasures(options);
1498 if(!this.title.length) { m.titleHeight = 0; }
1499 if(!this.config.showLegend) m.legendHeight = 0;
1500 this.argHeight = options.height || m.baseHeight;
1501
1502 this.state = {};
1503 this.options = {};
1504
1505 this.initTimeout = INIT_CHART_UPDATE_TIMEOUT;
1506
1507 if(this.config.isNavigable) {
1508 this.overlays = [];
1509 }
1510
1511 this.configure(options);
1512 }
1513
1514 prepareData(data) {
1515 return data;
1516 }
1517
1518 prepareFirstData(data) {
1519 return data;
1520 }
1521
1522 validateColors(colors, type) {
1523 const validColors = [];
1524 colors = (colors || []).concat(DEFAULT_COLORS[type]);
1525 colors.forEach((string) => {
1526 const color = getColor(string);
1527 if(!isValidColor(color)) {
1528 console.warn('"' + string + '" is not a valid color.');
1529 } else {
1530 validColors.push(color);
1531 }
1532 });
1533 return validColors;
1534 }
1535
1536 setMeasures() {
1537 // Override measures, including those for title and legend
1538 // set config for legend and title
1539 }
1540
1541 configure() {
1542 let height = this.argHeight;
1543 this.baseHeight = height;
1544 this.height = height - getExtraHeight(this.measures);
1545
1546 // Bind window events
1547 this.boundDrawFn = () => this.draw(true);
1548 window.addEventListener('resize', this.boundDrawFn);
1549 window.addEventListener('orientationchange', this.boundDrawFn);
1550 }
1551
1552 destroy() {
1553 window.removeEventListener('resize', this.boundDrawFn);
1554 window.removeEventListener('orientationchange', this.boundDrawFn);
1555 }
1556
1557 // Has to be called manually
1558 setup() {
1559 this.makeContainer();
1560 this.updateWidth();
1561 this.makeTooltip();
1562
1563 this.draw(false, true);
1564 }
1565
1566 makeContainer() {
1567 // Chart needs a dedicated parent element
1568 this.parent.innerHTML = '';
1569
1570 let args = {
1571 inside: this.parent,
1572 className: 'chart-container'
1573 };
1574
1575 if(this.independentWidth) {
1576 args.styles = { width: this.independentWidth + 'px' };
1577 }
1578
1579 this.container = $.create('div', args);
1580 }
1581
1582 makeTooltip() {
1583 this.tip = new SvgTip({
1584 parent: this.container,
1585 colors: this.colors
1586 });
1587 this.bindTooltip();
1588 }
1589
1590 bindTooltip() {}
1591
1592 draw(onlyWidthChange=false, init=false) {
1593 this.updateWidth();
1594
1595 this.calc(onlyWidthChange);
1596 this.makeChartArea();
1597 this.setupComponents();
1598
1599 this.components.forEach(c => c.setup(this.drawArea));
1600 // this.components.forEach(c => c.make());
1601 this.render(this.components, false);
1602
1603 if(init) {
1604 this.data = this.realData;
1605 setTimeout(() => {this.update(this.data);}, this.initTimeout);
1606 }
1607
1608 this.renderLegend();
1609
1610 this.setupNavigation(init);
1611 }
1612
1613 calc() {} // builds state
1614
1615 updateWidth() {
1616 this.baseWidth = getElementContentWidth(this.parent);
1617 this.width = this.baseWidth - getExtraWidth(this.measures);
1618 }
1619
1620 makeChartArea() {
1621 if(this.svg) {
1622 this.container.removeChild(this.svg);
1623 }
1624 let m = this.measures;
1625
1626 this.svg = makeSVGContainer(
1627 this.container,
1628 'frappe-chart chart',
1629 this.baseWidth,
1630 this.baseHeight
1631 );
1632 this.svgDefs = makeSVGDefs(this.svg);
1633
1634 if(this.title.length) {
1635 this.titleEL = makeText(
1636 'title',
1637 m.margins.left,
1638 m.margins.top,
1639 this.title,
1640 {
1641 fontSize: m.titleFontSize,
1642 fill: '#666666',
1643 dy: m.titleFontSize
1644 }
1645 );
1646 }
1647
1648 let top = getTopOffset(m);
1649 this.drawArea = makeSVGGroup(
1650 this.type + '-chart chart-draw-area',
1651 `translate(${getLeftOffset(m)}, ${top})`
1652 );
1653
1654 if(this.config.showLegend) {
1655 top += this.height + m.paddings.bottom;
1656 this.legendArea = makeSVGGroup(
1657 'chart-legend',
1658 `translate(${getLeftOffset(m)}, ${top})`
1659 );
1660 }
1661
1662 if(this.title.length) { this.svg.appendChild(this.titleEL); }
1663 this.svg.appendChild(this.drawArea);
1664 if(this.config.showLegend) { this.svg.appendChild(this.legendArea); }
1665
1666 this.updateTipOffset(getLeftOffset(m), getTopOffset(m));
1667 }
1668
1669 updateTipOffset(x, y) {
1670 this.tip.offset = {
1671 x: x,
1672 y: y
1673 };
1674 }
1675
1676 setupComponents() { this.components = new Map(); }
1677
1678 update(data) {
1679 if(!data) {
1680 console.error('No data to update.');
1681 }
1682 this.data = this.prepareData(data);
1683 this.calc(); // builds state
1684 this.render(this.components, this.config.animate);
1685 }
1686
1687 render(components=this.components, animate=true) {
1688 if(this.config.isNavigable) {
1689 // Remove all existing overlays
1690 this.overlays.map(o => o.parentNode.removeChild(o));
1691 // ref.parentNode.insertBefore(element, ref);
1692 }
1693 let elementsToAnimate = [];
1694 // Can decouple to this.refreshComponents() first to save animation timeout
1695 components.forEach(c => {
1696 elementsToAnimate = elementsToAnimate.concat(c.update(animate));
1697 });
1698 if(elementsToAnimate.length > 0) {
1699 runSMILAnimation(this.container, this.svg, elementsToAnimate);
1700 setTimeout(() => {
1701 components.forEach(c => c.make());
1702 this.updateNav();
1703 }, CHART_POST_ANIMATE_TIMEOUT);
1704 } else {
1705 components.forEach(c => c.make());
1706 this.updateNav();
1707 }
1708 }
1709
1710 updateNav() {
1711 if(this.config.isNavigable) {
1712 this.makeOverlay();
1713 this.bindUnits();
1714 }
1715 }
1716
1717 renderLegend() {}
1718
1719 setupNavigation(init=false) {
1720 if(!this.config.isNavigable) return;
1721
1722 if(init) {
1723 this.bindOverlay();
1724
1725 this.keyActions = {
1726 '13': this.onEnterKey.bind(this),
1727 '37': this.onLeftArrow.bind(this),
1728 '38': this.onUpArrow.bind(this),
1729 '39': this.onRightArrow.bind(this),
1730 '40': this.onDownArrow.bind(this),
1731 };
1732
1733 document.addEventListener('keydown', (e) => {
1734 if(isElementInViewport(this.container)) {
1735 e = e || window.event;
1736 if(this.keyActions[e.keyCode]) {
1737 this.keyActions[e.keyCode]();
1738 }
1739 }
1740 });
1741 }
1742 }
1743
1744 makeOverlay() {}
1745 updateOverlay() {}
1746 bindOverlay() {}
1747 bindUnits() {}
1748
1749 onLeftArrow() {}
1750 onRightArrow() {}
1751 onUpArrow() {}
1752 onDownArrow() {}
1753 onEnterKey() {}
1754
1755 addDataPoint() {}
1756 removeDataPoint() {}
1757
1758 getDataPoint() {}
1759 setCurrentDataPoint() {}
1760
1761 updateDataset() {}
1762
1763 export() {
1764 let chartSvg = prepareForExport(this.svg);
1765 downloadFile(this.title || 'Chart', [chartSvg]);
1766 }
1767}
1768
1769class AggregationChart extends BaseChart {
1770 constructor(parent, args) {
1771 super(parent, args);
1772 }
1773
1774 configure(args) {
1775 super.configure(args);
1776
1777 this.config.maxSlices = args.maxSlices || 20;
1778 this.config.maxLegendPoints = args.maxLegendPoints || 20;
1779 }
1780
1781 calc() {
1782 let s = this.state;
1783 let maxSlices = this.config.maxSlices;
1784 s.sliceTotals = [];
1785
1786 let allTotals = this.data.labels.map((label, i) => {
1787 let total = 0;
1788 this.data.datasets.map(e => {
1789 total += e.values[i];
1790 });
1791 return [total, label];
1792 }).filter(d => { return d[0] >= 0; }); // keep only positive results
1793
1794 let totals = allTotals;
1795 if(allTotals.length > maxSlices) {
1796 // Prune and keep a grey area for rest as per maxSlices
1797 allTotals.sort((a, b) => { return b[0] - a[0]; });
1798
1799 totals = allTotals.slice(0, maxSlices-1);
1800 let remaining = allTotals.slice(maxSlices-1);
1801
1802 let sumOfRemaining = 0;
1803 remaining.map(d => {sumOfRemaining += d[0];});
1804 totals.push([sumOfRemaining, 'Rest']);
1805 this.colors[maxSlices-1] = 'grey';
1806 }
1807
1808 s.labels = [];
1809 totals.map(d => {
1810 s.sliceTotals.push(d[0]);
1811 s.labels.push(d[1]);
1812 });
1813
1814 s.grandTotal = s.sliceTotals.reduce((a, b) => a + b, 0);
1815
1816 this.center = {
1817 x: this.width / 2,
1818 y: this.height / 2
1819 };
1820 }
1821
1822 renderLegend() {
1823 let s = this.state;
1824 this.legendArea.textContent = '';
1825 this.legendTotals = s.sliceTotals.slice(0, this.config.maxLegendPoints);
1826
1827 let count = 0;
1828 let y = 0;
1829 this.legendTotals.map((d, i) => {
1830 let barWidth = 110;
1831 let divisor = Math.floor(
1832 (this.width - getExtraWidth(this.measures))/barWidth
1833 );
1834 if (this.legendTotals.length < divisor) {
1835 barWidth = this.width/this.legendTotals.length;
1836 }
1837 if(count > divisor) {
1838 count = 0;
1839 y += 20;
1840 }
1841 let x = barWidth * count + 5;
1842 let dot = legendDot(
1843 x,
1844 y,
1845 5,
1846 this.colors[i],
1847 `${s.labels[i]}: ${d}`,
1848 this.config.truncateLegends
1849 );
1850 this.legendArea.appendChild(dot);
1851 count++;
1852 });
1853 }
1854}
1855
1856// Playing around with dates
1857
1858const NO_OF_YEAR_MONTHS = 12;
1859const NO_OF_DAYS_IN_WEEK = 7;
1860
1861const NO_OF_MILLIS = 1000;
1862const SEC_IN_DAY = 86400;
1863
1864const MONTH_NAMES = ["January", "February", "March", "April", "May",
1865 "June", "July", "August", "September", "October", "November", "December"];
1866
1867
1868const DAY_NAMES_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1869
1870
1871// https://stackoverflow.com/a/11252167/6495043
1872function treatAsUtc(date) {
1873 let result = new Date(date);
1874 result.setMinutes(result.getMinutes() - result.getTimezoneOffset());
1875 return result;
1876}
1877
1878function getYyyyMmDd(date) {
1879 let dd = date.getDate();
1880 let mm = date.getMonth() + 1; // getMonth() is zero-based
1881 return [
1882 date.getFullYear(),
1883 (mm>9 ? '' : '0') + mm,
1884 (dd>9 ? '' : '0') + dd
1885 ].join('-');
1886}
1887
1888function clone(date) {
1889 return new Date(date.getTime());
1890}
1891
1892
1893
1894
1895
1896// export function getMonthsBetween(startDate, endDate) {}
1897
1898function getWeeksBetween(startDate, endDate) {
1899 let weekStartDate = setDayToSunday(startDate);
1900 return Math.ceil(getDaysBetween(weekStartDate, endDate) / NO_OF_DAYS_IN_WEEK);
1901}
1902
1903function getDaysBetween(startDate, endDate) {
1904 let millisecondsPerDay = SEC_IN_DAY * NO_OF_MILLIS;
1905 return (treatAsUtc(endDate) - treatAsUtc(startDate)) / millisecondsPerDay;
1906}
1907
1908function areInSameMonth(startDate, endDate) {
1909 return startDate.getMonth() === endDate.getMonth()
1910 && startDate.getFullYear() === endDate.getFullYear();
1911}
1912
1913function getMonthName(i, short=false) {
1914 let monthName = MONTH_NAMES[i];
1915 return short ? monthName.slice(0, 3) : monthName;
1916}
1917
1918function getLastDateInMonth (month, year) {
1919 return new Date(year, month + 1, 0); // 0: last day in previous month
1920}
1921
1922// mutates
1923function setDayToSunday(date) {
1924 let newDate = clone(date);
1925 const day = newDate.getDay();
1926 if(day !== 0) {
1927 addDays(newDate, (-1) * day);
1928 }
1929 return newDate;
1930}
1931
1932// mutates
1933function addDays(date, numberOfDays) {
1934 date.setDate(date.getDate() + numberOfDays);
1935}
1936
1937class ChartComponent {
1938 constructor({
1939 layerClass = '',
1940 layerTransform = '',
1941 constants,
1942
1943 getData,
1944 makeElements,
1945 animateElements
1946 }) {
1947 this.layerTransform = layerTransform;
1948 this.constants = constants;
1949
1950 this.makeElements = makeElements;
1951 this.getData = getData;
1952
1953 this.animateElements = animateElements;
1954
1955 this.store = [];
1956 this.labels = [];
1957
1958 this.layerClass = layerClass;
1959 this.layerClass = typeof(this.layerClass) === 'function'
1960 ? this.layerClass() : this.layerClass;
1961
1962 this.refresh();
1963 }
1964
1965 refresh(data) {
1966 this.data = data || this.getData();
1967 }
1968
1969 setup(parent) {
1970 this.layer = makeSVGGroup(this.layerClass, this.layerTransform, parent);
1971 }
1972
1973 make() {
1974 this.render(this.data);
1975 this.oldData = this.data;
1976 }
1977
1978 render(data) {
1979 this.store = this.makeElements(data);
1980
1981 this.layer.textContent = '';
1982 this.store.forEach(element => {
1983 this.layer.appendChild(element);
1984 });
1985 this.labels.forEach(element => {
1986 this.layer.appendChild(element);
1987 });
1988 }
1989
1990 update(animate = true) {
1991 this.refresh();
1992 let animateElements = [];
1993 if(animate) {
1994 animateElements = this.animateElements(this.data) || [];
1995 }
1996 return animateElements;
1997 }
1998}
1999
2000let componentConfigs = {
2001 donutSlices: {
2002 layerClass: 'donut-slices',
2003 makeElements(data) {
2004 return data.sliceStrings.map((s, i) => {
2005 let slice = makePath(s, 'donut-path', data.colors[i], 'none', data.strokeWidth);
2006 slice.style.transition = 'transform .3s;';
2007 return slice;
2008 });
2009 },
2010
2011 animateElements(newData) {
2012 return this.store.map((slice, i) => animatePathStr(slice, newData.sliceStrings[i]));
2013 },
2014 },
2015 pieSlices: {
2016 layerClass: 'pie-slices',
2017 makeElements(data) {
2018 return data.sliceStrings.map((s, i) =>{
2019 let slice = makePath(s, 'pie-path', 'none', data.colors[i]);
2020 slice.style.transition = 'transform .3s;';
2021 return slice;
2022 });
2023 },
2024
2025 animateElements(newData) {
2026 return this.store.map((slice, i) =>
2027 animatePathStr(slice, newData.sliceStrings[i])
2028 );
2029 }
2030 },
2031 percentageBars: {
2032 layerClass: 'percentage-bars',
2033 makeElements(data) {
2034 return data.xPositions.map((x, i) =>{
2035 let y = 0;
2036 let bar = percentageBar(x, y, data.widths[i],
2037 this.constants.barHeight, this.constants.barDepth, data.colors[i]);
2038 return bar;
2039 });
2040 },
2041
2042 animateElements(newData) {
2043 if(newData) return [];
2044 }
2045 },
2046 yAxis: {
2047 layerClass: 'y axis',
2048 makeElements(data) {
2049 return data.positions.map((position, i) =>
2050 yLine(position, data.labels[i], this.constants.width,
2051 {mode: this.constants.mode, pos: this.constants.pos, shortenNumbers: this.constants.shortenNumbers})
2052 );
2053 },
2054
2055 animateElements(newData) {
2056 let newPos = newData.positions;
2057 let newLabels = newData.labels;
2058 let oldPos = this.oldData.positions;
2059 let oldLabels = this.oldData.labels;
2060
2061 [oldPos, newPos] = equilizeNoOfElements(oldPos, newPos);
2062 [oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels);
2063
2064 this.render({
2065 positions: oldPos,
2066 labels: newLabels
2067 });
2068
2069 return this.store.map((line, i) => {
2070 return translateHoriLine(
2071 line, newPos[i], oldPos[i]
2072 );
2073 });
2074 }
2075 },
2076
2077 xAxis: {
2078 layerClass: 'x axis',
2079 makeElements(data) {
2080 return data.positions.map((position, i) =>
2081 xLine(position, data.calcLabels[i], this.constants.height,
2082 {mode: this.constants.mode, pos: this.constants.pos})
2083 );
2084 },
2085
2086 animateElements(newData) {
2087 let newPos = newData.positions;
2088 let newLabels = newData.calcLabels;
2089 let oldPos = this.oldData.positions;
2090 let oldLabels = this.oldData.calcLabels;
2091
2092 [oldPos, newPos] = equilizeNoOfElements(oldPos, newPos);
2093 [oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels);
2094
2095 this.render({
2096 positions: oldPos,
2097 calcLabels: newLabels
2098 });
2099
2100 return this.store.map((line, i) => {
2101 return translateVertLine(
2102 line, newPos[i], oldPos[i]
2103 );
2104 });
2105 }
2106 },
2107
2108 yMarkers: {
2109 layerClass: 'y-markers',
2110 makeElements(data) {
2111 return data.map(m =>
2112 yMarker(m.position, m.label, this.constants.width,
2113 {labelPos: m.options.labelPos, mode: 'span', lineType: 'dashed'})
2114 );
2115 },
2116 animateElements(newData) {
2117 [this.oldData, newData] = equilizeNoOfElements(this.oldData, newData);
2118
2119 let newPos = newData.map(d => d.position);
2120 let newLabels = newData.map(d => d.label);
2121 let newOptions = newData.map(d => d.options);
2122
2123 let oldPos = this.oldData.map(d => d.position);
2124
2125 this.render(oldPos.map((pos, i) => {
2126 return {
2127 position: oldPos[i],
2128 label: newLabels[i],
2129 options: newOptions[i]
2130 };
2131 }));
2132
2133 return this.store.map((line, i) => {
2134 return translateHoriLine(
2135 line, newPos[i], oldPos[i]
2136 );
2137 });
2138 }
2139 },
2140
2141 yRegions: {
2142 layerClass: 'y-regions',
2143 makeElements(data) {
2144 return data.map(r =>
2145 yRegion(r.startPos, r.endPos, this.constants.width,
2146 r.label, {labelPos: r.options.labelPos})
2147 );
2148 },
2149 animateElements(newData) {
2150 [this.oldData, newData] = equilizeNoOfElements(this.oldData, newData);
2151
2152 let newPos = newData.map(d => d.endPos);
2153 let newLabels = newData.map(d => d.label);
2154 let newStarts = newData.map(d => d.startPos);
2155 let newOptions = newData.map(d => d.options);
2156
2157 let oldPos = this.oldData.map(d => d.endPos);
2158 let oldStarts = this.oldData.map(d => d.startPos);
2159
2160 this.render(oldPos.map((pos, i) => {
2161 return {
2162 startPos: oldStarts[i],
2163 endPos: oldPos[i],
2164 label: newLabels[i],
2165 options: newOptions[i]
2166 };
2167 }));
2168
2169 let animateElements = [];
2170
2171 this.store.map((rectGroup, i) => {
2172 animateElements = animateElements.concat(animateRegion(
2173 rectGroup, newStarts[i], newPos[i], oldPos[i]
2174 ));
2175 });
2176
2177 return animateElements;
2178 }
2179 },
2180
2181 heatDomain: {
2182 layerClass: function() { return 'heat-domain domain-' + this.constants.index; },
2183 makeElements(data) {
2184 let {index, colWidth, rowHeight, squareSize, xTranslate} = this.constants;
2185 let monthNameHeight = -12;
2186 let x = xTranslate, y = 0;
2187
2188 this.serializedSubDomains = [];
2189
2190 data.cols.map((week, weekNo) => {
2191 if(weekNo === 1) {
2192 this.labels.push(
2193 makeText('domain-name', x, monthNameHeight, getMonthName(index, true).toUpperCase(),
2194 {
2195 fontSize: 9
2196 }
2197 )
2198 );
2199 }
2200 week.map((day, i) => {
2201 if(day.fill) {
2202 let data = {
2203 'data-date': day.yyyyMmDd,
2204 'data-value': day.dataValue,
2205 'data-day': i
2206 };
2207 let square = heatSquare('day', x, y, squareSize, day.fill, data);
2208 this.serializedSubDomains.push(square);
2209 }
2210 y += rowHeight;
2211 });
2212 y = 0;
2213 x += colWidth;
2214 });
2215
2216 return this.serializedSubDomains;
2217 },
2218
2219 animateElements(newData) {
2220 if(newData) return [];
2221 }
2222 },
2223
2224 barGraph: {
2225 layerClass: function() { return 'dataset-units dataset-bars dataset-' + this.constants.index; },
2226 makeElements(data) {
2227 let c = this.constants;
2228 this.unitType = 'bar';
2229 this.units = data.yPositions.map((y, j) => {
2230 return datasetBar(
2231 data.xPositions[j],
2232 y,
2233 data.barWidth,
2234 c.color,
2235 data.labels[j],
2236 j,
2237 data.offsets[j],
2238 {
2239 zeroLine: data.zeroLine,
2240 barsWidth: data.barsWidth,
2241 minHeight: c.minHeight
2242 }
2243 );
2244 });
2245 return this.units;
2246 },
2247 animateElements(newData) {
2248 let newXPos = newData.xPositions;
2249 let newYPos = newData.yPositions;
2250 let newOffsets = newData.offsets;
2251 let newLabels = newData.labels;
2252
2253 let oldXPos = this.oldData.xPositions;
2254 let oldYPos = this.oldData.yPositions;
2255 let oldOffsets = this.oldData.offsets;
2256 let oldLabels = this.oldData.labels;
2257
2258 [oldXPos, newXPos] = equilizeNoOfElements(oldXPos, newXPos);
2259 [oldYPos, newYPos] = equilizeNoOfElements(oldYPos, newYPos);
2260 [oldOffsets, newOffsets] = equilizeNoOfElements(oldOffsets, newOffsets);
2261 [oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels);
2262
2263 this.render({
2264 xPositions: oldXPos,
2265 yPositions: oldYPos,
2266 offsets: oldOffsets,
2267 labels: newLabels,
2268
2269 zeroLine: this.oldData.zeroLine,
2270 barsWidth: this.oldData.barsWidth,
2271 barWidth: this.oldData.barWidth,
2272 });
2273
2274 let animateElements = [];
2275
2276 this.store.map((bar, i) => {
2277 animateElements = animateElements.concat(animateBar(
2278 bar, newXPos[i], newYPos[i], newData.barWidth, newOffsets[i],
2279 {zeroLine: newData.zeroLine}
2280 ));
2281 });
2282
2283 return animateElements;
2284 }
2285 },
2286
2287 lineGraph: {
2288 layerClass: function() { return 'dataset-units dataset-line dataset-' + this.constants.index; },
2289 makeElements(data) {
2290 let c = this.constants;
2291 this.unitType = 'dot';
2292 this.paths = {};
2293 if(!c.hideLine) {
2294 this.paths = getPaths(
2295 data.xPositions,
2296 data.yPositions,
2297 c.color,
2298 {
2299 heatline: c.heatline,
2300 regionFill: c.regionFill,
2301 spline: c.spline
2302 },
2303 {
2304 svgDefs: c.svgDefs,
2305 zeroLine: data.zeroLine
2306 }
2307 );
2308 }
2309
2310 this.units = [];
2311 if(!c.hideDots) {
2312 this.units = data.yPositions.map((y, j) => {
2313 return datasetDot(
2314 data.xPositions[j],
2315 y,
2316 data.radius,
2317 c.color,
2318 (c.valuesOverPoints ? data.values[j] : ''),
2319 j
2320 );
2321 });
2322 }
2323
2324 return Object.values(this.paths).concat(this.units);
2325 },
2326 animateElements(newData) {
2327 let newXPos = newData.xPositions;
2328 let newYPos = newData.yPositions;
2329 let newValues = newData.values;
2330
2331 let oldXPos = this.oldData.xPositions;
2332 let oldYPos = this.oldData.yPositions;
2333 let oldValues = this.oldData.values;
2334
2335 [oldXPos, newXPos] = equilizeNoOfElements(oldXPos, newXPos);
2336 [oldYPos, newYPos] = equilizeNoOfElements(oldYPos, newYPos);
2337 [oldValues, newValues] = equilizeNoOfElements(oldValues, newValues);
2338
2339 this.render({
2340 xPositions: oldXPos,
2341 yPositions: oldYPos,
2342 values: newValues,
2343
2344 zeroLine: this.oldData.zeroLine,
2345 radius: this.oldData.radius,
2346 });
2347
2348 let animateElements = [];
2349
2350 if(Object.keys(this.paths).length) {
2351 animateElements = animateElements.concat(animatePath(
2352 this.paths, newXPos, newYPos, newData.zeroLine, this.constants.spline));
2353 }
2354
2355 if(this.units.length) {
2356 this.units.map((dot, i) => {
2357 animateElements = animateElements.concat(animateDot(
2358 dot, newXPos[i], newYPos[i]));
2359 });
2360 }
2361
2362 return animateElements;
2363 }
2364 }
2365};
2366
2367function getComponent(name, constants, getData) {
2368 let keys = Object.keys(componentConfigs).filter(k => name.includes(k));
2369 let config = componentConfigs[keys[0]];
2370 Object.assign(config, {
2371 constants: constants,
2372 getData: getData
2373 });
2374 return new ChartComponent(config);
2375}
2376
2377class PercentageChart extends AggregationChart {
2378 constructor(parent, args) {
2379 super(parent, args);
2380 this.type = 'percentage';
2381 this.setup();
2382 }
2383
2384 setMeasures(options) {
2385 let m = this.measures;
2386 this.barOptions = options.barOptions || {};
2387
2388 let b = this.barOptions;
2389 b.height = b.height || PERCENTAGE_BAR_DEFAULT_HEIGHT;
2390 b.depth = b.depth || PERCENTAGE_BAR_DEFAULT_DEPTH;
2391
2392 m.paddings.right = 30;
2393 m.legendHeight = 60;
2394 m.baseHeight = (b.height + b.depth * 0.5) * 8;
2395 }
2396
2397 setupComponents() {
2398 let s = this.state;
2399
2400 let componentConfigs = [
2401 [
2402 'percentageBars',
2403 {
2404 barHeight: this.barOptions.height,
2405 barDepth: this.barOptions.depth,
2406 },
2407 function() {
2408 return {
2409 xPositions: s.xPositions,
2410 widths: s.widths,
2411 colors: this.colors
2412 };
2413 }.bind(this)
2414 ]
2415 ];
2416
2417 this.components = new Map(componentConfigs
2418 .map(args => {
2419 let component = getComponent(...args);
2420 return [args[0], component];
2421 }));
2422 }
2423
2424 calc() {
2425 super.calc();
2426 let s = this.state;
2427
2428 s.xPositions = [];
2429 s.widths = [];
2430
2431 let xPos = 0;
2432 s.sliceTotals.map((value) => {
2433 let width = this.width * value / s.grandTotal;
2434 s.widths.push(width);
2435 s.xPositions.push(xPos);
2436 xPos += width;
2437 });
2438 }
2439
2440 makeDataByIndex() { }
2441
2442 bindTooltip() {
2443 let s = this.state;
2444 this.container.addEventListener('mousemove', (e) => {
2445 let bars = this.components.get('percentageBars').store;
2446 let bar = e.target;
2447 if(bars.includes(bar)) {
2448
2449 let i = bars.indexOf(bar);
2450 let gOff = getOffset(this.container), pOff = getOffset(bar);
2451
2452 let x = pOff.left - gOff.left + parseInt(bar.getAttribute('width'))/2;
2453 let y = pOff.top - gOff.top;
2454 let title = (this.formattedLabels && this.formattedLabels.length>0
2455 ? this.formattedLabels[i] : this.state.labels[i]) + ': ';
2456 let fraction = s.sliceTotals[i]/s.grandTotal;
2457
2458 this.tip.setValues(x, y, {name: title, value: (fraction*100).toFixed(1) + "%"});
2459 this.tip.showTip();
2460 }
2461 });
2462 }
2463}
2464
2465class PieChart extends AggregationChart {
2466 constructor(parent, args) {
2467 super(parent, args);
2468 this.type = 'pie';
2469 this.initTimeout = 0;
2470 this.init = 1;
2471
2472 this.setup();
2473 }
2474
2475 configure(args) {
2476 super.configure(args);
2477 this.mouseMove = this.mouseMove.bind(this);
2478 this.mouseLeave = this.mouseLeave.bind(this);
2479
2480 this.hoverRadio = args.hoverRadio || 0.1;
2481 this.config.startAngle = args.startAngle || 0;
2482
2483 this.clockWise = args.clockWise || false;
2484 }
2485
2486 calc() {
2487 super.calc();
2488 let s = this.state;
2489 this.radius = (this.height > this.width ? this.center.x : this.center.y);
2490
2491 const { radius, clockWise } = this;
2492
2493 const prevSlicesProperties = s.slicesProperties || [];
2494 s.sliceStrings = [];
2495 s.slicesProperties = [];
2496 let curAngle = 180 - this.config.startAngle;
2497 s.sliceTotals.map((total, i) => {
2498 const startAngle = curAngle;
2499 const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE;
2500 const largeArc = originDiffAngle > 180 ? 1: 0;
2501 const diffAngle = clockWise ? -originDiffAngle : originDiffAngle;
2502 const endAngle = curAngle = curAngle + diffAngle;
2503 const startPosition = getPositionByAngle(startAngle, radius);
2504 const endPosition = getPositionByAngle(endAngle, radius);
2505
2506 const prevProperty = this.init && prevSlicesProperties[i];
2507
2508 let curStart,curEnd;
2509 if(this.init) {
2510 curStart = prevProperty ? prevProperty.startPosition : startPosition;
2511 curEnd = prevProperty ? prevProperty.endPosition : startPosition;
2512 } else {
2513 curStart = startPosition;
2514 curEnd = endPosition;
2515 }
2516 const curPath =
2517 originDiffAngle === 360
2518 ? makeCircleStr(curStart, curEnd, this.center, this.radius, clockWise, largeArc)
2519 : makeArcPathStr(curStart, curEnd, this.center, this.radius, clockWise, largeArc);
2520
2521 s.sliceStrings.push(curPath);
2522 s.slicesProperties.push({
2523 startPosition,
2524 endPosition,
2525 value: total,
2526 total: s.grandTotal,
2527 startAngle,
2528 endAngle,
2529 angle: diffAngle
2530 });
2531
2532 });
2533 this.init = 0;
2534 }
2535
2536 setupComponents() {
2537 let s = this.state;
2538
2539 let componentConfigs = [
2540 [
2541 'pieSlices',
2542 { },
2543 function() {
2544 return {
2545 sliceStrings: s.sliceStrings,
2546 colors: this.colors
2547 };
2548 }.bind(this)
2549 ]
2550 ];
2551
2552 this.components = new Map(componentConfigs
2553 .map(args => {
2554 let component = getComponent(...args);
2555 return [args[0], component];
2556 }));
2557 }
2558
2559 calTranslateByAngle(property){
2560 const{radius,hoverRadio} = this;
2561 const position = getPositionByAngle(property.startAngle+(property.angle / 2),radius);
2562 return `translate3d(${(position.x) * hoverRadio}px,${(position.y) * hoverRadio}px,0)`;
2563 }
2564
2565 hoverSlice(path,i,flag,e){
2566 if(!path) return;
2567 const color = this.colors[i];
2568 if(flag) {
2569 transform(path, this.calTranslateByAngle(this.state.slicesProperties[i]));
2570 path.style.fill = lightenDarkenColor(color, 50);
2571 let g_off = getOffset(this.svg);
2572 let x = e.pageX - g_off.left + 10;
2573 let y = e.pageY - g_off.top - 10;
2574 let title = (this.formatted_labels && this.formatted_labels.length > 0
2575 ? this.formatted_labels[i] : this.state.labels[i]) + ': ';
2576 let percent = (this.state.sliceTotals[i] * 100 / this.state.grandTotal).toFixed(1);
2577 this.tip.setValues(x, y, {name: title, value: percent + "%"});
2578 this.tip.showTip();
2579 } else {
2580 transform(path,'translate3d(0,0,0)');
2581 this.tip.hideTip();
2582 path.style.fill = color;
2583 }
2584 }
2585
2586 bindTooltip() {
2587 this.container.addEventListener('mousemove', this.mouseMove);
2588 this.container.addEventListener('mouseleave', this.mouseLeave);
2589 }
2590
2591 mouseMove(e){
2592 const target = e.target;
2593 let slices = this.components.get('pieSlices').store;
2594 let prevIndex = this.curActiveSliceIndex;
2595 let prevAcitve = this.curActiveSlice;
2596 if(slices.includes(target)) {
2597 let i = slices.indexOf(target);
2598 this.hoverSlice(prevAcitve, prevIndex,false);
2599 this.curActiveSlice = target;
2600 this.curActiveSliceIndex = i;
2601 this.hoverSlice(target, i, true, e);
2602 } else {
2603 this.mouseLeave();
2604 }
2605 }
2606
2607 mouseLeave(){
2608 this.hoverSlice(this.curActiveSlice,this.curActiveSliceIndex,false);
2609 }
2610}
2611
2612function normalize(x) {
2613 // Calculates mantissa and exponent of a number
2614 // Returns normalized number and exponent
2615 // https://stackoverflow.com/q/9383593/6495043
2616
2617 if(x===0) {
2618 return [0, 0];
2619 }
2620 if(isNaN(x)) {
2621 return {mantissa: -6755399441055744, exponent: 972};
2622 }
2623 var sig = x > 0 ? 1 : -1;
2624 if(!isFinite(x)) {
2625 return {mantissa: sig * 4503599627370496, exponent: 972};
2626 }
2627
2628 x = Math.abs(x);
2629 var exp = Math.floor(Math.log10(x));
2630 var man = x/Math.pow(10, exp);
2631
2632 return [sig * man, exp];
2633}
2634
2635function getChartRangeIntervals(max, min=0) {
2636 let upperBound = Math.ceil(max);
2637 let lowerBound = Math.floor(min);
2638 let range = upperBound - lowerBound;
2639
2640 let noOfParts = range;
2641 let partSize = 1;
2642
2643 // To avoid too many partitions
2644 if(range > 5) {
2645 if(range % 2 !== 0) {
2646 upperBound++;
2647 // Recalc range
2648 range = upperBound - lowerBound;
2649 }
2650 noOfParts = range/2;
2651 partSize = 2;
2652 }
2653
2654 // Special case: 1 and 2
2655 if(range <= 2) {
2656 noOfParts = 4;
2657 partSize = range/noOfParts;
2658 }
2659
2660 // Special case: 0
2661 if(range === 0) {
2662 noOfParts = 5;
2663 partSize = 1;
2664 }
2665
2666 let intervals = [];
2667 for(var i = 0; i <= noOfParts; i++){
2668 intervals.push(lowerBound + partSize * i);
2669 }
2670 return intervals;
2671}
2672
2673function getChartIntervals(maxValue, minValue=0) {
2674 let [normalMaxValue, exponent] = normalize(maxValue);
2675 let normalMinValue = minValue ? minValue/Math.pow(10, exponent): 0;
2676
2677 // Allow only 7 significant digits
2678 normalMaxValue = normalMaxValue.toFixed(6);
2679
2680 let intervals = getChartRangeIntervals(normalMaxValue, normalMinValue);
2681 intervals = intervals.map(value => value * Math.pow(10, exponent));
2682 return intervals;
2683}
2684
2685function calcChartIntervals(values, withMinimum=false) {
2686 //*** Where the magic happens ***
2687
2688 // Calculates best-fit y intervals from given values
2689 // and returns the interval array
2690
2691 let maxValue = Math.max(...values);
2692 let minValue = Math.min(...values);
2693
2694 // Exponent to be used for pretty print
2695 let exponent = 0, intervals = []; // eslint-disable-line no-unused-vars
2696
2697 function getPositiveFirstIntervals(maxValue, absMinValue) {
2698 let intervals = getChartIntervals(maxValue);
2699
2700 let intervalSize = intervals[1] - intervals[0];
2701
2702 // Then unshift the negative values
2703 let value = 0;
2704 for(var i = 1; value < absMinValue; i++) {
2705 value += intervalSize;
2706 intervals.unshift((-1) * value);
2707 }
2708 return intervals;
2709 }
2710
2711 // CASE I: Both non-negative
2712
2713 if(maxValue >= 0 && minValue >= 0) {
2714 exponent = normalize(maxValue)[1];
2715 if(!withMinimum) {
2716 intervals = getChartIntervals(maxValue);
2717 } else {
2718 intervals = getChartIntervals(maxValue, minValue);
2719 }
2720 }
2721
2722 // CASE II: Only minValue negative
2723
2724 else if(maxValue > 0 && minValue < 0) {
2725 // `withMinimum` irrelevant in this case,
2726 // We'll be handling both sides of zero separately
2727 // (both starting from zero)
2728 // Because ceil() and floor() behave differently
2729 // in those two regions
2730
2731 let absMinValue = Math.abs(minValue);
2732
2733 if(maxValue >= absMinValue) {
2734 exponent = normalize(maxValue)[1];
2735 intervals = getPositiveFirstIntervals(maxValue, absMinValue);
2736 } else {
2737 // Mirror: maxValue => absMinValue, then change sign
2738 exponent = normalize(absMinValue)[1];
2739 let posIntervals = getPositiveFirstIntervals(absMinValue, maxValue);
2740 intervals = posIntervals.map(d => d * (-1));
2741 }
2742
2743 }
2744
2745 // CASE III: Both non-positive
2746
2747 else if(maxValue <= 0 && minValue <= 0) {
2748 // Mirrored Case I:
2749 // Work with positives, then reverse the sign and array
2750
2751 let pseudoMaxValue = Math.abs(minValue);
2752 let pseudoMinValue = Math.abs(maxValue);
2753
2754 exponent = normalize(pseudoMaxValue)[1];
2755 if(!withMinimum) {
2756 intervals = getChartIntervals(pseudoMaxValue);
2757 } else {
2758 intervals = getChartIntervals(pseudoMaxValue, pseudoMinValue);
2759 }
2760
2761 intervals = intervals.reverse().map(d => d * (-1));
2762 }
2763
2764 return intervals;
2765}
2766
2767function getZeroIndex(yPts) {
2768 let zeroIndex;
2769 let interval = getIntervalSize(yPts);
2770 if(yPts.indexOf(0) >= 0) {
2771 // the range has a given zero
2772 // zero-line on the chart
2773 zeroIndex = yPts.indexOf(0);
2774 } else if(yPts[0] > 0) {
2775 // Minimum value is positive
2776 // zero-line is off the chart: below
2777 let min = yPts[0];
2778 zeroIndex = (-1) * min / interval;
2779 } else {
2780 // Maximum value is negative
2781 // zero-line is off the chart: above
2782 let max = yPts[yPts.length - 1];
2783 zeroIndex = (-1) * max / interval + (yPts.length - 1);
2784 }
2785 return zeroIndex;
2786}
2787
2788
2789
2790function getIntervalSize(orderedArray) {
2791 return orderedArray[1] - orderedArray[0];
2792}
2793
2794function getValueRange(orderedArray) {
2795 return orderedArray[orderedArray.length-1] - orderedArray[0];
2796}
2797
2798function scale(val, yAxis) {
2799 return floatTwo(yAxis.zeroLine - val * yAxis.scaleMultiplier);
2800}
2801
2802
2803
2804
2805
2806function getClosestInArray(goal, arr, index = false) {
2807 let closest = arr.reduce(function(prev, curr) {
2808 return (Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev);
2809 }, []);
2810
2811 return index ? arr.indexOf(closest) : closest;
2812}
2813
2814function calcDistribution(values, distributionSize) {
2815 // Assume non-negative values,
2816 // implying distribution minimum at zero
2817
2818 let dataMaxValue = Math.max(...values);
2819
2820 let distributionStep = 1 / (distributionSize - 1);
2821 let distribution = [];
2822
2823 for(var i = 0; i < distributionSize; i++) {
2824 let checkpoint = dataMaxValue * (distributionStep * i);
2825 distribution.push(checkpoint);
2826 }
2827
2828 return distribution;
2829}
2830
2831function getMaxCheckpoint(value, distribution) {
2832 return distribution.filter(d => d < value).length;
2833}
2834
2835const COL_WIDTH = HEATMAP_SQUARE_SIZE + HEATMAP_GUTTER_SIZE;
2836const ROW_HEIGHT = COL_WIDTH;
2837// const DAY_INCR = 1;
2838
2839class Heatmap extends BaseChart {
2840 constructor(parent, options) {
2841 super(parent, options);
2842 this.type = 'heatmap';
2843
2844 this.countLabel = options.countLabel || '';
2845
2846 let validStarts = ['Sunday', 'Monday'];
2847 let startSubDomain = validStarts.includes(options.startSubDomain)
2848 ? options.startSubDomain : 'Sunday';
2849 this.startSubDomainIndex = validStarts.indexOf(startSubDomain);
2850
2851 this.setup();
2852 }
2853
2854 setMeasures(options) {
2855 let m = this.measures;
2856 this.discreteDomains = options.discreteDomains === 0 ? 0 : 1;
2857
2858 m.paddings.top = ROW_HEIGHT * 3;
2859 m.paddings.bottom = 0;
2860 m.legendHeight = ROW_HEIGHT * 2;
2861 m.baseHeight = ROW_HEIGHT * NO_OF_DAYS_IN_WEEK
2862 + getExtraHeight(m);
2863
2864 let d = this.data;
2865 let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0;
2866 this.independentWidth = (getWeeksBetween(d.start, d.end)
2867 + spacing) * COL_WIDTH + getExtraWidth(m);
2868 }
2869
2870 updateWidth() {
2871 let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0;
2872 let noOfWeeks = this.state.noOfWeeks ? this.state.noOfWeeks : 52;
2873 this.baseWidth = (noOfWeeks + spacing) * COL_WIDTH
2874 + getExtraWidth(this.measures);
2875 }
2876
2877 prepareData(data=this.data) {
2878 if(data.start && data.end && data.start > data.end) {
2879 throw new Error('Start date cannot be greater than end date.');
2880 }
2881
2882 if(!data.start) {
2883 data.start = new Date();
2884 data.start.setFullYear( data.start.getFullYear() - 1 );
2885 }
2886 if(!data.end) { data.end = new Date(); }
2887 data.dataPoints = data.dataPoints || {};
2888
2889 if(parseInt(Object.keys(data.dataPoints)[0]) > 100000) {
2890 let points = {};
2891 Object.keys(data.dataPoints).forEach(timestampSec$$1 => {
2892 let date = new Date(timestampSec$$1 * NO_OF_MILLIS);
2893 points[getYyyyMmDd(date)] = data.dataPoints[timestampSec$$1];
2894 });
2895 data.dataPoints = points;
2896 }
2897
2898 return data;
2899 }
2900
2901 calc() {
2902 let s = this.state;
2903
2904 s.start = clone(this.data.start);
2905 s.end = clone(this.data.end);
2906
2907 s.firstWeekStart = clone(s.start);
2908 s.noOfWeeks = getWeeksBetween(s.start, s.end);
2909 s.distribution = calcDistribution(
2910 Object.values(this.data.dataPoints), HEATMAP_DISTRIBUTION_SIZE);
2911
2912 s.domainConfigs = this.getDomains();
2913 }
2914
2915 setupComponents() {
2916 let s = this.state;
2917 let lessCol = this.discreteDomains ? 0 : 1;
2918
2919 let componentConfigs = s.domainConfigs.map((config, i) => [
2920 'heatDomain',
2921 {
2922 index: config.index,
2923 colWidth: COL_WIDTH,
2924 rowHeight: ROW_HEIGHT,
2925 squareSize: HEATMAP_SQUARE_SIZE,
2926 xTranslate: s.domainConfigs
2927 .filter((config, j) => j < i)
2928 .map(config => config.cols.length - lessCol)
2929 .reduce((a, b) => a + b, 0)
2930 * COL_WIDTH
2931 },
2932 function() {
2933 return s.domainConfigs[i];
2934 }.bind(this)
2935
2936 ]);
2937
2938 this.components = new Map(componentConfigs
2939 .map((args, i) => {
2940 let component = getComponent(...args);
2941 return [args[0] + '-' + i, component];
2942 })
2943 );
2944
2945 let y = 0;
2946 DAY_NAMES_SHORT.forEach((dayName, i) => {
2947 if([1, 3, 5].includes(i)) {
2948 let dayText = makeText('subdomain-name', -COL_WIDTH/2, y, dayName,
2949 {
2950 fontSize: HEATMAP_SQUARE_SIZE,
2951 dy: 8,
2952 textAnchor: 'end'
2953 }
2954 );
2955 this.drawArea.appendChild(dayText);
2956 }
2957 y += ROW_HEIGHT;
2958 });
2959 }
2960
2961 update(data) {
2962 if(!data) {
2963 console.error('No data to update.');
2964 }
2965
2966 this.data = this.prepareData(data);
2967 this.draw();
2968 this.bindTooltip();
2969 }
2970
2971 bindTooltip() {
2972 this.container.addEventListener('mousemove', (e) => {
2973 this.components.forEach(comp => {
2974 let daySquares = comp.store;
2975 let daySquare = e.target;
2976 if(daySquares.includes(daySquare)) {
2977
2978 let count = daySquare.getAttribute('data-value');
2979 let dateParts = daySquare.getAttribute('data-date').split('-');
2980
2981 let month = getMonthName(parseInt(dateParts[1])-1, true);
2982
2983 let gOff = this.container.getBoundingClientRect(), pOff = daySquare.getBoundingClientRect();
2984
2985 let width = parseInt(e.target.getAttribute('width'));
2986 let x = pOff.left - gOff.left + width/2;
2987 let y = pOff.top - gOff.top;
2988 let value = count + ' ' + this.countLabel;
2989 let name = ' on ' + month + ' ' + dateParts[0] + ', ' + dateParts[2];
2990
2991 this.tip.setValues(x, y, {name: name, value: value, valueFirst: 1}, []);
2992 this.tip.showTip();
2993 }
2994 });
2995 });
2996 }
2997
2998 renderLegend() {
2999 this.legendArea.textContent = '';
3000 let x = 0;
3001 let y = ROW_HEIGHT;
3002
3003 let lessText = makeText('subdomain-name', x, y, 'Less',
3004 {
3005 fontSize: HEATMAP_SQUARE_SIZE + 1,
3006 dy: 9
3007 }
3008 );
3009 x = (COL_WIDTH * 2) + COL_WIDTH/2;
3010 this.legendArea.appendChild(lessText);
3011
3012 this.colors.slice(0, HEATMAP_DISTRIBUTION_SIZE).map((color, i) => {
3013 const square = heatSquare('heatmap-legend-unit', x + (COL_WIDTH + 3) * i,
3014 y, HEATMAP_SQUARE_SIZE, color);
3015 this.legendArea.appendChild(square);
3016 });
3017
3018 let moreTextX = x + HEATMAP_DISTRIBUTION_SIZE * (COL_WIDTH + 3) + COL_WIDTH/4;
3019 let moreText = makeText('subdomain-name', moreTextX, y, 'More',
3020 {
3021 fontSize: HEATMAP_SQUARE_SIZE + 1,
3022 dy: 9
3023 }
3024 );
3025 this.legendArea.appendChild(moreText);
3026 }
3027
3028 getDomains() {
3029 let s = this.state;
3030 const [startMonth, startYear] = [s.start.getMonth(), s.start.getFullYear()];
3031 const [endMonth, endYear] = [s.end.getMonth(), s.end.getFullYear()];
3032
3033 const noOfMonths = (endMonth - startMonth + 1) + (endYear - startYear) * 12;
3034
3035 let domainConfigs = [];
3036
3037 let startOfMonth = clone(s.start);
3038 for(var i = 0; i < noOfMonths; i++) {
3039 let endDate = s.end;
3040 if(!areInSameMonth(startOfMonth, s.end)) {
3041 let [month, year] = [startOfMonth.getMonth(), startOfMonth.getFullYear()];
3042 endDate = getLastDateInMonth(month, year);
3043 }
3044 domainConfigs.push(this.getDomainConfig(startOfMonth, endDate));
3045
3046 addDays(endDate, 1);
3047 startOfMonth = endDate;
3048 }
3049
3050 return domainConfigs;
3051 }
3052
3053 getDomainConfig(startDate, endDate='') {
3054 let [month, year] = [startDate.getMonth(), startDate.getFullYear()];
3055 let startOfWeek = setDayToSunday(startDate); // TODO: Monday as well
3056 endDate = clone(endDate) || getLastDateInMonth(month, year);
3057
3058 let domainConfig = {
3059 index: month,
3060 cols: []
3061 };
3062
3063 addDays(endDate, 1);
3064 let noOfMonthWeeks = getWeeksBetween(startOfWeek, endDate);
3065
3066 let cols = [], col;
3067 for(var i = 0; i < noOfMonthWeeks; i++) {
3068 col = this.getCol(startOfWeek, month);
3069 cols.push(col);
3070
3071 startOfWeek = new Date(col[NO_OF_DAYS_IN_WEEK - 1].yyyyMmDd);
3072 addDays(startOfWeek, 1);
3073 }
3074
3075 if(col[NO_OF_DAYS_IN_WEEK - 1].dataValue !== undefined) {
3076 addDays(startOfWeek, 1);
3077 cols.push(this.getCol(startOfWeek, month, true));
3078 }
3079
3080 domainConfig.cols = cols;
3081
3082 return domainConfig;
3083 }
3084
3085 getCol(startDate, month, empty = false) {
3086 let s = this.state;
3087
3088 // startDate is the start of week
3089 let currentDate = clone(startDate);
3090 let col = [];
3091
3092 for(var i = 0; i < NO_OF_DAYS_IN_WEEK; i++, addDays(currentDate, 1)) {
3093 let config = {};
3094
3095 // Non-generic adjustment for entire heatmap, needs state
3096 let currentDateWithinData = currentDate >= s.start && currentDate <= s.end;
3097
3098 if(empty || currentDate.getMonth() !== month || !currentDateWithinData) {
3099 config.yyyyMmDd = getYyyyMmDd(currentDate);
3100 } else {
3101 config = this.getSubDomainConfig(currentDate);
3102 }
3103 col.push(config);
3104 }
3105
3106 return col;
3107 }
3108
3109 getSubDomainConfig(date) {
3110 let yyyyMmDd = getYyyyMmDd(date);
3111 let dataValue = this.data.dataPoints[yyyyMmDd];
3112 let config = {
3113 yyyyMmDd: yyyyMmDd,
3114 dataValue: dataValue || 0,
3115 fill: this.colors[getMaxCheckpoint(dataValue, this.state.distribution)]
3116 };
3117 return config;
3118 }
3119}
3120
3121function dataPrep(data, type) {
3122 data.labels = data.labels || [];
3123
3124 let datasetLength = data.labels.length;
3125
3126 // Datasets
3127 let datasets = data.datasets;
3128 let zeroArray = new Array(datasetLength).fill(0);
3129 if(!datasets) {
3130 // default
3131 datasets = [{
3132 values: zeroArray
3133 }];
3134 }
3135
3136 datasets.map(d=> {
3137 // Set values
3138 if(!d.values) {
3139 d.values = zeroArray;
3140 } else {
3141 // Check for non values
3142 let vals = d.values;
3143 vals = vals.map(val => (!isNaN(val) ? val : 0));
3144
3145 // Trim or extend
3146 if(vals.length > datasetLength) {
3147 vals = vals.slice(0, datasetLength);
3148 } else {
3149 vals = fillArray(vals, datasetLength - vals.length, 0);
3150 }
3151 }
3152
3153 // Set labels
3154 //
3155
3156 // Set type
3157 if(!d.chartType ) {
3158 if(!AXIS_DATASET_CHART_TYPES.includes(type)) type === DEFAULT_AXIS_CHART_TYPE;
3159 d.chartType = type;
3160 }
3161
3162 });
3163
3164 // Markers
3165
3166 // Regions
3167 // data.yRegions = data.yRegions || [];
3168 if(data.yRegions) {
3169 data.yRegions.map(d => {
3170 if(d.end < d.start) {
3171 [d.start, d.end] = [d.end, d.start];
3172 }
3173 });
3174 }
3175
3176 return data;
3177}
3178
3179function zeroDataPrep(realData) {
3180 let datasetLength = realData.labels.length;
3181 let zeroArray = new Array(datasetLength).fill(0);
3182
3183 let zeroData = {
3184 labels: realData.labels.slice(0, -1),
3185 datasets: realData.datasets.map(d => {
3186 return {
3187 name: '',
3188 values: zeroArray.slice(0, -1),
3189 chartType: d.chartType
3190 };
3191 }),
3192 };
3193
3194 if(realData.yMarkers) {
3195 zeroData.yMarkers = [
3196 {
3197 value: 0,
3198 label: ''
3199 }
3200 ];
3201 }
3202
3203 if(realData.yRegions) {
3204 zeroData.yRegions = [
3205 {
3206 start: 0,
3207 end: 0,
3208 label: ''
3209 }
3210 ];
3211 }
3212
3213 return zeroData;
3214}
3215
3216function getShortenedLabels(chartWidth, labels=[], isSeries=true) {
3217 let allowedSpace = chartWidth / labels.length;
3218 if(allowedSpace <= 0) allowedSpace = 1;
3219 let allowedLetters = allowedSpace / DEFAULT_CHAR_WIDTH;
3220
3221 let calcLabels = labels.map((label, i) => {
3222 label += "";
3223 if(label.length > allowedLetters) {
3224
3225 if(!isSeries) {
3226 if(allowedLetters-3 > 0) {
3227 label = label.slice(0, allowedLetters-3) + " ...";
3228 } else {
3229 label = label.slice(0, allowedLetters) + '..';
3230 }
3231 } else {
3232 let multiple = Math.ceil(label.length/allowedLetters);
3233 if(i % multiple !== 0) {
3234 label = "";
3235 }
3236 }
3237 }
3238 return label;
3239 });
3240
3241 return calcLabels;
3242}
3243
3244class AxisChart extends BaseChart {
3245 constructor(parent, args) {
3246 super(parent, args);
3247
3248 this.barOptions = args.barOptions || {};
3249 this.lineOptions = args.lineOptions || {};
3250
3251 this.type = args.type || 'line';
3252 this.init = 1;
3253
3254 this.setup();
3255 }
3256
3257 setMeasures() {
3258 if(this.data.datasets.length <= 1) {
3259 this.config.showLegend = 0;
3260 this.measures.paddings.bottom = 30;
3261 }
3262 }
3263
3264 configure(options) {
3265 super.configure(options);
3266
3267 options.axisOptions = options.axisOptions || {};
3268 options.tooltipOptions = options.tooltipOptions || {};
3269
3270 this.config.xAxisMode = options.axisOptions.xAxisMode || 'span';
3271 this.config.yAxisMode = options.axisOptions.yAxisMode || 'span';
3272 this.config.xIsSeries = options.axisOptions.xIsSeries || 0;
3273 this.config.shortenYAxisNumbers = options.axisOptions.shortenYAxisNumbers || 0;
3274
3275 this.config.formatTooltipX = options.tooltipOptions.formatTooltipX;
3276 this.config.formatTooltipY = options.tooltipOptions.formatTooltipY;
3277
3278 this.config.valuesOverPoints = options.valuesOverPoints;
3279 }
3280
3281 prepareData(data=this.data) {
3282 return dataPrep(data, this.type);
3283 }
3284
3285 prepareFirstData(data=this.data) {
3286 return zeroDataPrep(data);
3287 }
3288
3289 calc(onlyWidthChange = false) {
3290 this.calcXPositions();
3291 if(!onlyWidthChange) {
3292 this.calcYAxisParameters(this.getAllYValues(), this.type === 'line');
3293 }
3294 this.makeDataByIndex();
3295 }
3296
3297 calcXPositions() {
3298 let s = this.state;
3299 let labels = this.data.labels;
3300 s.datasetLength = labels.length;
3301
3302 s.unitWidth = this.width/(s.datasetLength);
3303 // Default, as per bar, and mixed. Only line will be a special case
3304 s.xOffset = s.unitWidth/2;
3305
3306 // // For a pure Line Chart
3307 // s.unitWidth = this.width/(s.datasetLength - 1);
3308 // s.xOffset = 0;
3309
3310 s.xAxis = {
3311 labels: labels,
3312 positions: labels.map((d, i) =>
3313 floatTwo(s.xOffset + i * s.unitWidth)
3314 )
3315 };
3316 }
3317
3318 calcYAxisParameters(dataValues, withMinimum = 'false') {
3319 const yPts = calcChartIntervals(dataValues, withMinimum);
3320 const scaleMultiplier = this.height / getValueRange(yPts);
3321 const intervalHeight = getIntervalSize(yPts) * scaleMultiplier;
3322 const zeroLine = this.height - (getZeroIndex(yPts) * intervalHeight);
3323
3324 this.state.yAxis = {
3325 labels: yPts,
3326 positions: yPts.map(d => zeroLine - d * scaleMultiplier),
3327 scaleMultiplier: scaleMultiplier,
3328 zeroLine: zeroLine,
3329 };
3330
3331 // Dependent if above changes
3332 this.calcDatasetPoints();
3333 this.calcYExtremes();
3334 this.calcYRegions();
3335 }
3336
3337 calcDatasetPoints() {
3338 let s = this.state;
3339 let scaleAll = values => values.map(val => scale(val, s.yAxis));
3340
3341 s.datasets = this.data.datasets.map((d, i) => {
3342 let values = d.values;
3343 let cumulativeYs = d.cumulativeYs || [];
3344 return {
3345 name: d.name,
3346 index: i,
3347 chartType: d.chartType,
3348
3349 values: values,
3350 yPositions: scaleAll(values),
3351
3352 cumulativeYs: cumulativeYs,
3353 cumulativeYPos: scaleAll(cumulativeYs),
3354 };
3355 });
3356 }
3357
3358 calcYExtremes() {
3359 let s = this.state;
3360 if(this.barOptions.stacked) {
3361 s.yExtremes = s.datasets[s.datasets.length - 1].cumulativeYPos;
3362 return;
3363 }
3364 s.yExtremes = new Array(s.datasetLength).fill(9999);
3365 s.datasets.map(d => {
3366 d.yPositions.map((pos, j) => {
3367 if(pos < s.yExtremes[j]) {
3368 s.yExtremes[j] = pos;
3369 }
3370 });
3371 });
3372 }
3373
3374 calcYRegions() {
3375 let s = this.state;
3376 if(this.data.yMarkers) {
3377 this.state.yMarkers = this.data.yMarkers.map(d => {
3378 d.position = scale(d.value, s.yAxis);
3379 if(!d.options) d.options = {};
3380 // if(!d.label.includes(':')) {
3381 // d.label += ': ' + d.value;
3382 // }
3383 return d;
3384 });
3385 }
3386 if(this.data.yRegions) {
3387 this.state.yRegions = this.data.yRegions.map(d => {
3388 d.startPos = scale(d.start, s.yAxis);
3389 d.endPos = scale(d.end, s.yAxis);
3390 if(!d.options) d.options = {};
3391 return d;
3392 });
3393 }
3394 }
3395
3396 getAllYValues() {
3397 let key = 'values';
3398
3399 if(this.barOptions.stacked) {
3400 key = 'cumulativeYs';
3401 let cumulative = new Array(this.state.datasetLength).fill(0);
3402 this.data.datasets.map((d, i) => {
3403 let values = this.data.datasets[i].values;
3404 d[key] = cumulative = cumulative.map((c, i) => c + values[i]);
3405 });
3406 }
3407
3408 let allValueLists = this.data.datasets.map(d => d[key]);
3409 if(this.data.yMarkers) {
3410 allValueLists.push(this.data.yMarkers.map(d => d.value));
3411 }
3412 if(this.data.yRegions) {
3413 this.data.yRegions.map(d => {
3414 allValueLists.push([d.end, d.start]);
3415 });
3416 }
3417
3418 return [].concat(...allValueLists);
3419 }
3420
3421 setupComponents() {
3422 let componentConfigs = [
3423 [
3424 'yAxis',
3425 {
3426 mode: this.config.yAxisMode,
3427 width: this.width,
3428 shortenNumbers: this.config.shortenYAxisNumbers
3429 // pos: 'right'
3430 },
3431 function() {
3432 return this.state.yAxis;
3433 }.bind(this)
3434 ],
3435
3436 [
3437 'xAxis',
3438 {
3439 mode: this.config.xAxisMode,
3440 height: this.height,
3441 // pos: 'right'
3442 },
3443 function() {
3444 let s = this.state;
3445 s.xAxis.calcLabels = getShortenedLabels(this.width,
3446 s.xAxis.labels, this.config.xIsSeries);
3447
3448 return s.xAxis;
3449 }.bind(this)
3450 ],
3451
3452 [
3453 'yRegions',
3454 {
3455 width: this.width,
3456 pos: 'right'
3457 },
3458 function() {
3459 return this.state.yRegions;
3460 }.bind(this)
3461 ],
3462 ];
3463
3464 let barDatasets = this.state.datasets.filter(d => d.chartType === 'bar');
3465 let lineDatasets = this.state.datasets.filter(d => d.chartType === 'line');
3466
3467 let barsConfigs = barDatasets.map(d => {
3468 let index = d.index;
3469 return [
3470 'barGraph' + '-' + d.index,
3471 {
3472 index: index,
3473 color: this.colors[index],
3474 stacked: this.barOptions.stacked,
3475
3476 // same for all datasets
3477 valuesOverPoints: this.config.valuesOverPoints,
3478 minHeight: this.height * MIN_BAR_PERCENT_HEIGHT,
3479 },
3480 function() {
3481 let s = this.state;
3482 let d = s.datasets[index];
3483 let stacked = this.barOptions.stacked;
3484
3485 let spaceRatio = this.barOptions.spaceRatio || BAR_CHART_SPACE_RATIO;
3486 let barsWidth = s.unitWidth * (1 - spaceRatio);
3487 let barWidth = barsWidth/(stacked ? 1 : barDatasets.length);
3488
3489 let xPositions = s.xAxis.positions.map(x => x - barsWidth/2);
3490 if(!stacked) {
3491 xPositions = xPositions.map(p => p + barWidth * index);
3492 }
3493
3494 let labels = new Array(s.datasetLength).fill('');
3495 if(this.config.valuesOverPoints) {
3496 if(stacked && d.index === s.datasets.length - 1) {
3497 labels = d.cumulativeYs;
3498 } else {
3499 labels = d.values;
3500 }
3501 }
3502
3503 let offsets = new Array(s.datasetLength).fill(0);
3504 if(stacked) {
3505 offsets = d.yPositions.map((y, j) => y - d.cumulativeYPos[j]);
3506 }
3507
3508 return {
3509 xPositions: xPositions,
3510 yPositions: d.yPositions,
3511 offsets: offsets,
3512 // values: d.values,
3513 labels: labels,
3514
3515 zeroLine: s.yAxis.zeroLine,
3516 barsWidth: barsWidth,
3517 barWidth: barWidth,
3518 };
3519 }.bind(this)
3520 ];
3521 });
3522
3523 let lineConfigs = lineDatasets.map(d => {
3524 let index = d.index;
3525 return [
3526 'lineGraph' + '-' + d.index,
3527 {
3528 index: index,
3529 color: this.colors[index],
3530 svgDefs: this.svgDefs,
3531 heatline: this.lineOptions.heatline,
3532 regionFill: this.lineOptions.regionFill,
3533 spline: this.lineOptions.spline,
3534 hideDots: this.lineOptions.hideDots,
3535 hideLine: this.lineOptions.hideLine,
3536
3537 // same for all datasets
3538 valuesOverPoints: this.config.valuesOverPoints,
3539 },
3540 function() {
3541 let s = this.state;
3542 let d = s.datasets[index];
3543 let minLine = s.yAxis.positions[0] < s.yAxis.zeroLine
3544 ? s.yAxis.positions[0] : s.yAxis.zeroLine;
3545
3546 return {
3547 xPositions: s.xAxis.positions,
3548 yPositions: d.yPositions,
3549
3550 values: d.values,
3551
3552 zeroLine: minLine,
3553 radius: this.lineOptions.dotSize || LINE_CHART_DOT_SIZE,
3554 };
3555 }.bind(this)
3556 ];
3557 });
3558
3559 let markerConfigs = [
3560 [
3561 'yMarkers',
3562 {
3563 width: this.width,
3564 pos: 'right'
3565 },
3566 function() {
3567 return this.state.yMarkers;
3568 }.bind(this)
3569 ]
3570 ];
3571
3572 componentConfigs = componentConfigs.concat(barsConfigs, lineConfigs, markerConfigs);
3573
3574 let optionals = ['yMarkers', 'yRegions'];
3575 this.dataUnitComponents = [];
3576
3577 this.components = new Map(componentConfigs
3578 .filter(args => !optionals.includes(args[0]) || this.state[args[0]])
3579 .map(args => {
3580 let component = getComponent(...args);
3581 if(args[0].includes('lineGraph') || args[0].includes('barGraph')) {
3582 this.dataUnitComponents.push(component);
3583 }
3584 return [args[0], component];
3585 }));
3586 }
3587
3588 makeDataByIndex() {
3589 this.dataByIndex = {};
3590
3591 let s = this.state;
3592 let formatX = this.config.formatTooltipX;
3593 let formatY = this.config.formatTooltipY;
3594 let titles = s.xAxis.labels;
3595
3596 titles.map((label, index) => {
3597 let values = this.state.datasets.map((set, i) => {
3598 let value = set.values[index];
3599 return {
3600 title: set.name,
3601 value: value,
3602 yPos: set.yPositions[index],
3603 color: this.colors[i],
3604 formatted: formatY ? formatY(value) : value,
3605 };
3606 });
3607
3608 this.dataByIndex[index] = {
3609 label: label,
3610 formattedLabel: formatX ? formatX(label) : label,
3611 xPos: s.xAxis.positions[index],
3612 values: values,
3613 yExtreme: s.yExtremes[index],
3614 };
3615 });
3616 }
3617
3618 bindTooltip() {
3619 // NOTE: could be in tooltip itself, as it is a given functionality for its parent
3620 this.container.addEventListener('mousemove', (e) => {
3621 let m = this.measures;
3622 let o = getOffset(this.container);
3623 let relX = e.pageX - o.left - getLeftOffset(m);
3624 let relY = e.pageY - o.top;
3625
3626 if(relY < this.height + getTopOffset(m)
3627 && relY > getTopOffset(m)) {
3628 this.mapTooltipXPosition(relX);
3629 } else {
3630 this.tip.hideTip();
3631 }
3632 });
3633 }
3634
3635 mapTooltipXPosition(relX) {
3636 let s = this.state;
3637 if(!s.yExtremes) return;
3638
3639 let index = getClosestInArray(relX, s.xAxis.positions, true);
3640 let dbi = this.dataByIndex[index];
3641
3642 this.tip.setValues(
3643 dbi.xPos + this.tip.offset.x,
3644 dbi.yExtreme + this.tip.offset.y,
3645 {name: dbi.formattedLabel, value: ''},
3646 dbi.values,
3647 index
3648 );
3649
3650 this.tip.showTip();
3651 }
3652
3653 renderLegend() {
3654 let s = this.data;
3655 if(s.datasets.length > 1) {
3656 this.legendArea.textContent = '';
3657 s.datasets.map((d, i) => {
3658 let barWidth = AXIS_LEGEND_BAR_SIZE;
3659 // let rightEndPoint = this.baseWidth - this.measures.margins.left - this.measures.margins.right;
3660 // let multiplier = s.datasets.length - i;
3661 let rect = legendBar(
3662 // rightEndPoint - multiplier * barWidth, // To right align
3663 barWidth * i,
3664 '0',
3665 barWidth,
3666 this.colors[i],
3667 d.name,
3668 this.config.truncateLegends);
3669 this.legendArea.appendChild(rect);
3670 });
3671 }
3672 }
3673
3674
3675
3676 // Overlay
3677 makeOverlay() {
3678 if(this.init) {
3679 this.init = 0;
3680 return;
3681 }
3682 if(this.overlayGuides) {
3683 this.overlayGuides.forEach(g => {
3684 let o = g.overlay;
3685 o.parentNode.removeChild(o);
3686 });
3687 }
3688
3689 this.overlayGuides = this.dataUnitComponents.map(c => {
3690 return {
3691 type: c.unitType,
3692 overlay: undefined,
3693 units: c.units,
3694 };
3695 });
3696
3697 if(this.state.currentIndex === undefined) {
3698 this.state.currentIndex = this.state.datasetLength - 1;
3699 }
3700
3701 // Render overlays
3702 this.overlayGuides.map(d => {
3703 let currentUnit = d.units[this.state.currentIndex];
3704
3705 d.overlay = makeOverlay[d.type](currentUnit);
3706 this.drawArea.appendChild(d.overlay);
3707 });
3708 }
3709
3710 updateOverlayGuides() {
3711 if(this.overlayGuides) {
3712 this.overlayGuides.forEach(g => {
3713 let o = g.overlay;
3714 o.parentNode.removeChild(o);
3715 });
3716 }
3717 }
3718
3719 bindOverlay() {
3720 this.parent.addEventListener('data-select', () => {
3721 this.updateOverlay();
3722 });
3723 }
3724
3725 bindUnits() {
3726 this.dataUnitComponents.map(c => {
3727 c.units.map(unit => {
3728 unit.addEventListener('click', () => {
3729 let index = unit.getAttribute('data-point-index');
3730 this.setCurrentDataPoint(index);
3731 });
3732 });
3733 });
3734
3735 // Note: Doesn't work as tooltip is absolutely positioned
3736 this.tip.container.addEventListener('click', () => {
3737 let index = this.tip.container.getAttribute('data-point-index');
3738 this.setCurrentDataPoint(index);
3739 });
3740 }
3741
3742 updateOverlay() {
3743 this.overlayGuides.map(d => {
3744 let currentUnit = d.units[this.state.currentIndex];
3745 updateOverlay[d.type](currentUnit, d.overlay);
3746 });
3747 }
3748
3749 onLeftArrow() {
3750 this.setCurrentDataPoint(this.state.currentIndex - 1);
3751 }
3752
3753 onRightArrow() {
3754 this.setCurrentDataPoint(this.state.currentIndex + 1);
3755 }
3756
3757 getDataPoint(index=this.state.currentIndex) {
3758 let s = this.state;
3759 let data_point = {
3760 index: index,
3761 label: s.xAxis.labels[index],
3762 values: s.datasets.map(d => d.values[index])
3763 };
3764 return data_point;
3765 }
3766
3767 setCurrentDataPoint(index) {
3768 let s = this.state;
3769 index = parseInt(index);
3770 if(index < 0) index = 0;
3771 if(index >= s.xAxis.labels.length) index = s.xAxis.labels.length - 1;
3772 if(index === s.currentIndex) return;
3773 s.currentIndex = index;
3774 fire(this.parent, "data-select", this.getDataPoint());
3775 }
3776
3777
3778
3779 // API
3780 addDataPoint(label, datasetValues, index=this.state.datasetLength) {
3781 super.addDataPoint(label, datasetValues, index);
3782 this.data.labels.splice(index, 0, label);
3783 this.data.datasets.map((d, i) => {
3784 d.values.splice(index, 0, datasetValues[i]);
3785 });
3786 this.update(this.data);
3787 }
3788
3789 removeDataPoint(index = this.state.datasetLength-1) {
3790 if (this.data.labels.length <= 1) {
3791 return;
3792 }
3793 super.removeDataPoint(index);
3794 this.data.labels.splice(index, 1);
3795 this.data.datasets.map(d => {
3796 d.values.splice(index, 1);
3797 });
3798 this.update(this.data);
3799 }
3800
3801 updateDataset(datasetValues, index=0) {
3802 this.data.datasets[index].values = datasetValues;
3803 this.update(this.data);
3804 }
3805 // addDataset(dataset, index) {}
3806 // removeDataset(index = 0) {}
3807
3808 updateDatasets(datasets) {
3809 this.data.datasets.map((d, i) => {
3810 if(datasets[i]) {
3811 d.values = datasets[i];
3812 }
3813 });
3814 this.update(this.data);
3815 }
3816
3817 // updateDataPoint(dataPoint, index = 0) {}
3818 // addDataPoint(dataPoint, index = 0) {}
3819 // removeDataPoint(index = 0) {}
3820}
3821
3822class DonutChart extends AggregationChart {
3823 constructor(parent, args) {
3824 super(parent, args);
3825 this.type = 'donut';
3826 this.initTimeout = 0;
3827 this.init = 1;
3828
3829 this.setup();
3830 }
3831
3832 configure(args) {
3833 super.configure(args);
3834 this.mouseMove = this.mouseMove.bind(this);
3835 this.mouseLeave = this.mouseLeave.bind(this);
3836
3837 this.hoverRadio = args.hoverRadio || 0.1;
3838 this.config.startAngle = args.startAngle || 0;
3839
3840 this.clockWise = args.clockWise || false;
3841 this.strokeWidth = args.strokeWidth || 30;
3842 }
3843
3844 calc() {
3845 super.calc();
3846 let s = this.state;
3847 this.radius =
3848 this.height > this.width
3849 ? this.center.x - this.strokeWidth / 2
3850 : this.center.y - this.strokeWidth / 2;
3851
3852 const { radius, clockWise } = this;
3853
3854 const prevSlicesProperties = s.slicesProperties || [];
3855 s.sliceStrings = [];
3856 s.slicesProperties = [];
3857 let curAngle = 180 - this.config.startAngle;
3858
3859 s.sliceTotals.map((total, i) => {
3860 const startAngle = curAngle;
3861 const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE;
3862 const largeArc = originDiffAngle > 180 ? 1: 0;
3863 const diffAngle = clockWise ? -originDiffAngle : originDiffAngle;
3864 const endAngle = curAngle = curAngle + diffAngle;
3865 const startPosition = getPositionByAngle(startAngle, radius);
3866 const endPosition = getPositionByAngle(endAngle, radius);
3867
3868 const prevProperty = this.init && prevSlicesProperties[i];
3869
3870 let curStart,curEnd;
3871 if(this.init) {
3872 curStart = prevProperty ? prevProperty.startPosition : startPosition;
3873 curEnd = prevProperty ? prevProperty.endPosition : startPosition;
3874 } else {
3875 curStart = startPosition;
3876 curEnd = endPosition;
3877 }
3878 const curPath =
3879 originDiffAngle === 360
3880 ? makeStrokeCircleStr(curStart, curEnd, this.center, this.radius, this.clockWise, largeArc)
3881 : makeArcStrokePathStr(curStart, curEnd, this.center, this.radius, this.clockWise, largeArc);
3882
3883 s.sliceStrings.push(curPath);
3884 s.slicesProperties.push({
3885 startPosition,
3886 endPosition,
3887 value: total,
3888 total: s.grandTotal,
3889 startAngle,
3890 endAngle,
3891 angle: diffAngle
3892 });
3893
3894 });
3895 this.init = 0;
3896 }
3897
3898 setupComponents() {
3899 let s = this.state;
3900
3901 let componentConfigs = [
3902 [
3903 'donutSlices',
3904 { },
3905 function() {
3906 return {
3907 sliceStrings: s.sliceStrings,
3908 colors: this.colors,
3909 strokeWidth: this.strokeWidth,
3910 };
3911 }.bind(this)
3912 ]
3913 ];
3914
3915 this.components = new Map(componentConfigs
3916 .map(args => {
3917 let component = getComponent(...args);
3918 return [args[0], component];
3919 }));
3920 }
3921
3922 calTranslateByAngle(property){
3923 const{ radius, hoverRadio } = this;
3924 const position = getPositionByAngle(property.startAngle+(property.angle / 2),radius);
3925 return `translate3d(${(position.x) * hoverRadio}px,${(position.y) * hoverRadio}px,0)`;
3926 }
3927
3928 hoverSlice(path,i,flag,e){
3929 if(!path) return;
3930 const color = this.colors[i];
3931 if(flag) {
3932 transform(path, this.calTranslateByAngle(this.state.slicesProperties[i]));
3933 path.style.stroke = lightenDarkenColor(color, 50);
3934 let g_off = getOffset(this.svg);
3935 let x = e.pageX - g_off.left + 10;
3936 let y = e.pageY - g_off.top - 10;
3937 let title = (this.formatted_labels && this.formatted_labels.length > 0
3938 ? this.formatted_labels[i] : this.state.labels[i]) + ': ';
3939 let percent = (this.state.sliceTotals[i] * 100 / this.state.grandTotal).toFixed(1);
3940 this.tip.setValues(x, y, {name: title, value: percent + "%"});
3941 this.tip.showTip();
3942 } else {
3943 transform(path,'translate3d(0,0,0)');
3944 this.tip.hideTip();
3945 path.style.stroke = color;
3946 }
3947 }
3948
3949 bindTooltip() {
3950 this.container.addEventListener('mousemove', this.mouseMove);
3951 this.container.addEventListener('mouseleave', this.mouseLeave);
3952 }
3953
3954 mouseMove(e){
3955 const target = e.target;
3956 let slices = this.components.get('donutSlices').store;
3957 let prevIndex = this.curActiveSliceIndex;
3958 let prevAcitve = this.curActiveSlice;
3959 if(slices.includes(target)) {
3960 let i = slices.indexOf(target);
3961 this.hoverSlice(prevAcitve, prevIndex,false);
3962 this.curActiveSlice = target;
3963 this.curActiveSliceIndex = i;
3964 this.hoverSlice(target, i, true, e);
3965 } else {
3966 this.mouseLeave();
3967 }
3968 }
3969
3970 mouseLeave(){
3971 this.hoverSlice(this.curActiveSlice,this.curActiveSliceIndex,false);
3972 }
3973}
3974
3975// import MultiAxisChart from './charts/MultiAxisChart';
3976const chartTypes = {
3977 bar: AxisChart,
3978 line: AxisChart,
3979 // multiaxis: MultiAxisChart,
3980 percentage: PercentageChart,
3981 heatmap: Heatmap,
3982 pie: PieChart,
3983 donut: DonutChart,
3984};
3985
3986function getChartByType(chartType = 'line', parent, options) {
3987 if (chartType === 'axis-mixed') {
3988 options.type = 'line';
3989 return new AxisChart(parent, options);
3990 }
3991
3992 if (!chartTypes[chartType]) {
3993 console.error("Undefined chart type: " + chartType);
3994 return;
3995 }
3996
3997 return new chartTypes[chartType](parent, options);
3998}
3999
4000class Chart {
4001 constructor(parent, options) {
4002 return getChartByType(options.type, parent, options);
4003 }
4004}
4005
4006export { Chart, PercentageChart, PieChart, Heatmap, AxisChart };