"}function u(e){l+=""+t(e)+">"}function c(e){("start"===e.event?o:u)(e.node)}for(var s=0,l="",f=[];e.length||r.length;){var g=a();if(l+=n(i.substring(s,g[0].offset)),s=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=a();while(g===e&&g.length&&g[0].offset===s);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return l+n(i.substr(s))}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(i,a){if(!i.compiled){if(i.compiled=!0,i.k=i.k||i.bK,i.k){var u={},c=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");u[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof i.k?c("keyword",i.k):E(i.k).forEach(function(e){c(e,i.k[e])}),i.k=u}i.lR=t(i.l||/\w+/,!0),a&&(i.bK&&(i.b="\\b("+i.bK.split(" ").join("|")+")\\b"),i.b||(i.b=/\B|\b/),i.bR=t(i.b),i.e||i.eW||(i.e=/\B|\b/),i.e&&(i.eR=t(i.e)),i.tE=n(i.e)||"",i.eW&&a.tE&&(i.tE+=(i.e?"|":"")+a.tE)),i.i&&(i.iR=t(i.i)),null==i.r&&(i.r=1),i.c||(i.c=[]);var s=[];i.c.forEach(function(e){e.v?e.v.forEach(function(n){s.push(o(e,n))}):s.push("self"===e?i:e)}),i.c=s,i.c.forEach(function(e){r(e,i)}),i.starts&&r(i.starts,a);var l=i.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([i.tE,i.i]).map(n).filter(Boolean);i.t=l.length?t(l.join("|"),!0):{exec:function(){return null}}}}r(e)}function l(e,t,i,a){function o(e,n){var t,i;for(t=0,i=n.c.length;i>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!i&&r(n.iR,e)}function g(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function h(e,n,t,r){var i=r?"":y.classPrefix,a='',a+n+o}function p(){var e,t,r,i;if(!E.k)return n(B);for(i="",t=0,E.lR.lastIndex=0,r=E.lR.exec(B);r;)i+=n(B.substring(t,r.index)),e=g(E,r),e?(M+=e[1],i+=h(e[0],n(r[0]))):i+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(B);return i+n(B.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!x[E.sL])return n(B);var t=e?l(E.sL,B,!0,L[E.sL]):f(B,E.sL.length?E.sL:void 0);return E.r>0&&(M+=t.r),e&&(L[E.sL]=t.top),h(t.language,t.value,!1,!0)}function b(){k+=null!=E.sL?d():p(),B=""}function v(e){k+=e.cN?h(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(B+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?B+=n:(t.eB&&(B+=n),b(),t.rB||t.eB||(B=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var i=E;i.skip?B+=n:(i.rE||i.eE||(B+=n),b(),i.eE&&(B=n));do E.cN&&(k+=C),E.skip||(M+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),i.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return B+=n,n.length||1}var N=R(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var w,E=a||N,L={},k="";for(w=E;w!==N;w=w.parent)w.cN&&(k=h(w.cN,"",!0)+k);var B="",M=0;try{for(var I,j,O=0;;){if(E.t.lastIndex=O,I=E.t.exec(t),!I)break;j=m(t.substring(O,I.index),I[0]),O=I.index+j}for(m(t.substr(O)),w=E;w.parent;w=w.parent)w.cN&&(k+=C);return{r:M,value:k,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function f(e,t){t=t||y.languages||E(x);var r={r:0,value:n(e)},i=r;return t.filter(R).forEach(function(n){var t=l(n,e,!1);t.language=n,t.r>i.r&&(i=t),t.r>r.r&&(i=r,r=t)}),i.language&&(r.second_best=i),r}function g(e){return y.tabReplace||y.useBR?e.replace(M,function(e,n){return y.useBR&&"\n"===e?" ":y.tabReplace?n.replace(/\t/g,y.tabReplace):void 0}):e}function h(e,n,t){var r=n?L[n]:t,i=[e.trim()];return e.match(/\bhljs\b/)||i.push("hljs"),-1===e.indexOf(r)&&i.push(r),i.join(" ").trim()}function p(e){var n,t,r,o,s,p=a(e);i(p)||(y.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(/ /g,"\n")):n=e,s=n.textContent,r=p?l(p,s,!0):f(s),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),s)),r.value=g(r.value),e.innerHTML=r.value,e.className=h(e.className,p,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function d(e){y=o(y,e)}function b(){if(!b.called){b.called=!0;var e=document.querySelectorAll("pre code");w.forEach.call(e,p)}}function v(){addEventListener("DOMContentLoaded",b,!1),addEventListener("load",b,!1)}function m(n,t){var r=x[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function N(){return E(x)}function R(e){return e=(e||"").toLowerCase(),x[e]||x[L[e]]}var w=[],E=Object.keys,x={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C=" ",y={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},I={"&":"&","<":"<",">":">"};return e.highlight=l,e.highlightAuto=f,e.fixMarkup=g,e.highlightBlock=p,e.configure=d,e.initHighlighting=b,e.initHighlightingOnLoad=v,e.registerLanguage=m,e.listLanguages=N,e.getLanguage=R,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(n,t,r){var i=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return i.c.push(e.PWM),i.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),i},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("xml",function(s){var e="[A-Za-z0-9\\._:-]+",t={eW:!0,i:/,r:0,c:[{cN:"attr",b:e,r:0},{b:/=\s*/,r:0,c:[{cN:"string",endsParent:!0,v:[{b:/"/,e:/"/},{b:/'/,e:/'/},{b:/[^\s"'=<>`]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist"],cI:!0,c:[{cN:"meta",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},s.C("",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{b:/<\?(php)?/,e:/\?>/,sL:"php",c:[{b:"/\\*",e:"\\*/",skip:!0}]},{cN:"tag",b:"",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},{cN:"meta",v:[{b:/<\?xml/,e:/\?>/,r:10},{b:/<\?\w+/,e:/\?>/}]},{cN:"tag",b:"?",e:"/?>",c:[{cN:"name",b:/[^\/><\s]+/,r:0},t]}]}});hljs.registerLanguage("javascript",function(e){var r="[A-Za-z$_][0-9A-Za-z$_]*",t={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},a={cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},n={cN:"subst",b:"\\$\\{",e:"\\}",k:t,c:[]},c={cN:"string",b:"`",e:"`",c:[e.BE,n]};n.c=[e.ASM,e.QSM,c,a,e.RM];var s=n.c.concat([e.CBCM,e.CLCM]);return{aliases:["js","jsx"],k:t,c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},{cN:"meta",b:/^#!/,e:/$/},e.ASM,e.QSM,c,e.CLCM,e.CBCM,a,{b:/[{,]\s*/,r:0,c:[{b:r+"\\s*:",rB:!0,r:0,c:[{cN:"attr",b:r,r:0}]}]},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{cN:"function",b:"(\\(.*?\\)|"+r+")\\s*=>",rB:!0,e:"\\s*=>",c:[{cN:"params",v:[{b:r},{b:/\(\s*\)/},{b:/\(/,e:/\)/,eB:!0,eE:!0,k:t,c:s}]}]},{b:/,e:/(\/\w+|\w+\/)>/,sL:"xml",c:[{b:/<\w+\s*\/>/,skip:!0},{b:/<\w+/,e:/(\/\w+|\w+\/)>/,skip:!0,c:[{b:/<\w+\s*\/>/,skip:!0},"self"]}]}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:r}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:s}],i:/\[|%/},{b:/\$[(.]/},e.METHOD_GUARD,{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#(?!!)/}});
--------------------------------------------------------------------------------
/docs/assets/js/index.js:
--------------------------------------------------------------------------------
1 | import { shuffle, getRandomBias } from '../../../src/js/utils/helpers';
2 | import { HEATMAP_COLORS_YELLOW, HEATMAP_COLORS_BLUE } from '../../../src/js/utils/constants';
3 | import { SEC_IN_DAY, clone, timestampToMidnight, timestampSec, addDays } from '../../../src/js/utils/date-utils';
4 | /* eslint-disable no-unused-vars */
5 | import { fireballOver25, fireball_2_5, fireball_5_25, lineCompositeData,
6 | barCompositeData, typeData, trendsData, moonData } from './data';
7 | /* eslint-enable no-unused-vars */
8 | import demoConfig from './demoConfig';
9 | // import { lineComposite, barComposite } from './demoConfig';
10 | // ================================================================================
11 |
12 | let Chart = frappe.Chart; // eslint-disable-line no-undef
13 |
14 | let lc = demoConfig.lineComposite;
15 | let lineCompositeChart = new Chart (lc.elementID, lc.options);
16 |
17 | let bc = demoConfig.barComposite;
18 | let barCompositeChart = new Chart (bc.elementID, bc.options);
19 |
20 | lineCompositeChart.parent.addEventListener('data-select', (e) => {
21 | let i = e.index;
22 | barCompositeChart.updateDatasets([
23 | fireballOver25[i], fireball_5_25[i], fireball_2_5[i]
24 | ]);
25 | });
26 |
27 | // ================================================================================
28 |
29 | let customColors = ['purple', 'magenta', 'light-blue'];
30 | let typeChartArgs = {
31 | title: "My Awesome Chart",
32 | data: typeData,
33 | type: 'axis-mixed',
34 | height: 300,
35 | colors: customColors,
36 |
37 | // maxLegendPoints: 6,
38 | maxSlices: 10,
39 |
40 | tooltipOptions: {
41 | formatTooltipX: d => (d + '').toUpperCase(),
42 | formatTooltipY: d => d + ' pts',
43 | }
44 | };
45 |
46 | let aggrChart = new Chart("#chart-aggr", typeChartArgs);
47 |
48 | Array.prototype.slice.call(
49 | document.querySelectorAll('.aggr-type-buttons button')
50 | ).map(el => {
51 | el.addEventListener('click', (e) => {
52 | let btn = e.target;
53 | let type = btn.getAttribute('data-type');
54 | typeChartArgs.type = type;
55 | if(type !== 'axis-mixed') {
56 | typeChartArgs.colors = undefined;
57 | } else {
58 | typeChartArgs.colors = customColors;
59 | }
60 |
61 | if(type !== 'percentage') {
62 | typeChartArgs.height = 300;
63 | } else {
64 | typeChartArgs.height = undefined;
65 | }
66 |
67 | let newChart = new Chart("#chart-aggr", typeChartArgs);
68 | if(newChart){
69 | aggrChart = newChart;
70 | }
71 | Array.prototype.slice.call(
72 | btn.parentNode.querySelectorAll('button')).map(el => {
73 | el.classList.remove('active');
74 | });
75 | btn.classList.add('active');
76 | });
77 | });
78 |
79 | document.querySelector('.export-aggr').addEventListener('click', () => {
80 | aggrChart.export();
81 | });
82 |
83 | // Update values chart
84 | // ================================================================================
85 | let updateDataAllLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue",
86 | "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri",
87 | "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon"];
88 |
89 | let getRandom = () => Math.floor(getRandomBias(-40, 60, 0.8, 1));
90 | let updateDataAllValues = Array.from({length: 30}, getRandom);
91 |
92 | // We're gonna be shuffling this
93 | let updateDataAllIndices = updateDataAllLabels.map((d,i) => i);
94 |
95 | let getUpdateData = (source_array, length=10) => {
96 | let indices = updateDataAllIndices.slice(0, length);
97 | return indices.map((index) => source_array[index]);
98 | };
99 |
100 | let updateData = {
101 | labels: getUpdateData(updateDataAllLabels),
102 | datasets: [{
103 | "values": getUpdateData(updateDataAllValues)
104 | }],
105 | yMarkers: [
106 | {
107 | label: "Altitude",
108 | value: 25,
109 | type: 'dashed'
110 | }
111 | ],
112 | yRegions: [
113 | {
114 | label: "Range",
115 | start: 10,
116 | end: 45
117 | },
118 | ],
119 | };
120 |
121 | let updateChart = new Chart("#chart-update", {
122 | data: updateData,
123 | type: 'line',
124 | height: 300,
125 | colors: ['#ff6c03'],
126 | lineOptions: {
127 | // hideLine: 1,
128 | regionFill: 1
129 | },
130 | });
131 |
132 | let chartUpdateButtons = document.querySelector('.chart-update-buttons');
133 |
134 | chartUpdateButtons.querySelector('[data-update="random"]').addEventListener("click", () => {
135 | shuffle(updateDataAllIndices);
136 | let value = getRandom();
137 | let start = getRandom();
138 | let end = getRandom();
139 | let data = {
140 | labels: updateDataAllLabels.slice(0, 10),
141 | datasets: [{values: getUpdateData(updateDataAllValues)}],
142 | yMarkers: [
143 | {
144 | label: "Altitude",
145 | value: value,
146 | type: 'dashed'
147 | }
148 | ],
149 | yRegions: [
150 | {
151 | label: "Range",
152 | start: start,
153 | end: end
154 | },
155 | ],
156 | };
157 | updateChart.update(data);
158 | });
159 |
160 | chartUpdateButtons.querySelector('[data-update="add"]').addEventListener("click", () => {
161 | let index = updateChart.state.datasetLength; // last index to add
162 | if(index >= updateDataAllIndices.length) return;
163 | updateChart.addDataPoint(
164 | updateDataAllLabels[index], [updateDataAllValues[index]]
165 | );
166 | });
167 |
168 | chartUpdateButtons.querySelector('[data-update="remove"]').addEventListener("click", () => {
169 | updateChart.removeDataPoint();
170 | });
171 |
172 | document.querySelector('.export-update').addEventListener('click', () => {
173 | updateChart.export();
174 | });
175 |
176 | // Trends Chart
177 | // ================================================================================
178 |
179 | let plotChartArgs = {
180 | title: "Mean Total Sunspot Count - Yearly",
181 | data: trendsData,
182 | type: 'line',
183 | height: 300,
184 | colors: ['#238e38'],
185 | lineOptions: {
186 | hideDots: 1,
187 | heatline: 1,
188 | },
189 | axisOptions: {
190 | xAxisMode: 'tick',
191 | yAxisMode: 'span',
192 | xIsSeries: 1
193 | }
194 | };
195 |
196 | let trendsChart = new Chart("#chart-trends", plotChartArgs);
197 |
198 | Array.prototype.slice.call(
199 | document.querySelectorAll('.chart-plot-buttons button')
200 | ).map(el => {
201 | el.addEventListener('click', (e) => {
202 | let btn = e.target;
203 | let type = btn.getAttribute('data-type');
204 | let config = {};
205 | config[type] = 1;
206 |
207 | if(['regionFill', 'heatline'].includes(type)) {
208 | config.hideDots = 1;
209 | }
210 |
211 | // plotChartArgs.init = false;
212 | plotChartArgs.lineOptions = config;
213 |
214 | new Chart("#chart-trends", plotChartArgs);
215 |
216 | Array.prototype.slice.call(
217 | btn.parentNode.querySelectorAll('button')).map(el => {
218 | el.classList.remove('active');
219 | });
220 | btn.classList.add('active');
221 | });
222 | });
223 |
224 | document.querySelector('.export-trends').addEventListener('click', () => {
225 | trendsChart.export();
226 | });
227 |
228 |
229 | // Event chart
230 | // ================================================================================
231 |
232 |
233 |
234 | let eventsData = {
235 | labels: ["Ganymede", "Callisto", "Io", "Europa"],
236 | datasets: [
237 | {
238 | "values": moonData.distances,
239 | "formatted": moonData.distances.map(d => d*1000 + " km")
240 | }
241 | ]
242 | };
243 |
244 | let eventsChart = new Chart("#chart-events", {
245 | title: "Jupiter's Moons: Semi-major Axis (1000 km)",
246 | data: eventsData,
247 | type: 'bar',
248 | height: 330,
249 | colors: ['grey'],
250 | isNavigable: 1,
251 | });
252 |
253 | let dataDiv = document.querySelector('.chart-events-data');
254 |
255 | eventsChart.parent.addEventListener('data-select', (e) => {
256 | let name = moonData.names[e.index];
257 | dataDiv.querySelector('.moon-name').innerHTML = name;
258 | dataDiv.querySelector('.semi-major-axis').innerHTML = moonData.distances[e.index] * 1000;
259 | dataDiv.querySelector('.mass').innerHTML = moonData.masses[e.index];
260 | dataDiv.querySelector('.diameter').innerHTML = moonData.diameters[e.index];
261 | dataDiv.querySelector('img').src = "./assets/img/" + name.toLowerCase() + ".jpg";
262 | });
263 |
264 | // Heatmap
265 | // ================================================================================
266 |
267 | let today = new Date();
268 | let start = clone(today);
269 | addDays(start, 4);
270 | let end = clone(start);
271 | start.setFullYear( start.getFullYear() - 2 );
272 | end.setFullYear( end.getFullYear() - 1 );
273 |
274 | let dataPoints = {};
275 |
276 | let startTs = timestampSec(start);
277 | let endTs = timestampSec(end);
278 |
279 | startTs = timestampToMidnight(startTs);
280 | endTs = timestampToMidnight(endTs, true);
281 |
282 | while (startTs < endTs) {
283 | dataPoints[parseInt(startTs)] = Math.floor(getRandomBias(0, 5, 0.2, 1));
284 | startTs += SEC_IN_DAY;
285 | }
286 |
287 | const heatmapData = {
288 | dataPoints: dataPoints,
289 | start: start,
290 | end: end
291 | };
292 |
293 | let heatmapArgs = {
294 | title: "Monthly Distribution",
295 | data: heatmapData,
296 | type: 'heatmap',
297 | discreteDomains: 1,
298 | countLabel: 'Level',
299 | colors: HEATMAP_COLORS_BLUE,
300 | legendScale: [0, 1, 2, 4, 5]
301 | };
302 | let heatmapChart = new Chart("#chart-heatmap", heatmapArgs);
303 |
304 | Array.prototype.slice.call(
305 | document.querySelectorAll('.heatmap-mode-buttons button')
306 | ).map(el => {
307 | el.addEventListener('click', (e) => {
308 | let btn = e.target;
309 | let mode = btn.getAttribute('data-mode');
310 | let discreteDomains = 0;
311 |
312 | if(mode === 'discrete') {
313 | discreteDomains = 1;
314 | }
315 |
316 | let colors = [];
317 | let colors_mode = document
318 | .querySelector('.heatmap-color-buttons .active')
319 | .getAttribute('data-color');
320 | if(colors_mode === 'halloween') {
321 | colors = HEATMAP_COLORS_YELLOW;
322 | } else if (colors_mode === 'blue') {
323 | colors = HEATMAP_COLORS_BLUE;
324 | }
325 |
326 | heatmapArgs.discreteDomains = discreteDomains;
327 | heatmapArgs.colors = colors;
328 | new Chart("#chart-heatmap", heatmapArgs);
329 |
330 | Array.prototype.slice.call(
331 | btn.parentNode.querySelectorAll('button')).map(el => {
332 | el.classList.remove('active');
333 | });
334 | btn.classList.add('active');
335 | });
336 | });
337 |
338 | Array.prototype.slice.call(
339 | document.querySelectorAll('.heatmap-color-buttons button')
340 | ).map(el => {
341 | el.addEventListener('click', (e) => {
342 | let btn = e.target;
343 | let colors_mode = btn.getAttribute('data-color');
344 | let colors = [];
345 |
346 | if(colors_mode === 'halloween') {
347 | colors = HEATMAP_COLORS_YELLOW;
348 | } else if (colors_mode === 'blue') {
349 | colors = HEATMAP_COLORS_BLUE;
350 | }
351 |
352 | let discreteDomains = 1;
353 |
354 | let view_mode = document
355 | .querySelector('.heatmap-mode-buttons .active')
356 | .getAttribute('data-mode');
357 | if(view_mode === 'continuous') {
358 | discreteDomains = 0;
359 | }
360 |
361 | heatmapArgs.discreteDomains = discreteDomains;
362 | heatmapArgs.colors = colors;
363 | new Chart("#chart-heatmap", heatmapArgs);
364 |
365 | Array.prototype.slice.call(
366 | btn.parentNode.querySelectorAll('button')).map(el => {
367 | el.classList.remove('active');
368 | });
369 | btn.classList.add('active');
370 | });
371 | });
372 |
373 | document.querySelector('.export-heatmap').addEventListener('click', () => {
374 | heatmapChart.export();
375 | });
376 |
--------------------------------------------------------------------------------
/docs/assets/js/index.min.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 |
4 | var HEATMAP_COLORS_BLUE = ['#ebedf0', '#c0ddf9', '#73b3f3', '#3886e1', '#17459e'];
5 | var HEATMAP_COLORS_YELLOW = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'];
6 |
7 | // Universal constants
8 | var ANGLE_RATIO = Math.PI / 180;
9 |
10 | /**
11 | * Shuffles array in place. ES6 version
12 | * @param {Array} array An array containing the items.
13 | */
14 | function shuffle(array) {
15 | // Awesomeness: https://bost.ocks.org/mike/shuffle/
16 | // https://stackoverflow.com/a/2450976/6495043
17 | // https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array?noredirect=1&lq=1
18 |
19 | for (var i = array.length - 1; i > 0; i--) {
20 | var j = Math.floor(Math.random() * (i + 1));
21 | var _ref = [array[j], array[i]];
22 | array[i] = _ref[0];
23 | array[j] = _ref[1];
24 | }
25 |
26 | return array;
27 | }
28 |
29 | // https://stackoverflow.com/a/29325222
30 | function getRandomBias(min, max, bias, influence) {
31 | var range = max - min;
32 | var biasValue = range * bias + min;
33 | var rnd = Math.random() * range + min,
34 | // random in range
35 | mix = Math.random() * influence; // random mixer
36 | return rnd * (1 - mix) + biasValue * mix; // mix full range and bias
37 | }
38 |
39 | // Playing around with dates
40 | var NO_OF_MILLIS = 1000;
41 | var SEC_IN_DAY = 86400;
42 | var MONTH_NAMES_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
43 |
44 | function clone(date) {
45 | return new Date(date.getTime());
46 | }
47 |
48 | function timestampSec(date) {
49 | return date.getTime() / NO_OF_MILLIS;
50 | }
51 |
52 | function timestampToMidnight(timestamp) {
53 | var roundAhead = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
54 |
55 | var midnightTs = Math.floor(timestamp - timestamp % SEC_IN_DAY);
56 | if (roundAhead) {
57 | return midnightTs + SEC_IN_DAY;
58 | }
59 | return midnightTs;
60 | }
61 |
62 | // mutates
63 | function addDays(date, numberOfDays) {
64 | date.setDate(date.getDate() + numberOfDays);
65 | }
66 |
67 | // Composite Chart
68 | // ================================================================================
69 | var reportCountList = [152, 222, 199, 287, 534, 709, 1179, 1256, 1632, 1856, 1850];
70 |
71 | var lineCompositeData = {
72 | labels: ["2007", "2008", "2009", "2010", "2011", "2012", "2013", "2014", "2015", "2016", "2017"],
73 |
74 | yMarkers: [{
75 | label: "Average 100 reports/month",
76 | value: 1200,
77 | options: { labelPos: "left" }
78 | }],
79 |
80 | datasets: [{
81 | name: "Events",
82 | values: reportCountList
83 | }]
84 | };
85 |
86 | var fireball_5_25 = [[4, 0, 3, 1, 1, 2, 1, 1, 1, 0, 1, 1], [2, 3, 3, 2, 1, 3, 0, 1, 2, 7, 10, 4], [5, 6, 2, 4, 0, 1, 4, 3, 0, 2, 0, 1], [0, 2, 6, 2, 1, 1, 2, 3, 6, 3, 7, 8], [6, 8, 7, 7, 4, 5, 6, 5, 22, 12, 10, 11], [7, 10, 11, 7, 3, 2, 7, 7, 11, 15, 22, 20], [13, 16, 21, 18, 19, 17, 12, 17, 31, 28, 25, 29], [24, 14, 21, 14, 11, 15, 19, 21, 41, 22, 32, 18], [31, 20, 30, 22, 14, 17, 21, 35, 27, 50, 117, 24], [32, 24, 21, 27, 11, 27, 43, 37, 44, 40, 48, 32], [31, 38, 36, 26, 23, 23, 25, 29, 26, 47, 61, 50]];
87 | var fireball_2_5 = [[22, 6, 6, 9, 7, 8, 6, 14, 19, 10, 8, 20], [11, 13, 12, 8, 9, 11, 9, 13, 10, 22, 40, 24], [20, 13, 13, 19, 13, 10, 14, 13, 20, 18, 5, 9], [7, 13, 16, 19, 12, 11, 21, 27, 27, 24, 33, 33], [38, 25, 28, 22, 31, 21, 35, 42, 37, 32, 46, 53], [50, 33, 36, 34, 35, 28, 27, 52, 58, 59, 75, 69], [54, 67, 67, 45, 66, 51, 38, 64, 90, 113, 116, 87], [84, 52, 56, 51, 55, 46, 50, 87, 114, 83, 152, 93], [73, 58, 59, 63, 56, 51, 83, 140, 103, 115, 265, 89], [106, 95, 94, 71, 77, 75, 99, 136, 129, 154, 168, 156], [81, 102, 95, 72, 58, 91, 89, 122, 124, 135, 183, 171]];
88 | var fireballOver25 = [
89 | // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
90 | [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0], [1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2], [3, 2, 1, 3, 2, 0, 2, 2, 2, 3, 0, 1], [2, 3, 5, 2, 1, 3, 0, 2, 3, 5, 1, 4], [7, 4, 6, 1, 9, 2, 2, 2, 20, 9, 4, 9], [5, 6, 1, 2, 5, 4, 5, 5, 16, 9, 14, 9], [5, 4, 7, 5, 1, 5, 3, 3, 5, 7, 22, 2], [5, 13, 11, 6, 1, 7, 9, 8, 14, 17, 16, 3], [8, 9, 8, 6, 4, 8, 5, 6, 14, 11, 21, 12]];
91 |
92 | var barCompositeData = {
93 | labels: MONTH_NAMES_SHORT,
94 | datasets: [{
95 | name: "Over 25 reports",
96 | values: fireballOver25[9]
97 | }, {
98 | name: "5 to 25 reports",
99 | values: fireball_5_25[9]
100 | }, {
101 | name: "2 to 5 reports",
102 | values: fireball_2_5[9]
103 | }]
104 | };
105 |
106 | // Demo Chart multitype Chart
107 | // ================================================================================
108 | var typeData = {
109 | labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", "12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"],
110 |
111 | yMarkers: [{
112 | label: "Marker",
113 | value: 43,
114 | options: { labelPos: "left" }
115 | // type: 'dashed'
116 | }],
117 |
118 | yRegions: [{
119 | label: "Region",
120 | start: -10,
121 | end: 50,
122 | options: { labelPos: "right" }
123 | }],
124 |
125 | datasets: [{
126 | name: "Some Data",
127 | values: [18, 40, 30, 35, 8, 52, 17, -4],
128 | axisPosition: "right",
129 | chartType: "bar"
130 | }, {
131 | name: "Another Set",
132 | values: [30, 50, -10, 15, 18, 32, 27, 14],
133 | axisPosition: "right",
134 | chartType: "bar"
135 | }, {
136 | name: "Yet Another",
137 | values: [15, 20, -3, -15, 58, 12, -17, 37],
138 | chartType: "line"
139 | }]
140 | };
141 |
142 | var trendsData = {
143 | labels: [1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976, 1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016],
144 | datasets: [{
145 | values: [132.9, 150.0, 149.4, 148.0, 94.4, 97.6, 54.1, 49.2, 22.5, 18.4, 39.3, 131.0, 220.1, 218.9, 198.9, 162.4, 91.0, 60.5, 20.6, 14.8, 33.9, 123.0, 211.1, 191.8, 203.3, 133.0, 76.1, 44.9, 25.1, 11.6, 28.9, 88.3, 136.3, 173.9, 170.4, 163.6, 99.3, 65.3, 45.8, 24.7, 12.6, 4.2, 4.8, 24.9, 80.8, 84.5, 94.0, 113.3, 69.8, 39.8]
146 | }]
147 | };
148 |
149 | var moonData = {
150 | names: ["Ganymede", "Callisto", "Io", "Europa"],
151 | masses: [14819000, 10759000, 8931900, 4800000],
152 | distances: [1070.412, 1882.709, 421.7, 671.034],
153 | diameters: [5262.4, 4820.6, 3637.4, 3121.6]
154 | };
155 |
156 | var demoConfig = {
157 | lineComposite: {
158 | elementID: "#chart-composite-1",
159 | options: {
160 | title: "Fireball/Bolide Events - Yearly (reported)",
161 | data: lineCompositeData,
162 | type: "line",
163 | height: 190,
164 | colors: ["green"],
165 | isNavigable: 1,
166 | valuesOverPoints: 1,
167 |
168 | lineOptions: {
169 | dotSize: 8
170 | }
171 | }
172 | },
173 |
174 | barComposite: {
175 | elementID: "#chart-composite-2",
176 | options: {
177 | data: barCompositeData,
178 | type: "bar",
179 | height: 210,
180 | colors: ["violet", "light-blue", "#46a9f9"],
181 | valuesOverPoints: 1,
182 | axisOptions: {
183 | xAxisMode: "tick",
184 | shortenYAxisNumbers: true
185 | },
186 | barOptions: {
187 | stacked: 1
188 | }
189 | }
190 | },
191 |
192 | demoMain: {
193 | elementID: "",
194 | options: {
195 | title: "My Awesome Chart",
196 | data: "typeData",
197 | type: "axis-mixed",
198 | height: 300,
199 | colors: ["purple", "magenta", "light-blue"],
200 | maxSlices: 10,
201 |
202 | tooltipOptions: {
203 | formatTooltipX: function formatTooltipX(d) {
204 | return (d + '').toUpperCase();
205 | },
206 | formatTooltipY: function formatTooltipY(d) {
207 | return d + ' pts';
208 | }
209 | }
210 | }
211 | }
212 | };
213 |
214 | // import { lineComposite, barComposite } from './demoConfig';
215 | // ================================================================================
216 |
217 | var Chart = frappe.Chart; // eslint-disable-line no-undef
218 |
219 | var lc = demoConfig.lineComposite;
220 | var lineCompositeChart = new Chart(lc.elementID, lc.options);
221 |
222 | var bc = demoConfig.barComposite;
223 | var barCompositeChart = new Chart(bc.elementID, bc.options);
224 |
225 | lineCompositeChart.parent.addEventListener('data-select', function (e) {
226 | var i = e.index;
227 | barCompositeChart.updateDatasets([fireballOver25[i], fireball_5_25[i], fireball_2_5[i]]);
228 | });
229 |
230 | // ================================================================================
231 |
232 | var customColors = ['purple', 'magenta', 'light-blue'];
233 | var typeChartArgs = {
234 | title: "My Awesome Chart",
235 | data: typeData,
236 | type: 'axis-mixed',
237 | height: 300,
238 | colors: customColors,
239 |
240 | // maxLegendPoints: 6,
241 | maxSlices: 10,
242 |
243 | tooltipOptions: {
244 | formatTooltipX: function formatTooltipX(d) {
245 | return (d + '').toUpperCase();
246 | },
247 | formatTooltipY: function formatTooltipY(d) {
248 | return d + ' pts';
249 | }
250 | }
251 | };
252 |
253 | var aggrChart = new Chart("#chart-aggr", typeChartArgs);
254 |
255 | Array.prototype.slice.call(document.querySelectorAll('.aggr-type-buttons button')).map(function (el) {
256 | el.addEventListener('click', function (e) {
257 | var btn = e.target;
258 | var type = btn.getAttribute('data-type');
259 | typeChartArgs.type = type;
260 | if (type !== 'axis-mixed') {
261 | typeChartArgs.colors = undefined;
262 | } else {
263 | typeChartArgs.colors = customColors;
264 | }
265 |
266 | if (type !== 'percentage') {
267 | typeChartArgs.height = 300;
268 | } else {
269 | typeChartArgs.height = undefined;
270 | }
271 |
272 | var newChart = new Chart("#chart-aggr", typeChartArgs);
273 | if (newChart) {
274 | aggrChart = newChart;
275 | }
276 | Array.prototype.slice.call(btn.parentNode.querySelectorAll('button')).map(function (el) {
277 | el.classList.remove('active');
278 | });
279 | btn.classList.add('active');
280 | });
281 | });
282 |
283 | document.querySelector('.export-aggr').addEventListener('click', function () {
284 | aggrChart.export();
285 | });
286 |
287 | // Update values chart
288 | // ================================================================================
289 | var updateDataAllLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon"];
290 |
291 | var getRandom = function getRandom() {
292 | return Math.floor(getRandomBias(-40, 60, 0.8, 1));
293 | };
294 | var updateDataAllValues = Array.from({ length: 30 }, getRandom);
295 |
296 | // We're gonna be shuffling this
297 | var updateDataAllIndices = updateDataAllLabels.map(function (d, i) {
298 | return i;
299 | });
300 |
301 | var getUpdateData = function getUpdateData(source_array) {
302 | var length = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 10;
303 |
304 | var indices = updateDataAllIndices.slice(0, length);
305 | return indices.map(function (index) {
306 | return source_array[index];
307 | });
308 | };
309 |
310 | var updateData = {
311 | labels: getUpdateData(updateDataAllLabels),
312 | datasets: [{
313 | "values": getUpdateData(updateDataAllValues)
314 | }],
315 | yMarkers: [{
316 | label: "Altitude",
317 | value: 25,
318 | type: 'dashed'
319 | }],
320 | yRegions: [{
321 | label: "Range",
322 | start: 10,
323 | end: 45
324 | }]
325 | };
326 |
327 | var updateChart = new Chart("#chart-update", {
328 | data: updateData,
329 | type: 'line',
330 | height: 300,
331 | colors: ['#ff6c03'],
332 | lineOptions: {
333 | // hideLine: 1,
334 | regionFill: 1
335 | }
336 | });
337 |
338 | var chartUpdateButtons = document.querySelector('.chart-update-buttons');
339 |
340 | chartUpdateButtons.querySelector('[data-update="random"]').addEventListener("click", function () {
341 | shuffle(updateDataAllIndices);
342 | var value = getRandom();
343 | var start = getRandom();
344 | var end = getRandom();
345 | var data = {
346 | labels: updateDataAllLabels.slice(0, 10),
347 | datasets: [{ values: getUpdateData(updateDataAllValues) }],
348 | yMarkers: [{
349 | label: "Altitude",
350 | value: value,
351 | type: 'dashed'
352 | }],
353 | yRegions: [{
354 | label: "Range",
355 | start: start,
356 | end: end
357 | }]
358 | };
359 | updateChart.update(data);
360 | });
361 |
362 | chartUpdateButtons.querySelector('[data-update="add"]').addEventListener("click", function () {
363 | var index = updateChart.state.datasetLength; // last index to add
364 | if (index >= updateDataAllIndices.length) return;
365 | updateChart.addDataPoint(updateDataAllLabels[index], [updateDataAllValues[index]]);
366 | });
367 |
368 | chartUpdateButtons.querySelector('[data-update="remove"]').addEventListener("click", function () {
369 | updateChart.removeDataPoint();
370 | });
371 |
372 | document.querySelector('.export-update').addEventListener('click', function () {
373 | updateChart.export();
374 | });
375 |
376 | // Trends Chart
377 | // ================================================================================
378 |
379 | var plotChartArgs = {
380 | title: "Mean Total Sunspot Count - Yearly",
381 | data: trendsData,
382 | type: 'line',
383 | height: 300,
384 | colors: ['#238e38'],
385 | lineOptions: {
386 | hideDots: 1,
387 | heatline: 1
388 | },
389 | axisOptions: {
390 | xAxisMode: 'tick',
391 | yAxisMode: 'span',
392 | xIsSeries: 1
393 | }
394 | };
395 |
396 | var trendsChart = new Chart("#chart-trends", plotChartArgs);
397 |
398 | Array.prototype.slice.call(document.querySelectorAll('.chart-plot-buttons button')).map(function (el) {
399 | el.addEventListener('click', function (e) {
400 | var btn = e.target;
401 | var type = btn.getAttribute('data-type');
402 | var config = {};
403 | config[type] = 1;
404 |
405 | if (['regionFill', 'heatline'].includes(type)) {
406 | config.hideDots = 1;
407 | }
408 |
409 | // plotChartArgs.init = false;
410 | plotChartArgs.lineOptions = config;
411 |
412 | new Chart("#chart-trends", plotChartArgs);
413 |
414 | Array.prototype.slice.call(btn.parentNode.querySelectorAll('button')).map(function (el) {
415 | el.classList.remove('active');
416 | });
417 | btn.classList.add('active');
418 | });
419 | });
420 |
421 | document.querySelector('.export-trends').addEventListener('click', function () {
422 | trendsChart.export();
423 | });
424 |
425 | // Event chart
426 | // ================================================================================
427 |
428 |
429 | var eventsData = {
430 | labels: ["Ganymede", "Callisto", "Io", "Europa"],
431 | datasets: [{
432 | "values": moonData.distances,
433 | "formatted": moonData.distances.map(function (d) {
434 | return d * 1000 + " km";
435 | })
436 | }]
437 | };
438 |
439 | var eventsChart = new Chart("#chart-events", {
440 | title: "Jupiter's Moons: Semi-major Axis (1000 km)",
441 | data: eventsData,
442 | type: 'bar',
443 | height: 330,
444 | colors: ['grey'],
445 | isNavigable: 1
446 | });
447 |
448 | var dataDiv = document.querySelector('.chart-events-data');
449 |
450 | eventsChart.parent.addEventListener('data-select', function (e) {
451 | var name = moonData.names[e.index];
452 | dataDiv.querySelector('.moon-name').innerHTML = name;
453 | dataDiv.querySelector('.semi-major-axis').innerHTML = moonData.distances[e.index] * 1000;
454 | dataDiv.querySelector('.mass').innerHTML = moonData.masses[e.index];
455 | dataDiv.querySelector('.diameter').innerHTML = moonData.diameters[e.index];
456 | dataDiv.querySelector('img').src = "./assets/img/" + name.toLowerCase() + ".jpg";
457 | });
458 |
459 | // Heatmap
460 | // ================================================================================
461 |
462 | var today = new Date();
463 | var start = clone(today);
464 | addDays(start, 4);
465 | var end = clone(start);
466 | start.setFullYear(start.getFullYear() - 2);
467 | end.setFullYear(end.getFullYear() - 1);
468 |
469 | var dataPoints = {};
470 |
471 | var startTs = timestampSec(start);
472 | var endTs = timestampSec(end);
473 |
474 | startTs = timestampToMidnight(startTs);
475 | endTs = timestampToMidnight(endTs, true);
476 |
477 | while (startTs < endTs) {
478 | dataPoints[parseInt(startTs)] = Math.floor(getRandomBias(0, 5, 0.2, 1));
479 | startTs += SEC_IN_DAY;
480 | }
481 |
482 | var heatmapData = {
483 | dataPoints: dataPoints,
484 | start: start,
485 | end: end
486 | };
487 |
488 | var heatmapArgs = {
489 | title: "Monthly Distribution",
490 | data: heatmapData,
491 | type: 'heatmap',
492 | discreteDomains: 1,
493 | countLabel: 'Level',
494 | colors: HEATMAP_COLORS_BLUE,
495 | legendScale: [0, 1, 2, 4, 5]
496 | };
497 | var heatmapChart = new Chart("#chart-heatmap", heatmapArgs);
498 |
499 | Array.prototype.slice.call(document.querySelectorAll('.heatmap-mode-buttons button')).map(function (el) {
500 | el.addEventListener('click', function (e) {
501 | var btn = e.target;
502 | var mode = btn.getAttribute('data-mode');
503 | var discreteDomains = 0;
504 |
505 | if (mode === 'discrete') {
506 | discreteDomains = 1;
507 | }
508 |
509 | var colors = [];
510 | var colors_mode = document.querySelector('.heatmap-color-buttons .active').getAttribute('data-color');
511 | if (colors_mode === 'halloween') {
512 | colors = HEATMAP_COLORS_YELLOW;
513 | } else if (colors_mode === 'blue') {
514 | colors = HEATMAP_COLORS_BLUE;
515 | }
516 |
517 | heatmapArgs.discreteDomains = discreteDomains;
518 | heatmapArgs.colors = colors;
519 | new Chart("#chart-heatmap", heatmapArgs);
520 |
521 | Array.prototype.slice.call(btn.parentNode.querySelectorAll('button')).map(function (el) {
522 | el.classList.remove('active');
523 | });
524 | btn.classList.add('active');
525 | });
526 | });
527 |
528 | Array.prototype.slice.call(document.querySelectorAll('.heatmap-color-buttons button')).map(function (el) {
529 | el.addEventListener('click', function (e) {
530 | var btn = e.target;
531 | var colors_mode = btn.getAttribute('data-color');
532 | var colors = [];
533 |
534 | if (colors_mode === 'halloween') {
535 | colors = HEATMAP_COLORS_YELLOW;
536 | } else if (colors_mode === 'blue') {
537 | colors = HEATMAP_COLORS_BLUE;
538 | }
539 |
540 | var discreteDomains = 1;
541 |
542 | var view_mode = document.querySelector('.heatmap-mode-buttons .active').getAttribute('data-mode');
543 | if (view_mode === 'continuous') {
544 | discreteDomains = 0;
545 | }
546 |
547 | heatmapArgs.discreteDomains = discreteDomains;
548 | heatmapArgs.colors = colors;
549 | new Chart("#chart-heatmap", heatmapArgs);
550 |
551 | Array.prototype.slice.call(btn.parentNode.querySelectorAll('button')).map(function (el) {
552 | el.classList.remove('active');
553 | });
554 | btn.classList.add('active');
555 | });
556 | });
557 |
558 | document.querySelector('.export-heatmap').addEventListener('click', function () {
559 | heatmapChart.export();
560 | });
561 |
562 | }());
563 | //# sourceMappingURL=index.min.js.map
564 |
--------------------------------------------------------------------------------
/docs/docs.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frappe/charts/7b15424c3af80b1c8e178d8e612c290d2d630904/docs/docs.html
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 | ---
2 | redirect_to: "https://frappe.io/charts"
3 | ---
4 |
5 |
6 |
7 |
8 | Frappe Charts
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | Frappe Charts
30 | GitHub-inspired simple and modern SVG charts for the web with zero dependencies.
31 |
32 | Click or use arrow keys to navigate data points
33 |
34 |
35 |
36 |
37 | Create a chart
38 | <!--HTML-->
39 | <div id="chart"></div>
40 | // Javascript
41 | let chart = new frappe.Chart( "#chart", { // or DOM element
42 | data: {
43 | labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm",
44 | "12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"],
45 |
46 | datasets: [
47 | {
48 | name: "Some Data", chartType: 'bar',
49 | values: [25, 40, 30, 35, 8, 52, 17, -4]
50 | },
51 | {
52 | name: "Another Set", chartType: 'bar',
53 | values: [25, 50, -10, 15, 18, 32, 27, 14]
54 | },
55 | {
56 | name: "Yet Another", chartType: 'line',
57 | values: [15, 20, -3, -15, 58, 12, -17, 37]
58 | }
59 | ],
60 |
61 | yMarkers: [{ label: "Marker", value: 70,
62 | options: { labelPos: 'left' }}],
63 | yRegions: [{ label: "Region", start: -10, end: 50,
64 | options: { labelPos: 'right' }}]
65 | },
66 |
67 | title: "My Awesome Chart",
68 | type: 'axis-mixed', // or 'bar', 'line', 'pie', 'percentage', 'donut'
69 | height: 300,
70 | colors: ['purple', '#ffa3ef', 'light-blue'],
71 |
72 | tooltipOptions: {
73 | formatTooltipX: d => (d + '').toUpperCase(),
74 | formatTooltipY: d => d + ' pts',
75 | }
76 | });
77 |
78 | chart.export();
79 |
80 |
81 |
82 |
83 | Mixed
84 | Pie Chart
85 | Donut Chart
86 | Percentage Chart
87 |
88 |
89 | Export ...
90 |
91 |
92 |
93 |
94 | Update Values
95 |
96 |
97 |
98 | Random Data
99 | Add Value
100 | Remove Value
101 | Export ...
102 |
103 |
104 |
105 |
106 | Plot Trends
107 |
108 |
109 |
110 | Line
111 | Dots
112 | HeatLine
113 | Region
114 |
115 |
116 | Export ...
117 |
118 |
119 |
120 |
121 | Listen to state change
122 |
123 |
126 |
127 |
128 |
129 |
130 |
131 |
Europa
132 |
Semi-major-axis: 671034 km
133 |
Mass: 4800000 x 10^16 kg
134 |
Diameter: 3121.6 km
135 |
136 |
137 |
138 | ...
139 | isNavigable: 1, // Navigate across data points; default 0
140 | ...
141 |
142 | chart.parent.addEventListener('data-select', (e) => {
143 | update_moon_data(e.index); // e contains index and value of current datapoint
144 | });
145 |
146 |
147 |
148 |
149 | And a Month-wise Heatmap
150 |
151 |
153 |
154 | Discrete
155 | Continuous
156 |
157 |
158 | Green (Default)
159 | Blue
160 | GitHub's Halloween
161 |
162 |
163 | Export ...
164 |
165 | let heatmap = new frappe.Chart("#heatmap", {
166 | type: 'heatmap',
167 | title: "Monthly Distribution",
168 | data: {
169 | dataPoints: {'1524064033': 8, /* ... */},
170 | // object with timestamp-value pairs
171 | start: startDate
172 | end: endDate // Date objects
173 | },
174 | countLabel: 'Level',
175 | discreteDomains: 0 // default: 1
176 | colors: ['#ebedf0', '#c0ddf9', '#73b3f3', '#3886e1', '#17459e'],
177 | // Set of five incremental colors,
178 | // preferably with a low-saturation color for zero data;
179 | // def: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127']
180 | });
181 |
182 |
183 |
193 |
194 |
195 | Available options
196 |
197 | ...
198 | {
199 | data: {
200 | labels: [],
201 | datasets: [],
202 | yRegions: [],
203 | yMarkers: []
204 | }
205 | title: '',
206 | colors: [],
207 | height: 200,
208 |
209 | tooltipOptions: {
210 | formatTooltipX: d => (d + '').toUpperCase(),
211 | formatTooltipY: d => d + ' pts',
212 | }
213 |
214 | // Axis charts
215 | isNavigable: 1, // default: 0
216 | valuesOverPoints: 1, // default: 0
217 | barOptions: {
218 | spaceRatio: 1 // default: 0.5
219 | stacked: 1 // default: 0
220 | }
221 |
222 | lineOptions: {
223 | dotSize: 6, // default: 4
224 | hideLine: 0, // default: 0
225 | hideDots: 1, // default: 0
226 | heatline: 1, // default: 0
227 | regionFill: 1 // default: 0
228 | }
229 |
230 | axisOptions: {
231 | yAxisMode: 'span', // Axis lines, default
232 | xAxisMode: 'tick', // No axis lines, only short ticks
233 | xIsSeries: 1 // Allow skipping x values for space
234 | // default: 0
235 | },
236 |
237 | // Pie/Percentage/Donut charts
238 | maxLegendPoints: 6, // default: 20
239 | maxSlices: 10, // default: 20
240 |
241 | // Percentage chart
242 | barOptions: {
243 | height: 15 // default: 20
244 | depth: 5 // default: 2
245 | }
246 |
247 | // Heatmap
248 | discreteDomains: 1, // default: 1
249 | }
250 | ...
251 |
252 | // Updating values
253 | chart.update(data);
254 |
255 | // Axis charts:
256 | chart.addDataPoint(label, valueFromEachDataset, index)
257 | chart.removeDataPoint(index)
258 | chart.updateDataset(datasetValues, index)
259 |
260 | // Exporting
261 | chart.export();
262 |
263 | // Unbind window-resize events
264 | chart.destroy();
265 |
266 |
267 |
268 |
269 |
270 | Install
271 | Install via npm
272 | npm install frappe-charts
273 | And include it in your project
274 | import { Chart } from "frappe-charts"
275 | (for ES6+ import the ESModule from the dist folder)
276 | import { Chart } from "frappe-charts/dist/frappe-charts.esm.js"
277 | ... or include it directly in your HTML
278 | <script src="https://unpkg.com/frappe-charts@1.1.0"></script>
279 | Use as:
280 | new Chart(); // ES6 module
281 | // or
282 | new frappe.Chart(); // Browser
283 |
284 |
285 |
286 |
287 | Download
288 |
289 |
290 | View on GitHub
291 |
292 |
293 | Star
294 |
295 | License: MIT
296 |
297 |
298 |
299 |
300 |
301 | Project maintained by Frappe .
302 | Used in ERPNext .
303 | Read the blog post .
304 |
305 |
306 | Data from the American Meteor Society ,
307 | SILSO and
308 | NASA Open APIs
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frappe-charts",
3 | "version": "v1.6.3",
4 | "type": "module",
5 | "main": "dist/frappe-charts.esm.js",
6 | "module": "dist/frappe-charts.esm.js",
7 | "browser": "dist/frappe-charts.umd.js",
8 | "common": "dist/frappe-charts.cjs.js",
9 | "unnpkg": "dist/frappe-charts.umd.js",
10 | "description": "https://frappe.github.io/charts",
11 | "directories": {
12 | "doc": "docs"
13 | },
14 | "files": [
15 | "src",
16 | "dist"
17 | ],
18 | "scripts": {
19 | "test": "echo \"Error: no test specified\" && exit 1",
20 | "watch": "rollup -c --watch",
21 | "dev": "npm-run-all --parallel watch",
22 | "build": "rollup -c"
23 | },
24 | "repository": {
25 | "type": "git",
26 | "url": "git+https://github.com/frappe/charts.git"
27 | },
28 | "keywords": [
29 | "js",
30 | "charts"
31 | ],
32 | "author": "Prateeksha Singh",
33 | "license": "MIT",
34 | "bugs": {
35 | "url": "https://github.com/frappe/charts/issues"
36 | },
37 | "homepage": "https://github.com/frappe/charts#readme",
38 | "devDependencies": {
39 | "@babel/core": "^7.10.5",
40 | "@babel/preset-env": "^7.10.4",
41 | "node-sass": "^8.0.0",
42 | "rollup": "^2.21.0",
43 | "rollup-plugin-babel": "^4.4.0",
44 | "rollup-plugin-bundle-size": "^1.0.3",
45 | "rollup-plugin-commonjs": "^10.1.0",
46 | "rollup-plugin-eslint": "^7.0.0",
47 | "rollup-plugin-postcss": "^3.1.3",
48 | "rollup-plugin-scss": "^2.5.0",
49 | "rollup-plugin-terser": "^6.1.0"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import pkg from "./package.json";
2 |
3 | import commonjs from "rollup-plugin-commonjs";
4 | import babel from "rollup-plugin-babel";
5 | import postcss from "rollup-plugin-postcss";
6 | import scss from "rollup-plugin-scss";
7 | import bundleSize from "rollup-plugin-bundle-size";
8 | import { terser } from "rollup-plugin-terser";
9 |
10 | export default [
11 | // browser-friendly UMD build
12 | {
13 | input: "src/js/index.js",
14 | output: {
15 | sourcemap: true,
16 | name: "frappe",
17 | file: pkg.browser,
18 | format: "umd",
19 | },
20 | plugins: [
21 | commonjs(),
22 | babel({
23 | exclude: ["node_modules/**"],
24 | }),
25 | terser(),
26 | scss({ output: "dist/frappe-charts.min.css" }),
27 | bundleSize(),
28 | ],
29 | },
30 |
31 | // CommonJS (for Node) and ES module (for bundlers) build.
32 | {
33 | input: "src/js/chart.js",
34 | output: [
35 | { file: pkg.common, format: "cjs", sourcemap: true },
36 | { file: pkg.module, format: "es", sourcemap: true },
37 | ],
38 | plugins: [
39 | babel({
40 | exclude: ["node_modules/**"],
41 | }),
42 | terser(),
43 | postcss(),
44 | bundleSize(),
45 | ],
46 | },
47 | ];
48 |
--------------------------------------------------------------------------------
/src/css/charts.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --charts-label-color: #313b44;
3 | --charts-axis-line-color: #f4f5f6;
4 |
5 | --charts-tooltip-title: var(--charts-label-color);
6 | --charts-tooltip-label: var(--charts-label-color);
7 | --charts-tooltip-value: #192734;
8 | --charts-tooltip-bg: #ffffff;
9 |
10 | --charts-stroke-width: 2px;
11 | --charts-dataset-circle-stroke: #ffffff;
12 | --charts-dataset-circle-stroke-width: var(--charts-stroke-width);
13 |
14 | --charts-legend-label: var(--charts-label-color);
15 | --charts-legend-value: var(--charts-label-color);
16 | }
17 |
18 | .chart-container {
19 | position: relative;
20 | /* for absolutely positioned tooltip */
21 |
22 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
23 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
24 | sans-serif;
25 |
26 | .axis,
27 | .chart-label {
28 | fill: var(--charts-label-color);
29 |
30 | line {
31 | stroke: var(--charts-axis-line-color);
32 | }
33 | }
34 |
35 | .dataset-units {
36 | circle {
37 | stroke: var(--charts-dataset-circle-stroke);
38 | stroke-width: var(--charts-dataset-circle-stroke-width);
39 | }
40 |
41 | path {
42 | fill: none;
43 | stroke-opacity: 1;
44 | stroke-width: var(--charts-stroke-width);
45 | }
46 | }
47 |
48 | .dataset-path {
49 | stroke-width: var(--charts-stroke-width);
50 | }
51 |
52 | .path-group {
53 | path {
54 | fill: none;
55 | stroke-opacity: 1;
56 | stroke-width: var(--charts-stroke-width);
57 | }
58 | }
59 |
60 | line.dashed {
61 | stroke-dasharray: 5, 3;
62 | }
63 |
64 | .axis-line {
65 | .specific-value {
66 | text-anchor: start;
67 | }
68 |
69 | .y-line {
70 | text-anchor: end;
71 | }
72 |
73 | .x-line {
74 | text-anchor: middle;
75 | }
76 | }
77 |
78 | .legend-dataset-label {
79 | fill: var(--charts-legend-label);
80 | font-weight: 600;
81 | }
82 |
83 | .legend-dataset-value {
84 | fill: var(--charts-legend-value);
85 | }
86 | }
87 |
88 | .graph-svg-tip {
89 | position: absolute;
90 | z-index: 99999;
91 | padding: 10px;
92 | font-size: 12px;
93 | text-align: center;
94 | background: var(--charts-tooltip-bg);
95 | box-shadow: 0px 1px 4px rgba(17, 43, 66, 0.1),
96 | 0px 2px 6px rgba(17, 43, 66, 0.08),
97 | 0px 40px 30px -30px rgba(17, 43, 66, 0.1);
98 | border-radius: 6px;
99 |
100 | ul {
101 | padding-left: 0;
102 | display: flex;
103 | }
104 |
105 | ol {
106 | padding-left: 0;
107 | display: flex;
108 | }
109 |
110 | ul.data-point-list {
111 | li {
112 | min-width: 90px;
113 | font-weight: 600;
114 | }
115 | }
116 |
117 | .svg-pointer {
118 | position: absolute;
119 | height: 12px;
120 | width: 12px;
121 | border-radius: 2px;
122 | background: var(--charts-tooltip-bg);
123 | transform: rotate(45deg);
124 | margin-top: -7px;
125 | margin-left: -6px;
126 | }
127 |
128 | &.comparison {
129 | text-align: left;
130 | padding: 0px;
131 | pointer-events: none;
132 |
133 | .title {
134 | display: block;
135 | padding: 16px;
136 | margin: 0;
137 | color: var(--charts-tooltip-title);
138 | font-weight: 600;
139 | line-height: 1;
140 | pointer-events: none;
141 | text-transform: uppercase;
142 |
143 | strong {
144 | color: var(--charts-tooltip-value);
145 | }
146 | }
147 |
148 | ul {
149 | margin: 0;
150 | white-space: nowrap;
151 | list-style: none;
152 |
153 | &.tooltip-grid {
154 | display: grid;
155 | grid-template-columns: repeat(4, minmax(0, 1fr));
156 | gap: 5px;
157 | }
158 | }
159 |
160 | li {
161 | display: inline-block;
162 | display: flex;
163 | flex-direction: row;
164 | font-weight: 600;
165 | line-height: 1;
166 |
167 | padding: 5px 15px 15px 15px;
168 |
169 | .tooltip-legend {
170 | height: 12px;
171 | width: 12px;
172 | margin-right: 8px;
173 | border-radius: 2px;
174 | }
175 |
176 | .tooltip-label {
177 | margin-top: 4px;
178 | font-size: 11px;
179 | max-width: 100px;
180 |
181 | color: var(--fr-tooltip-label);
182 | overflow: hidden;
183 | text-overflow: ellipsis;
184 | white-space: nowrap;
185 | }
186 |
187 | .tooltip-value {
188 | color: var(--charts-tooltip-value);
189 | }
190 | }
191 | }
192 | }
--------------------------------------------------------------------------------
/src/css/chartsCss.js:
--------------------------------------------------------------------------------
1 | export const CSSTEXT =
2 | ".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;}.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}";
3 |
--------------------------------------------------------------------------------
/src/js/chart.js:
--------------------------------------------------------------------------------
1 | import "../css/charts.scss";
2 |
3 | import PercentageChart from "./charts/PercentageChart";
4 | import PieChart from "./charts/PieChart";
5 | import Heatmap from "./charts/Heatmap";
6 | import AxisChart from "./charts/AxisChart";
7 | import DonutChart from "./charts/DonutChart";
8 |
9 | const chartTypes = {
10 | bar: AxisChart,
11 | line: AxisChart,
12 | percentage: PercentageChart,
13 | heatmap: Heatmap,
14 | pie: PieChart,
15 | donut: DonutChart,
16 | };
17 |
18 | function getChartByType(chartType = "line", parent, options) {
19 | if (chartType === "axis-mixed") {
20 | options.type = "line";
21 | return new AxisChart(parent, options);
22 | }
23 |
24 | if (!chartTypes[chartType]) {
25 | console.error("Undefined chart type: " + chartType);
26 | return;
27 | }
28 |
29 | return new chartTypes[chartType](parent, options);
30 | }
31 |
32 | class Chart {
33 | constructor(parent, options) {
34 | return getChartByType(options.type, parent, options);
35 | }
36 | }
37 |
38 | export { Chart, PercentageChart, PieChart, Heatmap, AxisChart };
39 |
--------------------------------------------------------------------------------
/src/js/charts/AggregationChart.js:
--------------------------------------------------------------------------------
1 | import BaseChart from "./BaseChart";
2 | import { truncateString } from "../utils/draw-utils";
3 | import { legendDot } from "../utils/draw";
4 | import { round } from "../utils/helpers";
5 | import { getExtraWidth } from "../utils/constants";
6 |
7 | export default class AggregationChart extends BaseChart {
8 | constructor(parent, args) {
9 | super(parent, args);
10 | }
11 |
12 | configure(args) {
13 | super.configure(args);
14 |
15 | this.config.formatTooltipY = (args.tooltipOptions || {}).formatTooltipY;
16 | this.config.maxSlices = args.maxSlices || 20;
17 | this.config.maxLegendPoints = args.maxLegendPoints || 20;
18 | this.config.legendRowHeight = 60;
19 | }
20 |
21 | calc() {
22 | let s = this.state;
23 | let maxSlices = this.config.maxSlices;
24 | s.sliceTotals = [];
25 |
26 | let allTotals = this.data.labels
27 | .map((label, i) => {
28 | let total = 0;
29 | this.data.datasets.map((e) => {
30 | total += e.values[i];
31 | });
32 | return [total, label];
33 | })
34 | .filter((d) => {
35 | return d[0] >= 0;
36 | }); // keep only positive results
37 |
38 | let totals = allTotals;
39 | if (allTotals.length > maxSlices) {
40 | // Prune and keep a grey area for rest as per maxSlices
41 | allTotals.sort((a, b) => {
42 | return b[0] - a[0];
43 | });
44 |
45 | totals = allTotals.slice(0, maxSlices - 1);
46 | let remaining = allTotals.slice(maxSlices - 1);
47 |
48 | let sumOfRemaining = 0;
49 | remaining.map((d) => {
50 | sumOfRemaining += d[0];
51 | });
52 | totals.push([sumOfRemaining, "Rest"]);
53 | this.colors[maxSlices - 1] = "grey";
54 | }
55 |
56 | s.labels = [];
57 | totals.map((d) => {
58 | s.sliceTotals.push(round(d[0]));
59 | s.labels.push(d[1]);
60 | });
61 |
62 | s.grandTotal = s.sliceTotals.reduce((a, b) => a + b, 0);
63 |
64 | this.center = {
65 | x: this.width / 2,
66 | y: this.height / 2,
67 | };
68 | }
69 |
70 | renderLegend() {
71 | let s = this.state;
72 | this.legendArea.textContent = "";
73 | this.legendTotals = s.sliceTotals.slice(0, this.config.maxLegendPoints);
74 | super.renderLegend(this.legendTotals);
75 | }
76 |
77 | makeLegend(data, index, x_pos, y_pos) {
78 | let formatted = this.config.formatTooltipY
79 | ? this.config.formatTooltipY(data)
80 | : data;
81 |
82 | return legendDot(
83 | x_pos,
84 | y_pos,
85 | 12, // size
86 | 3, // dot radius
87 | this.colors[index], // fill
88 | this.state.labels[index], // label
89 | formatted, // value
90 | null, // base_font_size
91 | this.config.truncateLegends // truncate_legends
92 | );
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/js/charts/BaseChart.js:
--------------------------------------------------------------------------------
1 | import SvgTip from "../objects/SvgTip";
2 | import {
3 | $,
4 | isElementInViewport,
5 | getElementContentWidth,
6 | isHidden,
7 | } from "../utils/dom";
8 | import {
9 | makeSVGContainer,
10 | makeSVGDefs,
11 | makeSVGGroup,
12 | makeText,
13 | } from "../utils/draw";
14 | import { LEGEND_ITEM_WIDTH } from "../utils/constants";
15 | import {
16 | BASE_MEASURES,
17 | getExtraHeight,
18 | getExtraWidth,
19 | getTopOffset,
20 | getLeftOffset,
21 | INIT_CHART_UPDATE_TIMEOUT,
22 | CHART_POST_ANIMATE_TIMEOUT,
23 | DEFAULT_COLORS,
24 | } from "../utils/constants";
25 | import { getColor, isValidColor } from "../utils/colors";
26 | import { runSMILAnimation } from "../utils/animation";
27 | import { downloadFile, prepareForExport } from "../utils/export";
28 | import { deepClone } from "../utils/helpers";
29 |
30 | export default class BaseChart {
31 | constructor(parent, options) {
32 | // deepclone options to avoid making changes to orignal object
33 | options = deepClone(options);
34 |
35 | this.parent =
36 | typeof parent === "string"
37 | ? document.querySelector(parent)
38 | : parent;
39 |
40 | if (!(this.parent instanceof HTMLElement)) {
41 | throw new Error("No `parent` element to render on was provided.");
42 | }
43 |
44 | this.rawChartArgs = options;
45 |
46 | this.title = options.title || "";
47 | this.type = options.type || "";
48 |
49 | this.colors = this.validateColors(options.colors, this.type);
50 |
51 | this.config = {
52 | showTooltip: 1, // calculate
53 | showLegend:
54 | typeof options.showLegend !== "undefined"
55 | ? options.showLegend
56 | : 1,
57 | isNavigable: options.isNavigable || 0,
58 | animate: 0,
59 | overrideCeiling: options.overrideCeiling || false,
60 | overrideFloor: options.overrideFloor || false,
61 | truncateLegends:
62 | typeof options.truncateLegends !== "undefined"
63 | ? options.truncateLegends
64 | : 1,
65 | continuous:
66 | typeof options.continuous !== "undefined"
67 | ? options.continuous
68 | : 1,
69 | };
70 |
71 | this.measures = JSON.parse(JSON.stringify(BASE_MEASURES));
72 | let m = this.measures;
73 |
74 | this.realData = this.prepareData(options.data, this.config);
75 | this.data = this.prepareFirstData(this.realData);
76 |
77 | this.setMeasures(options);
78 | if (!this.title.length) {
79 | m.titleHeight = 0;
80 | }
81 | if (!this.config.showLegend) m.legendHeight = 0;
82 | this.argHeight = options.height || m.baseHeight;
83 |
84 | this.state = {};
85 | this.options = {};
86 |
87 | this.initTimeout = INIT_CHART_UPDATE_TIMEOUT;
88 |
89 | if (this.config.isNavigable) {
90 | this.overlays = [];
91 | }
92 |
93 | this.configure(options);
94 | }
95 |
96 | prepareData(data) {
97 | return data;
98 | }
99 |
100 | prepareFirstData(data) {
101 | return data;
102 | }
103 |
104 | validateColors(colors, type) {
105 | const validColors = [];
106 | colors = (colors || []).concat(DEFAULT_COLORS[type]);
107 | colors.forEach((string) => {
108 | const color = getColor(string);
109 | if (!isValidColor(color)) {
110 | console.warn('"' + string + '" is not a valid color.');
111 | } else {
112 | validColors.push(color);
113 | }
114 | });
115 | return validColors;
116 | }
117 |
118 | setMeasures() {
119 | // Override measures, including those for title and legend
120 | // set config for legend and title
121 | }
122 |
123 | configure() {
124 | let height = this.argHeight;
125 | this.baseHeight = height;
126 | this.height = height - getExtraHeight(this.measures);
127 |
128 | // Bind window events
129 | this.boundDrawFn = () => this.draw(true);
130 | // Look into improving responsiveness
131 | //if (ResizeObserver) {
132 | // this.resizeObserver = new ResizeObserver(this.boundDrawFn);
133 | // this.resizeObserver.observe(this.parent);
134 | //}
135 | window.addEventListener("resize", this.boundDrawFn);
136 | window.addEventListener("orientationchange", this.boundDrawFn);
137 | }
138 |
139 | destroy() {
140 | //if (this.resizeObserver) this.resizeObserver.disconnect();
141 | window.removeEventListener("resize", this.boundDrawFn);
142 | window.removeEventListener("orientationchange", this.boundDrawFn);
143 | }
144 |
145 | // Has to be called manually
146 | setup() {
147 | this.makeContainer();
148 | this.updateWidth();
149 | this.makeTooltip();
150 |
151 | this.draw(false, true);
152 | }
153 |
154 | makeContainer() {
155 | // Chart needs a dedicated parent element
156 | this.parent.innerHTML = "";
157 |
158 | let args = {
159 | inside: this.parent,
160 | className: "chart-container",
161 | };
162 |
163 | if (this.independentWidth) {
164 | args.styles = { width: this.independentWidth + "px" };
165 | }
166 |
167 | this.container = $.create("div", args);
168 | }
169 |
170 | makeTooltip() {
171 | this.tip = new SvgTip({
172 | parent: this.container,
173 | colors: this.colors,
174 | });
175 | this.bindTooltip();
176 | }
177 |
178 | bindTooltip() {}
179 |
180 | draw(onlyWidthChange = false, init = false) {
181 | if (onlyWidthChange && isHidden(this.parent)) {
182 | // Don't update anything if the chart is hidden
183 | return;
184 | }
185 | this.updateWidth();
186 |
187 | this.calc(onlyWidthChange);
188 | this.makeChartArea();
189 | this.setupComponents();
190 |
191 | this.components.forEach((c) => c.setup(this.drawArea));
192 | // this.components.forEach(c => c.make());
193 | this.render(this.components, false);
194 |
195 | if (init) {
196 | this.data = this.realData;
197 | this.update(this.data, true);
198 | // Not needed anymore since animate defaults to 0 and might potentially be refactored or deprecated
199 | /* setTimeout(() => {
200 | this.update(this.data, true);
201 | }, this.initTimeout); */
202 | }
203 |
204 | if (this.config.showLegend) {
205 | this.renderLegend();
206 | }
207 |
208 | this.setupNavigation(init);
209 | }
210 |
211 | calc() {} // builds state
212 |
213 | updateWidth() {
214 | this.baseWidth = getElementContentWidth(this.parent);
215 | this.width = this.baseWidth - getExtraWidth(this.measures);
216 | }
217 |
218 | makeChartArea() {
219 | if (this.svg) {
220 | this.container.removeChild(this.svg);
221 | }
222 | let m = this.measures;
223 |
224 | this.svg = makeSVGContainer(
225 | this.container,
226 | "frappe-chart chart",
227 | this.baseWidth,
228 | this.baseHeight
229 | );
230 | this.svgDefs = makeSVGDefs(this.svg);
231 |
232 | if (this.title.length) {
233 | this.titleEL = makeText(
234 | "title",
235 | m.margins.left,
236 | m.margins.top,
237 | this.title,
238 | {
239 | fontSize: m.titleFontSize,
240 | fill: "#666666",
241 | dy: m.titleFontSize,
242 | }
243 | );
244 | }
245 |
246 | let top = getTopOffset(m);
247 | this.drawArea = makeSVGGroup(
248 | this.type + "-chart chart-draw-area",
249 | `translate(${getLeftOffset(m)}, ${top})`
250 | );
251 |
252 | if (this.config.showLegend) {
253 | top += this.height + m.paddings.bottom;
254 | this.legendArea = makeSVGGroup(
255 | "chart-legend",
256 | `translate(${getLeftOffset(m)}, ${top})`
257 | );
258 | }
259 |
260 | if (this.title.length) {
261 | this.svg.appendChild(this.titleEL);
262 | }
263 | this.svg.appendChild(this.drawArea);
264 | if (this.config.showLegend) {
265 | this.svg.appendChild(this.legendArea);
266 | }
267 |
268 | this.updateTipOffset(getLeftOffset(m), getTopOffset(m));
269 | }
270 |
271 | updateTipOffset(x, y) {
272 | this.tip.offset = {
273 | x: x,
274 | y: y,
275 | };
276 | }
277 |
278 | setupComponents() {
279 | this.components = new Map();
280 | }
281 |
282 | update(data, drawing = false, config) {
283 | if (!data) console.error("No data to update.");
284 | if (!drawing) data = deepClone(data);
285 | this.data = this.prepareData(data, config);
286 | this.calc(); // builds state
287 | this.render(this.components, this.config.animate);
288 | }
289 |
290 | render(components = this.components, animate = true) {
291 | if (this.config.isNavigable) {
292 | // Remove all existing overlays
293 | this.overlays.map((o) => o.parentNode.removeChild(o));
294 | // ref.parentNode.insertBefore(element, ref);
295 | }
296 | let elementsToAnimate = [];
297 | // Can decouple to this.refreshComponents() first to save animation timeout
298 | components.forEach((c) => {
299 | elementsToAnimate = elementsToAnimate.concat(c.update(animate));
300 | });
301 | if (elementsToAnimate.length > 0) {
302 | runSMILAnimation(this.container, this.svg, elementsToAnimate);
303 | setTimeout(() => {
304 | components.forEach((c) => c.make());
305 | this.updateNav();
306 | }, CHART_POST_ANIMATE_TIMEOUT);
307 | } else {
308 | components.forEach((c) => c.make());
309 | this.updateNav();
310 | }
311 | }
312 |
313 | updateNav() {
314 | if (this.config.isNavigable) {
315 | this.makeOverlay();
316 | this.bindUnits();
317 | }
318 | }
319 |
320 | renderLegend(dataset) {
321 | this.legendArea.textContent = "";
322 | let count = 0;
323 | let y = 0;
324 |
325 | dataset.map((data, index) => {
326 | let divisor = Math.floor(this.width / LEGEND_ITEM_WIDTH);
327 | if (count > divisor) {
328 | count = 0;
329 | y += this.config.legendRowHeight;
330 | }
331 | let x = LEGEND_ITEM_WIDTH * count;
332 | let dot = this.makeLegend(data, index, x, y);
333 | this.legendArea.appendChild(dot);
334 | count++;
335 | });
336 | }
337 |
338 | makeLegend() {}
339 |
340 | setupNavigation(init = false) {
341 | if (!this.config.isNavigable) return;
342 |
343 | if (init) {
344 | this.bindOverlay();
345 |
346 | this.keyActions = {
347 | 13: this.onEnterKey.bind(this),
348 | 37: this.onLeftArrow.bind(this),
349 | 38: this.onUpArrow.bind(this),
350 | 39: this.onRightArrow.bind(this),
351 | 40: this.onDownArrow.bind(this),
352 | };
353 |
354 | document.addEventListener("keydown", (e) => {
355 | if (isElementInViewport(this.container)) {
356 | e = e || window.event;
357 | if (this.keyActions[e.keyCode]) {
358 | this.keyActions[e.keyCode]();
359 | }
360 | }
361 | });
362 | }
363 | }
364 |
365 | makeOverlay() {}
366 | updateOverlay() {}
367 | bindOverlay() {}
368 | bindUnits() {}
369 |
370 | onLeftArrow() {}
371 | onRightArrow() {}
372 | onUpArrow() {}
373 | onDownArrow() {}
374 | onEnterKey() {}
375 |
376 | addDataPoint() {}
377 | removeDataPoint() {}
378 |
379 | getDataPoint() {}
380 | setCurrentDataPoint() {}
381 |
382 | updateDataset() {}
383 |
384 | export() {
385 | let chartSvg = prepareForExport(this.svg);
386 | downloadFile(this.title || "Chart", [chartSvg]);
387 | }
388 | }
389 |
--------------------------------------------------------------------------------
/src/js/charts/DonutChart.js:
--------------------------------------------------------------------------------
1 | import AggregationChart from "./AggregationChart";
2 | import { getComponent } from "../objects/ChartComponents";
3 | import { getOffset } from "../utils/dom";
4 | import { getPositionByAngle } from "../utils/helpers";
5 | import { makeArcStrokePathStr, makeStrokeCircleStr } from "../utils/draw";
6 | import { lightenDarkenColor } from "../utils/colors";
7 | import { transform } from "../utils/animation";
8 | import { FULL_ANGLE } from "../utils/constants";
9 |
10 | export default class DonutChart extends AggregationChart {
11 | constructor(parent, args) {
12 | super(parent, args);
13 | this.type = "donut";
14 | this.initTimeout = 0;
15 | this.init = 1;
16 |
17 | this.setup();
18 | }
19 |
20 | configure(args) {
21 | super.configure(args);
22 | this.mouseMove = this.mouseMove.bind(this);
23 | this.mouseLeave = this.mouseLeave.bind(this);
24 |
25 | this.hoverRadio = args.hoverRadio || 0.1;
26 | this.config.startAngle = args.startAngle || 0;
27 |
28 | this.clockWise = args.clockWise || false;
29 | this.strokeWidth = args.strokeWidth || 30;
30 | }
31 |
32 | calc() {
33 | super.calc();
34 | let s = this.state;
35 | this.radius =
36 | this.height > this.width
37 | ? this.center.x - this.strokeWidth / 2
38 | : this.center.y - this.strokeWidth / 2;
39 |
40 | const { radius, clockWise } = this;
41 |
42 | const prevSlicesProperties = s.slicesProperties || [];
43 | s.sliceStrings = [];
44 | s.slicesProperties = [];
45 | let curAngle = 180 - this.config.startAngle;
46 |
47 | s.sliceTotals.map((total, i) => {
48 | const startAngle = curAngle;
49 | const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE;
50 | const largeArc = originDiffAngle > 180 ? 1 : 0;
51 | const diffAngle = clockWise ? -originDiffAngle : originDiffAngle;
52 | const endAngle = (curAngle = curAngle + diffAngle);
53 | const startPosition = getPositionByAngle(startAngle, radius);
54 | const endPosition = getPositionByAngle(endAngle, radius);
55 |
56 | const prevProperty = this.init && prevSlicesProperties[i];
57 |
58 | let curStart, curEnd;
59 | if (this.init) {
60 | curStart = prevProperty ? prevProperty.startPosition : startPosition;
61 | curEnd = prevProperty ? prevProperty.endPosition : startPosition;
62 | } else {
63 | curStart = startPosition;
64 | curEnd = endPosition;
65 | }
66 | const curPath =
67 | originDiffAngle === 360
68 | ? makeStrokeCircleStr(
69 | curStart,
70 | curEnd,
71 | this.center,
72 | this.radius,
73 | this.clockWise,
74 | largeArc
75 | )
76 | : makeArcStrokePathStr(
77 | curStart,
78 | curEnd,
79 | this.center,
80 | this.radius,
81 | this.clockWise,
82 | largeArc
83 | );
84 |
85 | s.sliceStrings.push(curPath);
86 | s.slicesProperties.push({
87 | startPosition,
88 | endPosition,
89 | value: total,
90 | total: s.grandTotal,
91 | startAngle,
92 | endAngle,
93 | angle: diffAngle,
94 | });
95 | });
96 | this.init = 0;
97 | }
98 |
99 | setupComponents() {
100 | let s = this.state;
101 |
102 | let componentConfigs = [
103 | [
104 | "donutSlices",
105 | {},
106 | function () {
107 | return {
108 | sliceStrings: s.sliceStrings,
109 | colors: this.colors,
110 | strokeWidth: this.strokeWidth,
111 | };
112 | }.bind(this),
113 | ],
114 | ];
115 |
116 | this.components = new Map(
117 | componentConfigs.map((args) => {
118 | let component = getComponent(...args);
119 | return [args[0], component];
120 | })
121 | );
122 | }
123 |
124 | calTranslateByAngle(property) {
125 | const { radius, hoverRadio } = this;
126 | const position = getPositionByAngle(
127 | property.startAngle + property.angle / 2,
128 | radius
129 | );
130 | return `translate3d(${position.x * hoverRadio}px,${
131 | position.y * hoverRadio
132 | }px,0)`;
133 | }
134 |
135 | hoverSlice(path, i, flag, e) {
136 | if (!path) return;
137 | const color = this.colors[i];
138 | if (flag) {
139 | transform(path, this.calTranslateByAngle(this.state.slicesProperties[i]));
140 | path.style.stroke = lightenDarkenColor(color, 50);
141 | let g_off = getOffset(this.svg);
142 | let x = e.pageX - g_off.left + 10;
143 | let y = e.pageY - g_off.top - 10;
144 | let title =
145 | (this.formatted_labels && this.formatted_labels.length > 0
146 | ? this.formatted_labels[i]
147 | : this.state.labels[i]) + ": ";
148 | let percent = (
149 | (this.state.sliceTotals[i] * 100) /
150 | this.state.grandTotal
151 | ).toFixed(1);
152 | this.tip.setValues(x, y, { name: title, value: percent + "%" });
153 | this.tip.showTip();
154 | } else {
155 | transform(path, "translate3d(0,0,0)");
156 | this.tip.hideTip();
157 | path.style.stroke = color;
158 | }
159 | }
160 |
161 | bindTooltip() {
162 | this.container.addEventListener("mousemove", this.mouseMove);
163 | this.container.addEventListener("mouseleave", this.mouseLeave);
164 | }
165 |
166 | mouseMove(e) {
167 | const target = e.target;
168 | let slices = this.components.get("donutSlices").store;
169 | let prevIndex = this.curActiveSliceIndex;
170 | let prevAcitve = this.curActiveSlice;
171 | if (slices.includes(target)) {
172 | let i = slices.indexOf(target);
173 | this.hoverSlice(prevAcitve, prevIndex, false);
174 | this.curActiveSlice = target;
175 | this.curActiveSliceIndex = i;
176 | this.hoverSlice(target, i, true, e);
177 | } else {
178 | this.mouseLeave();
179 | }
180 | }
181 |
182 | mouseLeave() {
183 | this.hoverSlice(this.curActiveSlice, this.curActiveSliceIndex, false);
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/src/js/charts/Heatmap.js:
--------------------------------------------------------------------------------
1 | import BaseChart from "./BaseChart";
2 | import { getComponent } from "../objects/ChartComponents";
3 | import { makeText, heatSquare } from "../utils/draw";
4 | import {
5 | DAY_NAMES_SHORT,
6 | toMidnightUTC,
7 | addDays,
8 | areInSameMonth,
9 | getLastDateInMonth,
10 | setDayToSunday,
11 | getYyyyMmDd,
12 | getWeeksBetween,
13 | getMonthName,
14 | clone,
15 | NO_OF_MILLIS,
16 | NO_OF_YEAR_MONTHS,
17 | NO_OF_DAYS_IN_WEEK,
18 | } from "../utils/date-utils";
19 | import { calcDistribution, getMaxCheckpoint } from "../utils/intervals";
20 | import {
21 | getExtraHeight,
22 | getExtraWidth,
23 | HEATMAP_DISTRIBUTION_SIZE,
24 | HEATMAP_SQUARE_SIZE,
25 | HEATMAP_GUTTER_SIZE,
26 | } from "../utils/constants";
27 |
28 | const COL_WIDTH = HEATMAP_SQUARE_SIZE + HEATMAP_GUTTER_SIZE;
29 | const ROW_HEIGHT = COL_WIDTH;
30 | // const DAY_INCR = 1;
31 |
32 | export default class Heatmap extends BaseChart {
33 | constructor(parent, options) {
34 | super(parent, options);
35 | this.type = "heatmap";
36 |
37 | this.countLabel = options.countLabel || "";
38 |
39 | let validStarts = ["Sunday", "Monday"];
40 | let startSubDomain = validStarts.includes(options.startSubDomain)
41 | ? options.startSubDomain
42 | : "Sunday";
43 | this.startSubDomainIndex = validStarts.indexOf(startSubDomain);
44 |
45 | this.setup();
46 | }
47 |
48 | setMeasures(options) {
49 | let m = this.measures;
50 | this.discreteDomains = options.discreteDomains === 0 ? 0 : 1;
51 |
52 | m.paddings.top = ROW_HEIGHT * 3;
53 | m.paddings.bottom = 0;
54 | m.legendHeight = ROW_HEIGHT * 2;
55 | m.baseHeight = ROW_HEIGHT * NO_OF_DAYS_IN_WEEK + getExtraHeight(m);
56 |
57 | let d = this.data;
58 | let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0;
59 | this.independentWidth =
60 | (getWeeksBetween(d.start, d.end) + spacing) * COL_WIDTH +
61 | getExtraWidth(m);
62 | }
63 |
64 | updateWidth() {
65 | let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0;
66 | let noOfWeeks = this.state.noOfWeeks ? this.state.noOfWeeks : 52;
67 | this.baseWidth =
68 | (noOfWeeks + spacing) * COL_WIDTH + getExtraWidth(this.measures);
69 | }
70 |
71 | prepareData(data = this.data) {
72 | if (data.start && data.end && data.start > data.end) {
73 | throw new Error("Start date cannot be greater than end date.");
74 | }
75 |
76 | if (!data.start) {
77 | data.start = new Date();
78 | data.start.setFullYear(data.start.getFullYear() - 1);
79 | }
80 | data.start = toMidnightUTC(data.start);
81 |
82 | if (!data.end) {
83 | data.end = new Date();
84 | }
85 | data.end = toMidnightUTC(data.end);
86 |
87 | data.dataPoints = data.dataPoints || {};
88 |
89 | if (parseInt(Object.keys(data.dataPoints)[0]) > 100000) {
90 | let points = {};
91 | Object.keys(data.dataPoints).forEach((timestampSec) => {
92 | let date = new Date(timestampSec * NO_OF_MILLIS);
93 | points[getYyyyMmDd(date)] = data.dataPoints[timestampSec];
94 | });
95 | data.dataPoints = points;
96 | }
97 |
98 | return data;
99 | }
100 |
101 | calc() {
102 | let s = this.state;
103 |
104 | s.start = clone(this.data.start);
105 | s.end = clone(this.data.end);
106 |
107 | s.firstWeekStart = clone(s.start);
108 | s.noOfWeeks = getWeeksBetween(s.start, s.end);
109 | s.distribution = calcDistribution(
110 | Object.values(this.data.dataPoints),
111 | HEATMAP_DISTRIBUTION_SIZE
112 | );
113 |
114 | s.domainConfigs = this.getDomains();
115 | }
116 |
117 | setupComponents() {
118 | let s = this.state;
119 | let lessCol = this.discreteDomains ? 0 : 1;
120 |
121 | let componentConfigs = s.domainConfigs.map((config, i) => [
122 | "heatDomain",
123 | {
124 | index: config.index,
125 | colWidth: COL_WIDTH,
126 | rowHeight: ROW_HEIGHT,
127 | squareSize: HEATMAP_SQUARE_SIZE,
128 | radius: this.rawChartArgs.radius || 0,
129 | xTranslate:
130 | s.domainConfigs
131 | .filter((config, j) => j < i)
132 | .map((config) => config.cols.length - lessCol)
133 | .reduce((a, b) => a + b, 0) * COL_WIDTH,
134 | },
135 | function () {
136 | return s.domainConfigs[i];
137 | }.bind(this),
138 | ]);
139 |
140 | this.components = new Map(
141 | componentConfigs.map((args, i) => {
142 | let component = getComponent(...args);
143 | return [args[0] + "-" + i, component];
144 | })
145 | );
146 |
147 | let y = 0;
148 | DAY_NAMES_SHORT.forEach((dayName, i) => {
149 | if ([1, 3, 5].includes(i)) {
150 | let dayText = makeText("subdomain-name", -COL_WIDTH / 2, y, dayName, {
151 | fontSize: HEATMAP_SQUARE_SIZE,
152 | dy: 8,
153 | textAnchor: "end",
154 | });
155 | this.drawArea.appendChild(dayText);
156 | }
157 | y += ROW_HEIGHT;
158 | });
159 | }
160 |
161 | update(data) {
162 | if (!data) {
163 | console.error("No data to update.");
164 | }
165 |
166 | this.data = this.prepareData(data);
167 | this.draw();
168 | this.bindTooltip();
169 | }
170 |
171 | bindTooltip() {
172 | this.container.addEventListener("mousemove", (e) => {
173 | this.components.forEach((comp) => {
174 | let daySquares = comp.store;
175 | let daySquare = e.target;
176 | if (daySquares.includes(daySquare)) {
177 | let count = daySquare.getAttribute("data-value");
178 | let dateParts = daySquare.getAttribute("data-date").split("-");
179 |
180 | let month = getMonthName(parseInt(dateParts[1]) - 1, true);
181 |
182 | let gOff = this.container.getBoundingClientRect(),
183 | pOff = daySquare.getBoundingClientRect();
184 |
185 | let width = parseInt(e.target.getAttribute("width"));
186 | let x = pOff.left - gOff.left + width / 2;
187 | let y = pOff.top - gOff.top;
188 | let value = count + " " + this.countLabel;
189 | let name = " on " + month + " " + dateParts[0] + ", " + dateParts[2];
190 |
191 | this.tip.setValues(
192 | x,
193 | y,
194 | { name: name, value: value, valueFirst: 1 },
195 | []
196 | );
197 | this.tip.showTip();
198 | }
199 | });
200 | });
201 | }
202 |
203 | renderLegend() {
204 | this.legendArea.textContent = "";
205 | let x = 0;
206 | let y = ROW_HEIGHT;
207 | let radius = this.rawChartArgs.radius || 0;
208 |
209 | let lessText = makeText("subdomain-name", x, y, "Less", {
210 | fontSize: HEATMAP_SQUARE_SIZE + 1,
211 | dy: 9,
212 | });
213 | x = COL_WIDTH * 2 + COL_WIDTH / 2;
214 | this.legendArea.appendChild(lessText);
215 |
216 | this.colors.slice(0, HEATMAP_DISTRIBUTION_SIZE).map((color, i) => {
217 | const square = heatSquare(
218 | "heatmap-legend-unit",
219 | x + (COL_WIDTH + 3) * i,
220 | y,
221 | HEATMAP_SQUARE_SIZE,
222 | radius,
223 | color
224 | );
225 | this.legendArea.appendChild(square);
226 | });
227 |
228 | let moreTextX =
229 | x + HEATMAP_DISTRIBUTION_SIZE * (COL_WIDTH + 3) + COL_WIDTH / 4;
230 | let moreText = makeText("subdomain-name", moreTextX, y, "More", {
231 | fontSize: HEATMAP_SQUARE_SIZE + 1,
232 | dy: 9,
233 | });
234 | this.legendArea.appendChild(moreText);
235 | }
236 |
237 | getDomains() {
238 | let s = this.state;
239 | const [startMonth, startYear] = [s.start.getMonth(), s.start.getFullYear()];
240 | const [endMonth, endYear] = [s.end.getMonth(), s.end.getFullYear()];
241 |
242 | const noOfMonths = endMonth - startMonth + 1 + (endYear - startYear) * 12;
243 |
244 | let domainConfigs = [];
245 |
246 | let startOfMonth = clone(s.start);
247 | for (var i = 0; i < noOfMonths; i++) {
248 | let endDate = s.end;
249 | if (!areInSameMonth(startOfMonth, s.end)) {
250 | let [month, year] = [
251 | startOfMonth.getMonth(),
252 | startOfMonth.getFullYear(),
253 | ];
254 | endDate = getLastDateInMonth(month, year);
255 | }
256 | domainConfigs.push(this.getDomainConfig(startOfMonth, endDate));
257 |
258 | addDays(endDate, 1);
259 | startOfMonth = endDate;
260 | }
261 |
262 | return domainConfigs;
263 | }
264 |
265 | getDomainConfig(startDate, endDate = "") {
266 | let [month, year] = [startDate.getMonth(), startDate.getFullYear()];
267 | let startOfWeek = setDayToSunday(startDate); // TODO: Monday as well
268 | endDate = endDate
269 | ? clone(endDate)
270 | : toMidnightUTC(getLastDateInMonth(month, year));
271 |
272 | let domainConfig = {
273 | index: month,
274 | cols: [],
275 | };
276 |
277 | addDays(endDate, 1);
278 | let noOfMonthWeeks = getWeeksBetween(startOfWeek, endDate);
279 |
280 | let cols = [],
281 | col;
282 | for (var i = 0; i < noOfMonthWeeks; i++) {
283 | col = this.getCol(startOfWeek, month);
284 | cols.push(col);
285 |
286 | startOfWeek = toMidnightUTC(
287 | new Date(col[NO_OF_DAYS_IN_WEEK - 1].yyyyMmDd)
288 | );
289 | addDays(startOfWeek, 1);
290 | }
291 |
292 | if (col[NO_OF_DAYS_IN_WEEK - 1].dataValue !== undefined) {
293 | addDays(startOfWeek, 1);
294 | cols.push(this.getCol(startOfWeek, month, true));
295 | }
296 |
297 | domainConfig.cols = cols;
298 |
299 | return domainConfig;
300 | }
301 |
302 | getCol(startDate, month, empty = false) {
303 | let s = this.state;
304 |
305 | // startDate is the start of week
306 | let currentDate = clone(startDate);
307 | let col = [];
308 |
309 | for (var i = 0; i < NO_OF_DAYS_IN_WEEK; i++, addDays(currentDate, 1)) {
310 | let config = {};
311 |
312 | // Non-generic adjustment for entire heatmap, needs state
313 | let currentDateWithinData =
314 | currentDate >= s.start && currentDate <= s.end;
315 |
316 | if (empty || currentDate.getMonth() !== month || !currentDateWithinData) {
317 | config.yyyyMmDd = getYyyyMmDd(currentDate);
318 | } else {
319 | config = this.getSubDomainConfig(currentDate);
320 | }
321 | col.push(config);
322 | }
323 |
324 | return col;
325 | }
326 |
327 | getSubDomainConfig(date) {
328 | let yyyyMmDd = getYyyyMmDd(date);
329 | let dataValue = this.data.dataPoints[yyyyMmDd];
330 | let config = {
331 | yyyyMmDd: yyyyMmDd,
332 | dataValue: dataValue || 0,
333 | fill: this.colors[getMaxCheckpoint(dataValue, this.state.distribution)],
334 | };
335 | return config;
336 | }
337 | }
338 |
--------------------------------------------------------------------------------
/src/js/charts/PercentageChart.js:
--------------------------------------------------------------------------------
1 | import AggregationChart from "./AggregationChart";
2 | import { getOffset } from "../utils/dom";
3 | import { getComponent } from "../objects/ChartComponents";
4 | import { PERCENTAGE_BAR_DEFAULT_HEIGHT } from "../utils/constants";
5 |
6 | export default class PercentageChart extends AggregationChart {
7 | constructor(parent, args) {
8 | super(parent, args);
9 | this.type = "percentage";
10 | this.setup();
11 | }
12 |
13 | setMeasures(options) {
14 | let m = this.measures;
15 | this.barOptions = options.barOptions || {};
16 |
17 | let b = this.barOptions;
18 | b.height = b.height || PERCENTAGE_BAR_DEFAULT_HEIGHT;
19 |
20 | m.paddings.right = 30;
21 | m.legendHeight = 60;
22 | m.baseHeight = (b.height + b.depth * 0.5) * 8;
23 | }
24 |
25 | setupComponents() {
26 | let s = this.state;
27 |
28 | let componentConfigs = [
29 | [
30 | "percentageBars",
31 | {
32 | barHeight: this.barOptions.height,
33 | },
34 | function () {
35 | return {
36 | xPositions: s.xPositions,
37 | widths: s.widths,
38 | colors: this.colors,
39 | };
40 | }.bind(this),
41 | ],
42 | ];
43 |
44 | this.components = new Map(
45 | componentConfigs.map((args) => {
46 | let component = getComponent(...args);
47 | return [args[0], component];
48 | })
49 | );
50 | }
51 |
52 | calc() {
53 | super.calc();
54 | let s = this.state;
55 |
56 | s.xPositions = [];
57 | s.widths = [];
58 |
59 | let xPos = 0;
60 | s.sliceTotals.map((value) => {
61 | let width = (this.width * value) / s.grandTotal;
62 | s.widths.push(width);
63 | s.xPositions.push(xPos);
64 | xPos += width;
65 | });
66 | }
67 |
68 | makeDataByIndex() {}
69 |
70 | bindTooltip() {
71 | let s = this.state;
72 | this.container.addEventListener("mousemove", (e) => {
73 | let bars = this.components.get("percentageBars").store;
74 | let bar = e.target;
75 | if (bars.includes(bar)) {
76 | let i = bars.indexOf(bar);
77 | let gOff = getOffset(this.container),
78 | pOff = getOffset(bar);
79 |
80 | let x = pOff.left - gOff.left + parseInt(bar.getAttribute("width")) / 2;
81 | let y = pOff.top - gOff.top;
82 | let title =
83 | (this.formattedLabels && this.formattedLabels.length > 0
84 | ? this.formattedLabels[i]
85 | : this.state.labels[i]) + ": ";
86 | let fraction = s.sliceTotals[i] / s.grandTotal;
87 |
88 | this.tip.setValues(x, y, {
89 | name: title,
90 | value: (fraction * 100).toFixed(1) + "%",
91 | });
92 | this.tip.showTip();
93 | }
94 | });
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/js/charts/PieChart.js:
--------------------------------------------------------------------------------
1 | import AggregationChart from "./AggregationChart";
2 | import { getComponent } from "../objects/ChartComponents";
3 | import { getOffset, fire } from "../utils/dom";
4 | import { getPositionByAngle } from "../utils/helpers";
5 | import { makeArcPathStr, makeCircleStr } from "../utils/draw";
6 | import { lightenDarkenColor } from "../utils/colors";
7 | import { transform } from "../utils/animation";
8 | import { FULL_ANGLE } from "../utils/constants";
9 |
10 | export default class PieChart extends AggregationChart {
11 | constructor(parent, args) {
12 | super(parent, args);
13 | this.type = "pie";
14 | this.initTimeout = 0;
15 | this.init = 1;
16 |
17 | this.setup();
18 | }
19 |
20 | configure(args) {
21 | super.configure(args);
22 | this.mouseMove = this.mouseMove.bind(this);
23 | this.mouseLeave = this.mouseLeave.bind(this);
24 |
25 | this.hoverRadio = args.hoverRadio || 0.1;
26 | this.config.startAngle = args.startAngle || 0;
27 |
28 | this.clockWise = args.clockWise || false;
29 | }
30 |
31 | calc() {
32 | super.calc();
33 | let s = this.state;
34 | this.radius = this.height > this.width ? this.center.x : this.center.y;
35 |
36 | const { radius, clockWise } = this;
37 |
38 | const prevSlicesProperties = s.slicesProperties || [];
39 | s.sliceStrings = [];
40 | s.slicesProperties = [];
41 | let curAngle = 180 - this.config.startAngle;
42 |
43 | s.sliceTotals.map((total, i) => {
44 | const startAngle = curAngle;
45 | const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE;
46 | const largeArc = originDiffAngle > 180 ? 1 : 0;
47 | const diffAngle = clockWise ? -originDiffAngle : originDiffAngle;
48 | const endAngle = (curAngle = curAngle + diffAngle);
49 | const startPosition = getPositionByAngle(startAngle, radius);
50 | const endPosition = getPositionByAngle(endAngle, radius);
51 |
52 | const prevProperty = this.init && prevSlicesProperties[i];
53 |
54 | let curStart, curEnd;
55 | if (this.init) {
56 | curStart = prevProperty ? prevProperty.startPosition : startPosition;
57 | curEnd = prevProperty ? prevProperty.endPosition : startPosition;
58 | } else {
59 | curStart = startPosition;
60 | curEnd = endPosition;
61 | }
62 | const curPath =
63 | originDiffAngle === 360
64 | ? makeCircleStr(
65 | curStart,
66 | curEnd,
67 | this.center,
68 | this.radius,
69 | clockWise,
70 | largeArc
71 | )
72 | : makeArcPathStr(
73 | curStart,
74 | curEnd,
75 | this.center,
76 | this.radius,
77 | clockWise,
78 | largeArc
79 | );
80 |
81 | s.sliceStrings.push(curPath);
82 | s.slicesProperties.push({
83 | startPosition,
84 | endPosition,
85 | value: total,
86 | total: s.grandTotal,
87 | startAngle,
88 | endAngle,
89 | angle: diffAngle,
90 | });
91 | });
92 | this.init = 0;
93 | }
94 |
95 | setupComponents() {
96 | let s = this.state;
97 |
98 | let componentConfigs = [
99 | [
100 | "pieSlices",
101 | {},
102 | function () {
103 | return {
104 | sliceStrings: s.sliceStrings,
105 | colors: this.colors,
106 | };
107 | }.bind(this),
108 | ],
109 | ];
110 |
111 | this.components = new Map(
112 | componentConfigs.map((args) => {
113 | let component = getComponent(...args);
114 | return [args[0], component];
115 | })
116 | );
117 | }
118 |
119 | calTranslateByAngle(property) {
120 | const { radius, hoverRadio } = this;
121 | const position = getPositionByAngle(
122 | property.startAngle + property.angle / 2,
123 | radius
124 | );
125 | return `translate3d(${position.x * hoverRadio}px,${
126 | position.y * hoverRadio
127 | }px,0)`;
128 | }
129 |
130 | hoverSlice(path, i, flag, e) {
131 | if (!path) return;
132 | const color = this.colors[i];
133 | if (flag) {
134 | transform(path, this.calTranslateByAngle(this.state.slicesProperties[i]));
135 | path.style.fill = lightenDarkenColor(color, 50);
136 | let g_off = getOffset(this.svg);
137 | let x = e.pageX - g_off.left + 10;
138 | let y = e.pageY - g_off.top - 10;
139 | let title =
140 | (this.formatted_labels && this.formatted_labels.length > 0
141 | ? this.formatted_labels[i]
142 | : this.state.labels[i]) + ": ";
143 | let percent = (
144 | (this.state.sliceTotals[i] * 100) /
145 | this.state.grandTotal
146 | ).toFixed(1);
147 | this.tip.setValues(x, y, { name: title, value: percent + "%" });
148 | this.tip.showTip();
149 | } else {
150 | transform(path, "translate3d(0,0,0)");
151 | this.tip.hideTip();
152 | path.style.fill = color;
153 | }
154 | }
155 |
156 | bindTooltip() {
157 | this.container.addEventListener("mousemove", this.mouseMove);
158 | this.container.addEventListener("mouseleave", this.mouseLeave);
159 | }
160 | getDataPoint(index = this.state.currentIndex) {
161 | let s = this.state;
162 | let data_point = {
163 | index: index,
164 | label: s.labels[index],
165 | values: s.sliceTotals[index],
166 | };
167 | return data_point;
168 | }
169 | setCurrentDataPoint(index) {
170 | let s = this.state;
171 | index = parseInt(index);
172 | if (index < 0) index = 0;
173 | if (index >= s.labels.length) index = s.labels.length - 1;
174 | if (index === s.currentIndex) return;
175 | s.currentIndex = index;
176 | fire(this.parent, "data-select", this.getDataPoint());
177 | }
178 |
179 | bindUnits() {
180 | const units = this.components.get("pieSlices").store;
181 | if (!units) return;
182 | units.forEach((unit, index) => {
183 | unit.addEventListener("click", () => {
184 | this.setCurrentDataPoint(index);
185 | });
186 | });
187 | }
188 | mouseMove(e) {
189 | const target = e.target;
190 | let slices = this.components.get("pieSlices").store;
191 | let prevIndex = this.curActiveSliceIndex;
192 | let prevAcitve = this.curActiveSlice;
193 | if (slices.includes(target)) {
194 | let i = slices.indexOf(target);
195 | this.hoverSlice(prevAcitve, prevIndex, false);
196 | this.curActiveSlice = target;
197 | this.curActiveSliceIndex = i;
198 | this.hoverSlice(target, i, true, e);
199 | } else {
200 | this.mouseLeave();
201 | }
202 | }
203 |
204 | mouseLeave() {
205 | this.hoverSlice(this.curActiveSlice, this.curActiveSliceIndex, false);
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/src/js/index.js:
--------------------------------------------------------------------------------
1 | import * as Charts from "./chart";
2 |
3 | let frappe = {};
4 |
5 | frappe.NAME = "Frappe Charts";
6 | frappe.VERSION = "1.6.2";
7 |
8 | frappe = Object.assign({}, frappe, Charts);
9 |
10 | export default frappe;
11 |
--------------------------------------------------------------------------------
/src/js/objects/ChartComponents.js:
--------------------------------------------------------------------------------
1 | import { makeSVGGroup } from "../utils/draw";
2 | import {
3 | makeText,
4 | makePath,
5 | xLine,
6 | yLine,
7 | generateAxisLabel,
8 | yMarker,
9 | yRegion,
10 | datasetBar,
11 | datasetDot,
12 | percentageBar,
13 | getPaths,
14 | heatSquare,
15 | } from "../utils/draw";
16 | import { equilizeNoOfElements } from "../utils/draw-utils";
17 | import {
18 | translateHoriLine,
19 | translateVertLine,
20 | animateRegion,
21 | animateBar,
22 | animateDot,
23 | animatePath,
24 | animatePathStr,
25 | } from "../utils/animate";
26 | import { getMonthName } from "../utils/date-utils";
27 |
28 | class ChartComponent {
29 | constructor({
30 | layerClass = "",
31 | layerTransform = "",
32 | constants,
33 |
34 | getData,
35 | makeElements,
36 | animateElements,
37 | }) {
38 | this.layerTransform = layerTransform;
39 | this.constants = constants;
40 |
41 | this.makeElements = makeElements;
42 | this.getData = getData;
43 |
44 | this.animateElements = animateElements;
45 |
46 | this.store = [];
47 | this.labels = [];
48 |
49 | this.layerClass = layerClass;
50 | this.layerClass =
51 | typeof this.layerClass === "function"
52 | ? this.layerClass()
53 | : this.layerClass;
54 |
55 | this.refresh();
56 | }
57 |
58 | refresh(data) {
59 | this.data = data || this.getData();
60 | }
61 |
62 | setup(parent) {
63 | this.layer = makeSVGGroup(this.layerClass, this.layerTransform, parent);
64 | }
65 |
66 | make() {
67 | this.render(this.data);
68 | this.oldData = this.data;
69 | }
70 |
71 | render(data) {
72 | this.store = this.makeElements(data);
73 |
74 | this.layer.textContent = "";
75 | this.store.forEach((element) => {
76 | element.length
77 | ? element.forEach((el) => {
78 | this.layer.appendChild(el);
79 | })
80 | : this.layer.appendChild(element);
81 | });
82 | this.labels.forEach((element) => {
83 | this.layer.appendChild(element);
84 | });
85 | }
86 |
87 | update(animate = true) {
88 | this.refresh();
89 | let animateElements = [];
90 | if (animate) {
91 | animateElements = this.animateElements(this.data) || [];
92 | }
93 | return animateElements;
94 | }
95 | }
96 |
97 | let componentConfigs = {
98 | donutSlices: {
99 | layerClass: "donut-slices",
100 | makeElements(data) {
101 | return data.sliceStrings.map((s, i) => {
102 | let slice = makePath(
103 | s,
104 | "donut-path",
105 | data.colors[i],
106 | "none",
107 | data.strokeWidth
108 | );
109 | slice.style.transition = "transform .3s;";
110 | return slice;
111 | });
112 | },
113 |
114 | animateElements(newData) {
115 | return this.store.map((slice, i) =>
116 | animatePathStr(slice, newData.sliceStrings[i])
117 | );
118 | },
119 | },
120 | pieSlices: {
121 | layerClass: "pie-slices",
122 | makeElements(data) {
123 | return data.sliceStrings.map((s, i) => {
124 | let slice = makePath(s, "pie-path", "none", data.colors[i]);
125 | slice.style.transition = "transform .3s;";
126 | return slice;
127 | });
128 | },
129 |
130 | animateElements(newData) {
131 | return this.store.map((slice, i) =>
132 | animatePathStr(slice, newData.sliceStrings[i])
133 | );
134 | },
135 | },
136 | percentageBars: {
137 | layerClass: "percentage-bars",
138 | makeElements(data) {
139 | const numberOfPoints = data.xPositions.length;
140 | return data.xPositions.map((x, i) => {
141 | let y = 0;
142 |
143 | let isLast = i == numberOfPoints - 1;
144 | let isFirst = i == 0;
145 |
146 | let bar = percentageBar(
147 | x,
148 | y,
149 | data.widths[i],
150 | this.constants.barHeight,
151 | isFirst,
152 | isLast,
153 | data.colors[i]
154 | );
155 | return bar;
156 | });
157 | },
158 |
159 | animateElements(newData) {
160 | if (newData) return [];
161 | },
162 | },
163 | yAxis: {
164 | layerClass: "y axis",
165 | makeElements(data) {
166 | let elements = [];
167 | // will loop through each yaxis dataset if it exists
168 | if (data.length) {
169 | data.forEach((item, i) => {
170 | item.positions.map((position, i) => {
171 | elements.push(
172 | yLine(
173 | position,
174 | item.labels[i],
175 | this.constants.width,
176 | {
177 | mode: this.constants.mode,
178 | pos: item.pos || this.constants.pos,
179 | shortenNumbers:
180 | this.constants.shortenNumbers,
181 | title: item.title,
182 | }
183 | )
184 | );
185 | });
186 | // we need to make yAxis titles if they are defined
187 | if (item.title) {
188 | elements.push(
189 | generateAxisLabel({
190 | title: item.title,
191 | position: item.pos,
192 | height: this.constants.height || data.zeroLine,
193 | width: this.constants.width,
194 | })
195 | );
196 | }
197 | });
198 |
199 | return elements;
200 | }
201 |
202 | data.positions.forEach((position, i) => {
203 | elements.push(
204 | yLine(position, data.labels[i], this.constants.width, {
205 | mode: this.constants.mode,
206 | pos: data.pos || this.constants.pos,
207 | shortenNumbers: this.constants.shortenNumbers,
208 | })
209 | );
210 | });
211 |
212 | if (data.title) {
213 | elements.push(
214 | generateAxisLabel({
215 | title: data.title,
216 | position: data.pos,
217 | height: this.constants.height || data.zeroLine,
218 | width: this.constants.width,
219 | })
220 | );
221 | }
222 |
223 | return elements;
224 | },
225 |
226 | animateElements(newData) {
227 | const animateMultipleElements = (oldData, newData) => {
228 | let newPos = newData.positions;
229 | let newLabels = newData.labels;
230 | let oldPos = oldData.positions;
231 | let oldLabels = oldData.labels;
232 |
233 | [oldPos, newPos] = equilizeNoOfElements(oldPos, newPos);
234 | [oldLabels, newLabels] = equilizeNoOfElements(
235 | oldLabels,
236 | newLabels
237 | );
238 |
239 | this.render({
240 | positions: oldPos,
241 | labels: newLabels,
242 | });
243 |
244 | return this.store.map((line, i) => {
245 | return translateHoriLine(line, newPos[i], oldPos[i]);
246 | });
247 | };
248 |
249 | // we will need to animate both axis if we have more than one.
250 | // so check if the oldData is an array of values.
251 | if (this.oldData instanceof Array) {
252 | return this.oldData.forEach((old, i) => {
253 | animateMultipleElements(old, newData[i]);
254 | });
255 | }
256 |
257 | let newPos = newData.positions;
258 | let newLabels = newData.labels;
259 | let oldPos = this.oldData.positions;
260 | let oldLabels = this.oldData.labels;
261 |
262 | [oldPos, newPos] = equilizeNoOfElements(oldPos, newPos);
263 | [oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels);
264 |
265 | this.render({
266 | positions: oldPos,
267 | labels: newLabels,
268 | });
269 |
270 | return this.store.map((line, i) => {
271 | return translateHoriLine(line, newPos[i], oldPos[i]);
272 | });
273 | },
274 | },
275 |
276 | xAxis: {
277 | layerClass: "x axis",
278 | makeElements(data) {
279 | return data.positions.map((position, i) =>
280 | xLine(position, data.calcLabels[i], this.constants.height, {
281 | mode: this.constants.mode,
282 | pos: this.constants.pos,
283 | })
284 | );
285 | },
286 |
287 | animateElements(newData) {
288 | let newPos = newData.positions;
289 | let newLabels = newData.calcLabels;
290 | let oldPos = this.oldData.positions;
291 | let oldLabels = this.oldData.calcLabels;
292 |
293 | [oldPos, newPos] = equilizeNoOfElements(oldPos, newPos);
294 | [oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels);
295 |
296 | this.render({
297 | positions: oldPos,
298 | calcLabels: newLabels,
299 | });
300 |
301 | return this.store.map((line, i) => {
302 | return translateVertLine(line, newPos[i], oldPos[i]);
303 | });
304 | },
305 | },
306 |
307 | yMarkers: {
308 | layerClass: "y-markers",
309 | makeElements(data) {
310 | return data.map((m) =>
311 | yMarker(m.position, m.label, this.constants.width, {
312 | labelPos: m.options.labelPos,
313 | stroke: m.options.stroke,
314 | mode: "span",
315 | lineType: m.options.lineType,
316 | })
317 | );
318 | },
319 | animateElements(newData) {
320 | [this.oldData, newData] = equilizeNoOfElements(
321 | this.oldData,
322 | newData
323 | );
324 |
325 | let newPos = newData.map((d) => d.position);
326 | let newLabels = newData.map((d) => d.label);
327 | let newOptions = newData.map((d) => d.options);
328 |
329 | let oldPos = this.oldData.map((d) => d.position);
330 |
331 | this.render(
332 | oldPos.map((pos, i) => {
333 | return {
334 | position: oldPos[i],
335 | label: newLabels[i],
336 | options: newOptions[i],
337 | };
338 | })
339 | );
340 |
341 | return this.store.map((line, i) => {
342 | return translateHoriLine(line, newPos[i], oldPos[i]);
343 | });
344 | },
345 | },
346 |
347 | yRegions: {
348 | layerClass: "y-regions",
349 | makeElements(data) {
350 | return data.map((r) =>
351 | yRegion(r.startPos, r.endPos, this.constants.width, r.label, {
352 | labelPos: r.options.labelPos,
353 | })
354 | );
355 | },
356 | animateElements(newData) {
357 | [this.oldData, newData] = equilizeNoOfElements(
358 | this.oldData,
359 | newData
360 | );
361 |
362 | let newPos = newData.map((d) => d.endPos);
363 | let newLabels = newData.map((d) => d.label);
364 | let newStarts = newData.map((d) => d.startPos);
365 | let newOptions = newData.map((d) => d.options);
366 |
367 | let oldPos = this.oldData.map((d) => d.endPos);
368 | let oldStarts = this.oldData.map((d) => d.startPos);
369 |
370 | this.render(
371 | oldPos.map((pos, i) => {
372 | return {
373 | startPos: oldStarts[i],
374 | endPos: oldPos[i],
375 | label: newLabels[i],
376 | options: newOptions[i],
377 | };
378 | })
379 | );
380 |
381 | let animateElements = [];
382 |
383 | this.store.map((rectGroup, i) => {
384 | animateElements = animateElements.concat(
385 | animateRegion(rectGroup, newStarts[i], newPos[i], oldPos[i])
386 | );
387 | });
388 |
389 | return animateElements;
390 | },
391 | },
392 |
393 | heatDomain: {
394 | layerClass: function () {
395 | return "heat-domain domain-" + this.constants.index;
396 | },
397 | makeElements(data) {
398 | let { index, colWidth, rowHeight, squareSize, radius, xTranslate } =
399 | this.constants;
400 | let monthNameHeight = -12;
401 | let x = xTranslate,
402 | y = 0;
403 |
404 | this.serializedSubDomains = [];
405 |
406 | data.cols.map((week, weekNo) => {
407 | if (weekNo === 1) {
408 | this.labels.push(
409 | makeText(
410 | "domain-name",
411 | x,
412 | monthNameHeight,
413 | getMonthName(index, true).toUpperCase(),
414 | {
415 | fontSize: 9,
416 | }
417 | )
418 | );
419 | }
420 | week.map((day, i) => {
421 | if (day.fill) {
422 | let data = {
423 | "data-date": day.yyyyMmDd,
424 | "data-value": day.dataValue,
425 | "data-day": i,
426 | };
427 | let square = heatSquare(
428 | "day",
429 | x,
430 | y,
431 | squareSize,
432 | radius,
433 | day.fill,
434 | data
435 | );
436 | this.serializedSubDomains.push(square);
437 | }
438 | y += rowHeight;
439 | });
440 | y = 0;
441 | x += colWidth;
442 | });
443 |
444 | return this.serializedSubDomains;
445 | },
446 |
447 | animateElements(newData) {
448 | if (newData) return [];
449 | },
450 | },
451 |
452 | barGraph: {
453 | layerClass: function () {
454 | return "dataset-units dataset-bars dataset-" + this.constants.index;
455 | },
456 | makeElements(data) {
457 | let c = this.constants;
458 | this.unitType = "bar";
459 | this.units = data.yPositions.map((y, j) => {
460 | return datasetBar(
461 | data.xPositions[j],
462 | y,
463 | data.barWidth,
464 | c.color,
465 | data.labels[j],
466 | j,
467 | data.offsets[j],
468 | {
469 | zeroLine: data.zeroLine,
470 | barsWidth: data.barsWidth,
471 | minHeight: c.minHeight,
472 | }
473 | );
474 | });
475 | return this.units;
476 | },
477 | animateElements(newData) {
478 | let newXPos = newData.xPositions;
479 | let newYPos = newData.yPositions;
480 | let newOffsets = newData.offsets;
481 | let newLabels = newData.labels;
482 |
483 | let oldXPos = this.oldData.xPositions;
484 | let oldYPos = this.oldData.yPositions;
485 | let oldOffsets = this.oldData.offsets;
486 | let oldLabels = this.oldData.labels;
487 |
488 | [oldXPos, newXPos] = equilizeNoOfElements(oldXPos, newXPos);
489 | [oldYPos, newYPos] = equilizeNoOfElements(oldYPos, newYPos);
490 | [oldOffsets, newOffsets] = equilizeNoOfElements(
491 | oldOffsets,
492 | newOffsets
493 | );
494 | [oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels);
495 |
496 | this.render({
497 | xPositions: oldXPos,
498 | yPositions: oldYPos,
499 | offsets: oldOffsets,
500 | labels: newLabels,
501 |
502 | zeroLine: this.oldData.zeroLine,
503 | barsWidth: this.oldData.barsWidth,
504 | barWidth: this.oldData.barWidth,
505 | });
506 |
507 | let animateElements = [];
508 |
509 | this.store.map((bar, i) => {
510 | animateElements = animateElements.concat(
511 | animateBar(
512 | bar,
513 | newXPos[i],
514 | newYPos[i],
515 | newData.barWidth,
516 | newOffsets[i],
517 | { zeroLine: newData.zeroLine }
518 | )
519 | );
520 | });
521 |
522 | return animateElements;
523 | },
524 | },
525 |
526 | lineGraph: {
527 | layerClass: function () {
528 | return "dataset-units dataset-line dataset-" + this.constants.index;
529 | },
530 | makeElements(data) {
531 | let c = this.constants;
532 | this.unitType = "dot";
533 | this.paths = {};
534 | if (!c.hideLine) {
535 | this.paths = getPaths(
536 | data.xPositions,
537 | data.yPositions,
538 | c.color,
539 | {
540 | heatline: c.heatline,
541 | regionFill: c.regionFill,
542 | spline: c.spline,
543 | },
544 | {
545 | svgDefs: c.svgDefs,
546 | zeroLine: data.zeroLine,
547 | }
548 | );
549 | }
550 |
551 | this.units = [];
552 | if (!c.hideDots) {
553 | this.units = data.yPositions.map((y, j) => {
554 | return datasetDot(
555 | data.xPositions[j],
556 | y,
557 | data.radius,
558 | c.color,
559 | c.valuesOverPoints ? data.values[j] : "",
560 | j
561 | );
562 | });
563 | }
564 |
565 | return Object.values(this.paths).concat(this.units);
566 | },
567 | animateElements(newData) {
568 | let newXPos = newData.xPositions;
569 | let newYPos = newData.yPositions;
570 | let newValues = newData.values;
571 |
572 | let oldXPos = this.oldData.xPositions;
573 | let oldYPos = this.oldData.yPositions;
574 | let oldValues = this.oldData.values;
575 |
576 | [oldXPos, newXPos] = equilizeNoOfElements(oldXPos, newXPos);
577 | [oldYPos, newYPos] = equilizeNoOfElements(oldYPos, newYPos);
578 | [oldValues, newValues] = equilizeNoOfElements(oldValues, newValues);
579 |
580 | this.render({
581 | xPositions: oldXPos,
582 | yPositions: oldYPos,
583 | values: newValues,
584 |
585 | zeroLine: this.oldData.zeroLine,
586 | radius: this.oldData.radius,
587 | });
588 |
589 | let animateElements = [];
590 |
591 | if (Object.keys(this.paths).length) {
592 | animateElements = animateElements.concat(
593 | animatePath(
594 | this.paths,
595 | newXPos,
596 | newYPos,
597 | newData.zeroLine,
598 | this.constants.spline
599 | )
600 | );
601 | }
602 |
603 | if (this.units.length) {
604 | this.units.map((dot, i) => {
605 | animateElements = animateElements.concat(
606 | animateDot(dot, newXPos[i], newYPos[i])
607 | );
608 | });
609 | }
610 |
611 | return animateElements;
612 | },
613 | },
614 | };
615 |
616 | export function getComponent(name, constants, getData) {
617 | let keys = Object.keys(componentConfigs).filter((k) => name.includes(k));
618 | let config = componentConfigs[keys[0]];
619 | Object.assign(config, {
620 | constants: constants,
621 | getData: getData,
622 | });
623 | return new ChartComponent(config);
624 | }
625 |
--------------------------------------------------------------------------------
/src/js/objects/SvgTip.js:
--------------------------------------------------------------------------------
1 | import { $ } from "../utils/dom";
2 | import { TOOLTIP_POINTER_TRIANGLE_HEIGHT } from "../utils/constants";
3 |
4 | export default class SvgTip {
5 | constructor({ parent = null, colors = [] }) {
6 | this.parent = parent;
7 | this.colors = colors;
8 | this.titleName = "";
9 | this.titleValue = "";
10 | this.listValues = [];
11 | this.titleValueFirst = 0;
12 |
13 | this.x = 0;
14 | this.y = 0;
15 |
16 | this.top = 0;
17 | this.left = 0;
18 |
19 | this.setup();
20 | }
21 |
22 | setup() {
23 | this.makeTooltip();
24 | }
25 |
26 | refresh() {
27 | this.fill();
28 | this.calcPosition();
29 | }
30 |
31 | makeTooltip() {
32 | this.container = $.create("div", {
33 | inside: this.parent,
34 | className: "graph-svg-tip comparison",
35 | innerHTML: `
36 |
37 |
`,
38 | });
39 | this.hideTip();
40 |
41 | this.title = this.container.querySelector(".title");
42 | this.list = this.container.querySelector(".data-point-list");
43 | this.dataPointList = this.container.querySelector(".data-point-list");
44 |
45 | this.parent.addEventListener("mouseleave", () => {
46 | this.hideTip();
47 | });
48 | }
49 |
50 | fill() {
51 | let title;
52 | if (this.index) {
53 | this.container.setAttribute("data-point-index", this.index);
54 | }
55 | if (this.titleValueFirst) {
56 | title = `${this.titleValue} ${this.titleName}`;
57 | } else {
58 | title = `${this.titleName}${this.titleValue} `;
59 | }
60 |
61 | if (this.listValues.length > 4) {
62 | this.list.classList.add("tooltip-grid");
63 | } else {
64 | this.list.classList.remove("tooltip-grid");
65 | }
66 |
67 | this.title.innerHTML = title;
68 | this.dataPointList.innerHTML = "";
69 |
70 | this.listValues.map((set, i) => {
71 | const color = this.colors[i] || "black";
72 | let value =
73 | set.formatted === 0 || set.formatted ? set.formatted : set.value;
74 | let li = $.create("li", {
75 | innerHTML: `
76 |
77 |
${value === 0 || value ? value : ""}
78 |
${set.title ? set.title : ""}
79 |
`,
80 | });
81 |
82 | this.dataPointList.appendChild(li);
83 | });
84 | }
85 |
86 | calcPosition() {
87 | let width = this.container.offsetWidth;
88 |
89 | this.top =
90 | this.y - this.container.offsetHeight - TOOLTIP_POINTER_TRIANGLE_HEIGHT;
91 | this.left = this.x - width / 2;
92 | let maxLeft = this.parent.offsetWidth - width;
93 |
94 | let pointer = this.container.querySelector(".svg-pointer");
95 |
96 | if (this.left < 0) {
97 | pointer.style.left = `calc(50% - ${-1 * this.left}px)`;
98 | this.left = 0;
99 | } else if (this.left > maxLeft) {
100 | let delta = this.left - maxLeft;
101 | let pointerOffset = `calc(50% + ${delta}px)`;
102 | pointer.style.left = pointerOffset;
103 |
104 | this.left = maxLeft;
105 | } else {
106 | pointer.style.left = `50%`;
107 | }
108 | }
109 |
110 | setValues(x, y, title = {}, listValues = [], index = -1) {
111 | this.titleName = title.name;
112 | this.titleValue = title.value;
113 | this.listValues = listValues;
114 | this.x = x;
115 | this.y = y;
116 | this.titleValueFirst = title.valueFirst || 0;
117 | this.index = index;
118 | this.refresh();
119 | }
120 |
121 | hideTip() {
122 | this.container.style.top = "0px";
123 | this.container.style.left = "0px";
124 | this.container.style.opacity = "0";
125 | }
126 |
127 | showTip() {
128 | this.container.style.top = this.top + "px";
129 | this.container.style.left = this.left + "px";
130 | this.container.style.opacity = "1";
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/js/utils/animate.js:
--------------------------------------------------------------------------------
1 | import { getBarHeightAndYAttr, getSplineCurvePointsStr } from "./draw-utils";
2 |
3 | export const UNIT_ANIM_DUR = 350;
4 | export const PATH_ANIM_DUR = 350;
5 | export const MARKER_LINE_ANIM_DUR = UNIT_ANIM_DUR;
6 | export const REPLACE_ALL_NEW_DUR = 250;
7 |
8 | export const STD_EASING = "easein";
9 |
10 | export function translate(unit, oldCoord, newCoord, duration) {
11 | let old = typeof oldCoord === "string" ? oldCoord : oldCoord.join(", ");
12 | return [
13 | unit,
14 | { transform: newCoord.join(", ") },
15 | duration,
16 | STD_EASING,
17 | "translate",
18 | { transform: old },
19 | ];
20 | }
21 |
22 | export function translateVertLine(xLine, newX, oldX) {
23 | return translate(xLine, [oldX, 0], [newX, 0], MARKER_LINE_ANIM_DUR);
24 | }
25 |
26 | export function translateHoriLine(yLine, newY, oldY) {
27 | return translate(yLine, [0, oldY], [0, newY], MARKER_LINE_ANIM_DUR);
28 | }
29 |
30 | export function animateRegion(rectGroup, newY1, newY2, oldY2) {
31 | let newHeight = newY1 - newY2;
32 | let rect = rectGroup.childNodes[0];
33 | let width = rect.getAttribute("width");
34 | let rectAnim = [
35 | rect,
36 | { height: newHeight, "stroke-dasharray": `${width}, ${newHeight}` },
37 | MARKER_LINE_ANIM_DUR,
38 | STD_EASING,
39 | ];
40 |
41 | let groupAnim = translate(
42 | rectGroup,
43 | [0, oldY2],
44 | [0, newY2],
45 | MARKER_LINE_ANIM_DUR
46 | );
47 | return [rectAnim, groupAnim];
48 | }
49 |
50 | export function animateBar(bar, x, yTop, width, offset = 0, meta = {}) {
51 | let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine);
52 | y -= offset;
53 | if (bar.nodeName !== "rect") {
54 | let rect = bar.childNodes[0];
55 | let rectAnim = [
56 | rect,
57 | { width: width, height: height },
58 | UNIT_ANIM_DUR,
59 | STD_EASING,
60 | ];
61 |
62 | let oldCoordStr = bar.getAttribute("transform").split("(")[1].slice(0, -1);
63 | let groupAnim = translate(bar, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR);
64 | return [rectAnim, groupAnim];
65 | } else {
66 | return [
67 | [
68 | bar,
69 | { width: width, height: height, x: x, y: y },
70 | UNIT_ANIM_DUR,
71 | STD_EASING,
72 | ],
73 | ];
74 | }
75 | // bar.animate({height: args.newHeight, y: yTop}, UNIT_ANIM_DUR, mina.easein);
76 | }
77 |
78 | export function animateDot(dot, x, y) {
79 | if (dot.nodeName !== "circle") {
80 | let oldCoordStr = dot.getAttribute("transform").split("(")[1].slice(0, -1);
81 | let groupAnim = translate(dot, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR);
82 | return [groupAnim];
83 | } else {
84 | return [[dot, { cx: x, cy: y }, UNIT_ANIM_DUR, STD_EASING]];
85 | }
86 | // dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein);
87 | }
88 |
89 | export function animatePath(paths, newXList, newYList, zeroLine, spline) {
90 | let pathComponents = [];
91 | let pointsStr = newYList.map((y, i) => newXList[i] + "," + y).join("L");
92 |
93 | if (spline) pointsStr = getSplineCurvePointsStr(newXList, newYList);
94 |
95 | const animPath = [
96 | paths.path,
97 | { d: "M" + pointsStr },
98 | PATH_ANIM_DUR,
99 | STD_EASING,
100 | ];
101 | pathComponents.push(animPath);
102 |
103 | if (paths.region) {
104 | let regStartPt = `${newXList[0]},${zeroLine}L`;
105 | let regEndPt = `L${newXList.slice(-1)[0]}, ${zeroLine}`;
106 |
107 | const animRegion = [
108 | paths.region,
109 | { d: "M" + regStartPt + pointsStr + regEndPt },
110 | PATH_ANIM_DUR,
111 | STD_EASING,
112 | ];
113 | pathComponents.push(animRegion);
114 | }
115 |
116 | return pathComponents;
117 | }
118 |
119 | export function animatePathStr(oldPath, pathStr) {
120 | return [oldPath, { d: pathStr }, UNIT_ANIM_DUR, STD_EASING];
121 | }
122 |
--------------------------------------------------------------------------------
/src/js/utils/animation.js:
--------------------------------------------------------------------------------
1 | // Leveraging SMIL Animations
2 |
3 | import { REPLACE_ALL_NEW_DUR } from "./animate";
4 |
5 | const EASING = {
6 | ease: "0.25 0.1 0.25 1",
7 | linear: "0 0 1 1",
8 | // easein: "0.42 0 1 1",
9 | easein: "0.1 0.8 0.2 1",
10 | easeout: "0 0 0.58 1",
11 | easeinout: "0.42 0 0.58 1",
12 | };
13 |
14 | function animateSVGElement(
15 | element,
16 | props,
17 | dur,
18 | easingType = "linear",
19 | type = undefined,
20 | oldValues = {}
21 | ) {
22 | let animElement = element.cloneNode(true);
23 | let newElement = element.cloneNode(true);
24 |
25 | for (var attributeName in props) {
26 | let animateElement;
27 | if (attributeName === "transform") {
28 | animateElement = document.createElementNS(
29 | "http://www.w3.org/2000/svg",
30 | "animateTransform"
31 | );
32 | } else {
33 | animateElement = document.createElementNS(
34 | "http://www.w3.org/2000/svg",
35 | "animate"
36 | );
37 | }
38 | let currentValue =
39 | oldValues[attributeName] || element.getAttribute(attributeName);
40 | let value = props[attributeName];
41 |
42 | let animAttr = {
43 | attributeName: attributeName,
44 | from: currentValue,
45 | to: value,
46 | begin: "0s",
47 | dur: dur / 1000 + "s",
48 | values: currentValue + ";" + value,
49 | keySplines: EASING[easingType],
50 | keyTimes: "0;1",
51 | calcMode: "spline",
52 | fill: "freeze",
53 | };
54 |
55 | if (type) {
56 | animAttr["type"] = type;
57 | }
58 |
59 | for (var i in animAttr) {
60 | animateElement.setAttribute(i, animAttr[i]);
61 | }
62 |
63 | animElement.appendChild(animateElement);
64 |
65 | if (type) {
66 | newElement.setAttribute(attributeName, `translate(${value})`);
67 | } else {
68 | newElement.setAttribute(attributeName, value);
69 | }
70 | }
71 |
72 | return [animElement, newElement];
73 | }
74 |
75 | export function transform(element, style) {
76 | // eslint-disable-line no-unused-vars
77 | element.style.transform = style;
78 | element.style.webkitTransform = style;
79 | element.style.msTransform = style;
80 | element.style.mozTransform = style;
81 | element.style.oTransform = style;
82 | }
83 |
84 | function animateSVG(svgContainer, elements) {
85 | let newElements = [];
86 | let animElements = [];
87 |
88 | elements.map((element) => {
89 | let unit = element[0];
90 | let parent = unit.parentNode;
91 |
92 | let animElement, newElement;
93 |
94 | element[0] = unit;
95 | [animElement, newElement] = animateSVGElement(...element);
96 |
97 | newElements.push(newElement);
98 | animElements.push([animElement, parent]);
99 |
100 | if (parent) {
101 | parent.replaceChild(animElement, unit);
102 | }
103 | });
104 |
105 | let animSvg = svgContainer.cloneNode(true);
106 |
107 | animElements.map((animElement, i) => {
108 | if (animElement[1]) {
109 | animElement[1].replaceChild(newElements[i], animElement[0]);
110 | elements[i][0] = newElements[i];
111 | }
112 | });
113 |
114 | return animSvg;
115 | }
116 |
117 | export function runSMILAnimation(parent, svgElement, elementsToAnimate) {
118 | if (elementsToAnimate.length === 0) return;
119 |
120 | let animSvgElement = animateSVG(svgElement, elementsToAnimate);
121 | if (svgElement.parentNode == parent) {
122 | parent.removeChild(svgElement);
123 | parent.appendChild(animSvgElement);
124 | }
125 |
126 | // Replace the new svgElement (data has already been replaced)
127 | setTimeout(() => {
128 | if (animSvgElement.parentNode == parent) {
129 | parent.removeChild(animSvgElement);
130 | parent.appendChild(svgElement);
131 | }
132 | }, REPLACE_ALL_NEW_DUR);
133 | }
134 |
--------------------------------------------------------------------------------
/src/js/utils/axis-chart-utils.js:
--------------------------------------------------------------------------------
1 | import { fillArray } from "../utils/helpers";
2 | import {
3 | DEFAULT_AXIS_CHART_TYPE,
4 | AXIS_DATASET_CHART_TYPES,
5 | DEFAULT_CHAR_WIDTH,
6 | SERIES_LABEL_SPACE_RATIO,
7 | } from "../utils/constants";
8 |
9 | export function dataPrep(data, type, config) {
10 | data.labels = data.labels || [];
11 |
12 | let datasetLength = data.labels.length;
13 |
14 | // Datasets
15 | let datasets = data.datasets;
16 | let zeroArray = new Array(datasetLength).fill(0);
17 | if (!datasets) {
18 | // default
19 | datasets = [
20 | {
21 | values: zeroArray,
22 | },
23 | ];
24 | }
25 |
26 | datasets.map((d) => {
27 | // Set values
28 | if (!d.values) {
29 | d.values = zeroArray;
30 | } else {
31 | // Check for non values
32 | let vals = d.values;
33 | vals = vals.map((val) => (!isNaN(val) ? val : 0));
34 |
35 | // Trim or extend
36 | if (vals.length > datasetLength) {
37 | vals = vals.slice(0, datasetLength);
38 | }
39 | if (config) {
40 | vals = fillArray(vals, datasetLength - vals.length, null);
41 | } else {
42 | vals = fillArray(vals, datasetLength - vals.length, 0);
43 | }
44 | d.values = vals;
45 | }
46 |
47 | // Set type
48 | if (!d.chartType) {
49 | if (!AXIS_DATASET_CHART_TYPES.includes(type))
50 | type = DEFAULT_AXIS_CHART_TYPE;
51 | d.chartType = type;
52 | }
53 | });
54 |
55 | // Markers
56 |
57 | // Regions
58 | // data.yRegions = data.yRegions || [];
59 | if (data.yRegions) {
60 | data.yRegions.map((d) => {
61 | if (d.end < d.start) {
62 | [d.start, d.end] = [d.end, d.start];
63 | }
64 | });
65 | }
66 |
67 | return data;
68 | }
69 |
70 | export function zeroDataPrep(realData) {
71 | let datasetLength = realData.labels.length;
72 | let zeroArray = new Array(datasetLength).fill(0);
73 |
74 | let zeroData = {
75 | labels: realData.labels.slice(0, -1),
76 | datasets: realData.datasets.map((d) => {
77 | const { axisID } = d;
78 | return {
79 | axisID,
80 | name: "",
81 | values: zeroArray.slice(0, -1),
82 | chartType: d.chartType,
83 | };
84 | }),
85 | };
86 |
87 | if (realData.yMarkers) {
88 | zeroData.yMarkers = [
89 | {
90 | value: 0,
91 | label: "",
92 | },
93 | ];
94 | }
95 |
96 | if (realData.yRegions) {
97 | zeroData.yRegions = [
98 | {
99 | start: 0,
100 | end: 0,
101 | label: "",
102 | },
103 | ];
104 | }
105 |
106 | return zeroData;
107 | }
108 |
109 | export function getShortenedLabels(chartWidth, labels = [], isSeries = true) {
110 | let allowedSpace = (chartWidth / labels.length) * SERIES_LABEL_SPACE_RATIO;
111 | if (allowedSpace <= 0) allowedSpace = 1;
112 | let allowedLetters = allowedSpace / DEFAULT_CHAR_WIDTH;
113 |
114 | let seriesMultiple;
115 | if (isSeries) {
116 | // Find the maximum label length for spacing calculations
117 | let maxLabelLength = Math.max(...labels.map((label) => label.length));
118 | seriesMultiple = Math.ceil(maxLabelLength / allowedLetters);
119 | }
120 |
121 | let calcLabels = labels.map((label, i) => {
122 | label += "";
123 | if (label.length > allowedLetters) {
124 | if (!isSeries) {
125 | if (allowedLetters - 3 > 0) {
126 | label = label.slice(0, allowedLetters - 3) + " ...";
127 | } else {
128 | label = label.slice(0, allowedLetters) + "..";
129 | }
130 | } else {
131 | if (i % seriesMultiple !== 0 && i !== labels.length - 1) {
132 | label = "";
133 | }
134 | }
135 | }
136 | return label;
137 | });
138 |
139 | return calcLabels;
140 | }
141 |
--------------------------------------------------------------------------------
/src/js/utils/colors.js:
--------------------------------------------------------------------------------
1 | const PRESET_COLOR_MAP = {
2 | pink: "#F683AE",
3 | blue: "#318AD8",
4 | green: "#48BB74",
5 | grey: "#A6B1B9",
6 | red: "#F56B6B",
7 | yellow: "#FACF7A",
8 | purple: "#44427B",
9 | teal: "#5FD8C4",
10 | cyan: "#15CCEF",
11 | orange: "#F8814F",
12 | "light-pink": "#FED7E5",
13 | "light-blue": "#BFDDF7",
14 | "light-green": "#48BB74",
15 | "light-grey": "#F4F5F6",
16 | "light-red": "#F6DFDF",
17 | "light-yellow": "#FEE9BF",
18 | "light-purple": "#E8E8F7",
19 | "light-teal": "#D3FDF6",
20 | "light-cyan": "#DDF8FD",
21 | "light-orange": "#FECDB8",
22 | };
23 |
24 | function limitColor(r) {
25 | if (r > 255) return 255;
26 | else if (r < 0) return 0;
27 | return r;
28 | }
29 |
30 | export function lightenDarkenColor(color, amt) {
31 | let col = getColor(color);
32 | let usePound = false;
33 | if (col[0] == "#") {
34 | col = col.slice(1);
35 | usePound = true;
36 | }
37 | let num = parseInt(col, 16);
38 | let r = limitColor((num >> 16) + amt);
39 | let b = limitColor(((num >> 8) & 0x00ff) + amt);
40 | let g = limitColor((num & 0x0000ff) + amt);
41 | return (usePound ? "#" : "") + (g | (b << 8) | (r << 16)).toString(16);
42 | }
43 |
44 | export function isValidColor(string) {
45 | // https://stackoverflow.com/a/32685393
46 | let HEX_RE = /(^\s*)(#)((?:[A-Fa-f0-9]{3}){1,2})$/i;
47 | let RGB_RE =
48 | /(^\s*)(rgb|hsl)(a?)[(]\s*([\d.]+\s*%?)\s*,\s*([\d.]+\s*%?)\s*,\s*([\d.]+\s*%?)\s*(?:,\s*([\d.]+)\s*)?[)]$/i;
49 | return HEX_RE.test(string) || RGB_RE.test(string);
50 | }
51 |
52 | export const getColor = (color) => {
53 | // When RGB color, convert to hexadecimal (alpha value is omitted)
54 | if (/rgb[a]{0,1}\([\d, ]+\)/gim.test(color)) {
55 | return /\D+(\d*)\D+(\d*)\D+(\d*)/gim
56 | .exec(color)
57 | .map((x, i) => (i !== 0 ? Number(x).toString(16) : "#"))
58 | .reduce((c, ch) => `${c}${ch}`);
59 | }
60 | return PRESET_COLOR_MAP[color] || color;
61 | };
62 |
--------------------------------------------------------------------------------
/src/js/utils/constants.js:
--------------------------------------------------------------------------------
1 | export const ALL_CHART_TYPES = [
2 | "line",
3 | "scatter",
4 | "bar",
5 | "percentage",
6 | "heatmap",
7 | "pie",
8 | ];
9 |
10 | export const COMPATIBLE_CHARTS = {
11 | bar: ["line", "scatter", "percentage", "pie"],
12 | line: ["scatter", "bar", "percentage", "pie"],
13 | pie: ["line", "scatter", "percentage", "bar"],
14 | percentage: ["bar", "line", "scatter", "pie"],
15 | heatmap: [],
16 | };
17 |
18 | export const DATA_COLOR_DIVISIONS = {
19 | bar: "datasets",
20 | line: "datasets",
21 | pie: "labels",
22 | percentage: "labels",
23 | heatmap: HEATMAP_DISTRIBUTION_SIZE,
24 | };
25 |
26 | export const BASE_MEASURES = {
27 | margins: {
28 | top: 10,
29 | bottom: 10,
30 | left: 20,
31 | right: 20,
32 | },
33 | paddings: {
34 | top: 20,
35 | bottom: 40,
36 | left: 30,
37 | right: 10,
38 | },
39 |
40 | baseHeight: 240,
41 | titleHeight: 20,
42 | legendHeight: 30,
43 |
44 | titleFontSize: 12,
45 | };
46 |
47 | export function getTopOffset(m) {
48 | return m.titleHeight + m.margins.top + m.paddings.top;
49 | }
50 |
51 | export function getLeftOffset(m) {
52 | return m.margins.left + m.paddings.left;
53 | }
54 |
55 | export function getExtraHeight(m) {
56 | let totalExtraHeight =
57 | m.margins.top +
58 | m.margins.bottom +
59 | m.paddings.top +
60 | m.paddings.bottom +
61 | m.titleHeight +
62 | m.legendHeight;
63 | return totalExtraHeight;
64 | }
65 |
66 | export function getExtraWidth(m) {
67 | let totalExtraWidth =
68 | m.margins.left + m.margins.right + m.paddings.left + m.paddings.right;
69 |
70 | return totalExtraWidth;
71 | }
72 |
73 | export const INIT_CHART_UPDATE_TIMEOUT = 700;
74 | export const CHART_POST_ANIMATE_TIMEOUT = 400;
75 |
76 | export const DEFAULT_AXIS_CHART_TYPE = "line";
77 | export const AXIS_DATASET_CHART_TYPES = ["line", "bar"];
78 |
79 | export const LEGEND_ITEM_WIDTH = 150;
80 | export const SERIES_LABEL_SPACE_RATIO = 0.6;
81 |
82 | export const BAR_CHART_SPACE_RATIO = 0.5;
83 | export const MIN_BAR_PERCENT_HEIGHT = 0.0;
84 |
85 | export const LINE_CHART_DOT_SIZE = 4;
86 | export const DOT_OVERLAY_SIZE_INCR = 4;
87 |
88 | export const PERCENTAGE_BAR_DEFAULT_HEIGHT = 16;
89 |
90 | // Fixed 5-color theme,
91 | // More colors are difficult to parse visually
92 | export const HEATMAP_DISTRIBUTION_SIZE = 5;
93 |
94 | export const HEATMAP_SQUARE_SIZE = 10;
95 | export const HEATMAP_GUTTER_SIZE = 2;
96 |
97 | export const DEFAULT_CHAR_WIDTH = 7;
98 |
99 | export const TOOLTIP_POINTER_TRIANGLE_HEIGHT = 7.48;
100 | const DEFAULT_CHART_COLORS = [
101 | "pink",
102 | "blue",
103 | "green",
104 | "grey",
105 | "red",
106 | "yellow",
107 | "purple",
108 | "teal",
109 | "cyan",
110 | "orange",
111 | ];
112 | const HEATMAP_COLORS_GREEN = [
113 | "#ebedf0",
114 | "#c6e48b",
115 | "#7bc96f",
116 | "#239a3b",
117 | "#196127",
118 | ];
119 | export const HEATMAP_COLORS_BLUE = [
120 | "#ebedf0",
121 | "#c0ddf9",
122 | "#73b3f3",
123 | "#3886e1",
124 | "#17459e",
125 | ];
126 | export const HEATMAP_COLORS_YELLOW = [
127 | "#ebedf0",
128 | "#fdf436",
129 | "#ffc700",
130 | "#ff9100",
131 | "#06001c",
132 | ];
133 |
134 | export const DEFAULT_COLORS = {
135 | bar: DEFAULT_CHART_COLORS,
136 | line: DEFAULT_CHART_COLORS,
137 | pie: DEFAULT_CHART_COLORS,
138 | percentage: DEFAULT_CHART_COLORS,
139 | heatmap: HEATMAP_COLORS_GREEN,
140 | donut: DEFAULT_CHART_COLORS,
141 | };
142 |
143 | // Universal constants
144 | export const ANGLE_RATIO = Math.PI / 180;
145 | export const FULL_ANGLE = 360;
146 |
--------------------------------------------------------------------------------
/src/js/utils/date-utils.js:
--------------------------------------------------------------------------------
1 | // Playing around with dates
2 |
3 | export const NO_OF_YEAR_MONTHS = 12;
4 | export const NO_OF_DAYS_IN_WEEK = 7;
5 | export const DAYS_IN_YEAR = 375;
6 | export const NO_OF_MILLIS = 1000;
7 | export const SEC_IN_DAY = 86400;
8 |
9 | export const MONTH_NAMES = [
10 | "January",
11 | "February",
12 | "March",
13 | "April",
14 | "May",
15 | "June",
16 | "July",
17 | "August",
18 | "September",
19 | "October",
20 | "November",
21 | "December",
22 | ];
23 | export const MONTH_NAMES_SHORT = [
24 | "Jan",
25 | "Feb",
26 | "Mar",
27 | "Apr",
28 | "May",
29 | "Jun",
30 | "Jul",
31 | "Aug",
32 | "Sep",
33 | "Oct",
34 | "Nov",
35 | "Dec",
36 | ];
37 |
38 | export const DAY_NAMES_SHORT = [
39 | "Sun",
40 | "Mon",
41 | "Tue",
42 | "Wed",
43 | "Thu",
44 | "Fri",
45 | "Sat",
46 | ];
47 | export const DAY_NAMES = [
48 | "Sunday",
49 | "Monday",
50 | "Tuesday",
51 | "Wednesday",
52 | "Thursday",
53 | "Friday",
54 | "Saturday",
55 | ];
56 |
57 | // https://stackoverflow.com/a/11252167/6495043
58 | function treatAsUtc(date) {
59 | let result = new Date(date);
60 | result.setMinutes(result.getMinutes() - result.getTimezoneOffset());
61 | return result;
62 | }
63 |
64 | export function toMidnightUTC(date) {
65 | let result = new Date(date);
66 | result.setUTCHours(0, result.getTimezoneOffset(), 0, 0);
67 | return result;
68 | }
69 |
70 | export function getYyyyMmDd(date) {
71 | let dd = date.getDate();
72 | let mm = date.getMonth() + 1; // getMonth() is zero-based
73 | return [
74 | date.getFullYear(),
75 | (mm > 9 ? "" : "0") + mm,
76 | (dd > 9 ? "" : "0") + dd,
77 | ].join("-");
78 | }
79 |
80 | export function clone(date) {
81 | return new Date(date.getTime());
82 | }
83 |
84 | export function timestampSec(date) {
85 | return date.getTime() / NO_OF_MILLIS;
86 | }
87 |
88 | export function timestampToMidnight(timestamp, roundAhead = false) {
89 | let midnightTs = Math.floor(timestamp - (timestamp % SEC_IN_DAY));
90 | if (roundAhead) {
91 | return midnightTs + SEC_IN_DAY;
92 | }
93 | return midnightTs;
94 | }
95 |
96 | // export function getMonthsBetween(startDate, endDate) {}
97 |
98 | export function getWeeksBetween(startDate, endDate) {
99 | let weekStartDate = setDayToSunday(startDate);
100 | return Math.ceil(getDaysBetween(weekStartDate, endDate) / NO_OF_DAYS_IN_WEEK);
101 | }
102 |
103 | export function getDaysBetween(startDate, endDate) {
104 | let millisecondsPerDay = SEC_IN_DAY * NO_OF_MILLIS;
105 | return (treatAsUtc(endDate) - treatAsUtc(startDate)) / millisecondsPerDay;
106 | }
107 |
108 | export function areInSameMonth(startDate, endDate) {
109 | return (
110 | startDate.getMonth() === endDate.getMonth() &&
111 | startDate.getFullYear() === endDate.getFullYear()
112 | );
113 | }
114 |
115 | export function getMonthName(i, short = false) {
116 | let monthName = MONTH_NAMES[i];
117 | return short ? monthName.slice(0, 3) : monthName;
118 | }
119 |
120 | export function getLastDateInMonth(month, year) {
121 | return new Date(year, month + 1, 0); // 0: last day in previous month
122 | }
123 |
124 | // mutates
125 | export function setDayToSunday(date) {
126 | let newDate = clone(date);
127 | const day = newDate.getDay();
128 | if (day !== 0) {
129 | addDays(newDate, -1 * day);
130 | }
131 | return newDate;
132 | }
133 |
134 | // mutates
135 | export function addDays(date, numberOfDays) {
136 | date.setDate(date.getDate() + numberOfDays);
137 | }
138 |
--------------------------------------------------------------------------------
/src/js/utils/dom.js:
--------------------------------------------------------------------------------
1 | export function $(expr, con) {
2 | return typeof expr === "string"
3 | ? (con || document).querySelector(expr)
4 | : expr || null;
5 | }
6 |
7 | export function findNodeIndex(node) {
8 | var i = 0;
9 | while (node.previousSibling) {
10 | node = node.previousSibling;
11 | i++;
12 | }
13 | return i;
14 | }
15 |
16 | $.create = (tag, o) => {
17 | var element = document.createElement(tag);
18 |
19 | for (var i in o) {
20 | var val = o[i];
21 |
22 | if (i === "inside") {
23 | $(val).appendChild(element);
24 | } else if (i === "around") {
25 | var ref = $(val);
26 | ref.parentNode.insertBefore(element, ref);
27 | element.appendChild(ref);
28 | } else if (i === "styles") {
29 | if (typeof val === "object") {
30 | Object.keys(val).map((prop) => {
31 | element.style[prop] = val[prop];
32 | });
33 | }
34 | } else if (i in element) {
35 | element[i] = val;
36 | } else {
37 | element.setAttribute(i, val);
38 | }
39 | }
40 |
41 | return element;
42 | };
43 |
44 | export function getOffset(element) {
45 | let rect = element.getBoundingClientRect();
46 | return {
47 | // https://stackoverflow.com/a/7436602/6495043
48 | // rect.top varies with scroll, so we add whatever has been
49 | // scrolled to it to get absolute distance from actual page top
50 | top:
51 | rect.top +
52 | (document.documentElement.scrollTop || document.body.scrollTop),
53 | left:
54 | rect.left +
55 | (document.documentElement.scrollLeft || document.body.scrollLeft),
56 | };
57 | }
58 |
59 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
60 | // an element's offsetParent property will return null whenever it, or any of its parents,
61 | // is hidden via the display style property.
62 | export function isHidden(el) {
63 | return el.offsetParent === null;
64 | }
65 |
66 | export function isElementInViewport(el) {
67 | // Although straightforward: https://stackoverflow.com/a/7557433/6495043
68 | var rect = el.getBoundingClientRect();
69 |
70 | return (
71 | rect.top >= 0 &&
72 | rect.left >= 0 &&
73 | rect.bottom <=
74 | (window.innerHeight ||
75 | document.documentElement.clientHeight) /*or $(window).height() */ &&
76 | rect.right <=
77 | (window.innerWidth ||
78 | document.documentElement.clientWidth) /*or $(window).width() */
79 | );
80 | }
81 |
82 | export function getElementContentWidth(element) {
83 | var styles = window.getComputedStyle(element);
84 | var padding =
85 | parseFloat(styles.paddingLeft) + parseFloat(styles.paddingRight);
86 |
87 | return element.clientWidth - padding;
88 | }
89 |
90 | export function bind(element, o) {
91 | if (element) {
92 | for (var event in o) {
93 | var callback = o[event];
94 |
95 | event.split(/\s+/).forEach(function (event) {
96 | element.addEventListener(event, callback);
97 | });
98 | }
99 | }
100 | }
101 |
102 | export function unbind(element, o) {
103 | if (element) {
104 | for (var event in o) {
105 | var callback = o[event];
106 |
107 | event.split(/\s+/).forEach(function (event) {
108 | element.removeEventListener(event, callback);
109 | });
110 | }
111 | }
112 | }
113 |
114 | export function fire(target, type, properties) {
115 | var evt = document.createEvent("HTMLEvents");
116 |
117 | evt.initEvent(type, true, true);
118 |
119 | for (var j in properties) {
120 | evt[j] = properties[j];
121 | }
122 |
123 | return target.dispatchEvent(evt);
124 | }
125 |
126 | // https://css-tricks.com/snippets/javascript/loop-queryselectorall-matches/
127 | export function forEachNode(nodeList, callback, scope) {
128 | if (!nodeList) return;
129 | for (var i = 0; i < nodeList.length; i++) {
130 | callback.call(scope, nodeList[i], i);
131 | }
132 | }
133 |
134 | export function activate(
135 | $parent,
136 | $child,
137 | commonClass,
138 | activeClass = "active",
139 | index = -1
140 | ) {
141 | let $children = $parent.querySelectorAll(`.${commonClass}.${activeClass}`);
142 |
143 | forEachNode($children, (node, i) => {
144 | if (index >= 0 && i <= index) return;
145 | node.classList.remove(activeClass);
146 | });
147 |
148 | $child.classList.add(activeClass);
149 | }
150 |
--------------------------------------------------------------------------------
/src/js/utils/draw-utils.js:
--------------------------------------------------------------------------------
1 | import { fillArray } from "./helpers";
2 |
3 | export function getBarHeightAndYAttr(yTop, zeroLine) {
4 | let height, y;
5 | if (yTop <= zeroLine) {
6 | height = zeroLine - yTop;
7 | y = yTop;
8 | } else {
9 | height = yTop - zeroLine;
10 | y = zeroLine;
11 | }
12 |
13 | return [height, y];
14 | }
15 |
16 | export function equilizeNoOfElements(
17 | array1,
18 | array2,
19 | extraCount = array2.length - array1.length
20 | ) {
21 | // Doesn't work if either has zero elements.
22 | if (extraCount > 0) {
23 | array1 = fillArray(array1, extraCount);
24 | } else {
25 | array2 = fillArray(array2, extraCount);
26 | }
27 | return [array1, array2];
28 | }
29 |
30 | export function truncateString(txt, len) {
31 | if (!txt) {
32 | return;
33 | }
34 | if (txt.length > len) {
35 | return txt.slice(0, len - 3) + "...";
36 | } else {
37 | return txt;
38 | }
39 | }
40 |
41 | export function shortenLargeNumber(label) {
42 | let number;
43 | if (typeof label === "number") number = label;
44 | else if (typeof label === "string") {
45 | number = Number(label);
46 | if (Number.isNaN(number)) return label;
47 | }
48 |
49 | // Using absolute since log wont work for negative numbers
50 | let p = Math.floor(Math.log10(Math.abs(number)));
51 | if (p <= 2) return number; // Return as is for a 3 digit number of less
52 | let l = Math.floor(p / 3);
53 | let shortened =
54 | Math.pow(10, p - l * 3) * +(number / Math.pow(10, p)).toFixed(1);
55 |
56 | // Correct for floating point error upto 2 decimal places
57 | return Math.round(shortened * 100) / 100 + " " + ["", "K", "M", "B", "T"][l];
58 | }
59 |
60 | // cubic bezier curve calculation (from example by François Romain)
61 | export function getSplineCurvePointsStr(xList, yList) {
62 | let points = [];
63 | for (let i = 0; i < xList.length; i++) {
64 | points.push([xList[i], yList[i]]);
65 | }
66 |
67 | let smoothing = 0.2;
68 | let line = (pointA, pointB) => {
69 | let lengthX = pointB[0] - pointA[0];
70 | let lengthY = pointB[1] - pointA[1];
71 | return {
72 | length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),
73 | angle: Math.atan2(lengthY, lengthX),
74 | };
75 | };
76 |
77 | let controlPoint = (current, previous, next, reverse) => {
78 | let p = previous || current;
79 | let n = next || current;
80 | let o = line(p, n);
81 | let angle = o.angle + (reverse ? Math.PI : 0);
82 | let length = o.length * smoothing;
83 | let x = current[0] + Math.cos(angle) * length;
84 | let y = current[1] + Math.sin(angle) * length;
85 | return [x, y];
86 | };
87 |
88 | let bezierCommand = (point, i, a) => {
89 | let cps = controlPoint(a[i - 1], a[i - 2], point);
90 | let cpe = controlPoint(point, a[i - 1], a[i + 1], true);
91 | return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}`;
92 | };
93 |
94 | let pointStr = (points, command) => {
95 | return points.reduce(
96 | (acc, point, i, a) =>
97 | i === 0 ? `${point[0]},${point[1]}` : `${acc} ${command(point, i, a)}`,
98 | ""
99 | );
100 | };
101 |
102 | return pointStr(points, bezierCommand);
103 | }
104 |
--------------------------------------------------------------------------------
/src/js/utils/draw.js:
--------------------------------------------------------------------------------
1 | import {
2 | getBarHeightAndYAttr,
3 | truncateString,
4 | shortenLargeNumber,
5 | getSplineCurvePointsStr,
6 | } from "./draw-utils";
7 | import { getStringWidth, isValidNumber, round } from "./helpers";
8 |
9 | import {
10 | DOT_OVERLAY_SIZE_INCR,
11 | } from "./constants";
12 |
13 | export const AXIS_TICK_LENGTH = 6;
14 | const LABEL_MARGIN = 4;
15 | const LABEL_WIDTH = 25;
16 | const TOTAL_PADDING = 120;
17 | const LABEL_MAX_CHARS = 18;
18 | export const FONT_SIZE = 10;
19 | const BASE_LINE_COLOR = "#E2E6E9";
20 |
21 | function $(expr, con) {
22 | return typeof expr === "string"
23 | ? (con || document).querySelector(expr)
24 | : expr || null;
25 | }
26 |
27 | export function createSVG(tag, o) {
28 | var element = document.createElementNS("http://www.w3.org/2000/svg", tag);
29 |
30 | for (var i in o) {
31 | var val = o[i];
32 |
33 | if (i === "inside") {
34 | $(val).appendChild(element);
35 | } else if (i === "around") {
36 | var ref = $(val);
37 | ref.parentNode.insertBefore(element, ref);
38 | element.appendChild(ref);
39 | } else if (i === "styles") {
40 | if (typeof val === "object") {
41 | Object.keys(val).map((prop) => {
42 | element.style[prop] = val[prop];
43 | });
44 | }
45 | } else {
46 | if (i === "className") {
47 | i = "class";
48 | }
49 | if (i === "innerHTML") {
50 | element["textContent"] = val;
51 | } else {
52 | element.setAttribute(i, val);
53 | }
54 | }
55 | }
56 |
57 | return element;
58 | }
59 |
60 | function renderVerticalGradient(svgDefElem, gradientId) {
61 | return createSVG("linearGradient", {
62 | inside: svgDefElem,
63 | id: gradientId,
64 | x1: 0,
65 | x2: 0,
66 | y1: 0,
67 | y2: 1,
68 | });
69 | }
70 |
71 | function setGradientStop(gradElem, offset, color, opacity) {
72 | return createSVG("stop", {
73 | inside: gradElem,
74 | style: `stop-color: ${color}`,
75 | offset: offset,
76 | "stop-opacity": opacity,
77 | });
78 | }
79 |
80 | export function makeSVGContainer(parent, className, width, height) {
81 | return createSVG("svg", {
82 | className: className,
83 | inside: parent,
84 | width: width,
85 | height: height,
86 | });
87 | }
88 |
89 | export function makeSVGDefs(svgContainer) {
90 | return createSVG("defs", {
91 | inside: svgContainer,
92 | });
93 | }
94 |
95 | export function makeSVGGroup(className, transform = "", parent = undefined) {
96 | let args = {
97 | className: className,
98 | transform: transform,
99 | };
100 | if (parent) args.inside = parent;
101 | return createSVG("g", args);
102 | }
103 |
104 | export function wrapInSVGGroup(elements, className = "") {
105 | let g = createSVG("g", {
106 | className: className,
107 | });
108 | elements.forEach((e) => g.appendChild(e));
109 | return g;
110 | }
111 |
112 | export function makePath(
113 | pathStr,
114 | className = "",
115 | stroke = "none",
116 | fill = "none",
117 | strokeWidth = 2
118 | ) {
119 | return createSVG("path", {
120 | className: className,
121 | d: pathStr,
122 | styles: {
123 | stroke: stroke,
124 | fill: fill,
125 | "stroke-width": strokeWidth,
126 | },
127 | });
128 | }
129 |
130 | export function makeArcPathStr(
131 | startPosition,
132 | endPosition,
133 | center,
134 | radius,
135 | clockWise = 1,
136 | largeArc = 0
137 | ) {
138 | let [arcStartX, arcStartY] = [
139 | center.x + startPosition.x,
140 | center.y + startPosition.y,
141 | ];
142 | let [arcEndX, arcEndY] = [
143 | center.x + endPosition.x,
144 | center.y + endPosition.y,
145 | ];
146 | return `M${center.x} ${center.y}
147 | L${arcStartX} ${arcStartY}
148 | A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
149 | ${arcEndX} ${arcEndY} z`;
150 | }
151 |
152 | export function makeCircleStr(
153 | startPosition,
154 | endPosition,
155 | center,
156 | radius,
157 | clockWise = 1,
158 | largeArc = 0
159 | ) {
160 | let [arcStartX, arcStartY] = [
161 | center.x + startPosition.x,
162 | center.y + startPosition.y,
163 | ];
164 | let [arcEndX, midArc, arcEndY] = [
165 | center.x + endPosition.x,
166 | center.y * 2,
167 | center.y + endPosition.y,
168 | ];
169 | return `M${center.x} ${center.y}
170 | L${arcStartX} ${arcStartY}
171 | A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
172 | ${arcEndX} ${midArc} z
173 | L${arcStartX} ${midArc}
174 | A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
175 | ${arcEndX} ${arcEndY} z`;
176 | }
177 |
178 | export function makeArcStrokePathStr(
179 | startPosition,
180 | endPosition,
181 | center,
182 | radius,
183 | clockWise = 1,
184 | largeArc = 0
185 | ) {
186 | let [arcStartX, arcStartY] = [
187 | center.x + startPosition.x,
188 | center.y + startPosition.y,
189 | ];
190 | let [arcEndX, arcEndY] = [
191 | center.x + endPosition.x,
192 | center.y + endPosition.y,
193 | ];
194 |
195 | return `M${arcStartX} ${arcStartY}
196 | A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
197 | ${arcEndX} ${arcEndY}`;
198 | }
199 |
200 | export function makeStrokeCircleStr(
201 | startPosition,
202 | endPosition,
203 | center,
204 | radius,
205 | clockWise = 1,
206 | largeArc = 0
207 | ) {
208 | let [arcStartX, arcStartY] = [
209 | center.x + startPosition.x,
210 | center.y + startPosition.y,
211 | ];
212 | let [arcEndX, midArc, arcEndY] = [
213 | center.x + endPosition.x,
214 | radius * 2 + arcStartY,
215 | center.y + startPosition.y,
216 | ];
217 |
218 | return `M${arcStartX} ${arcStartY}
219 | A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
220 | ${arcEndX} ${midArc}
221 | M${arcStartX} ${midArc}
222 | A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0}
223 | ${arcEndX} ${arcEndY}`;
224 | }
225 |
226 | export function makeGradient(svgDefElem, color, lighter = false) {
227 | let gradientId =
228 | "path-fill-gradient" +
229 | "-" +
230 | color +
231 | "-" +
232 | (lighter ? "lighter" : "default");
233 | let gradientDef = renderVerticalGradient(svgDefElem, gradientId);
234 | let opacities = [1, 0.6, 0.2];
235 | if (lighter) {
236 | opacities = [0.4, 0.2, 0];
237 | }
238 |
239 | setGradientStop(gradientDef, "0%", color, opacities[0]);
240 | setGradientStop(gradientDef, "50%", color, opacities[1]);
241 | setGradientStop(gradientDef, "100%", color, opacities[2]);
242 |
243 | return gradientId;
244 | }
245 |
246 | export function rightRoundedBar(x, width, height) {
247 | // https://medium.com/@dennismphil/one-side-rounded-rectangle-using-svg-fb31cf318d90
248 | let radius = height / 2;
249 | let xOffset = width - radius;
250 |
251 | return `M${x},0 h${xOffset} q${radius},0 ${radius},${radius} q0,${radius} -${radius},${radius} h-${xOffset} v${height}z`;
252 | }
253 |
254 | export function leftRoundedBar(x, width, height) {
255 | let radius = height / 2;
256 | let xOffset = width - radius;
257 |
258 | return `M${
259 | x + radius
260 | },0 h${xOffset} v${height} h-${xOffset} q-${radius}, 0 -${radius},-${radius} q0,-${radius} ${radius},-${radius}z`;
261 | }
262 |
263 | export function percentageBar(
264 | x,
265 | y,
266 | width,
267 | height,
268 | isFirst,
269 | isLast,
270 | fill = "none"
271 | ) {
272 | if (isLast) {
273 | let pathStr = rightRoundedBar(x, width, height);
274 | return makePath(pathStr, "percentage-bar", null, fill);
275 | }
276 |
277 | if (isFirst) {
278 | let pathStr = leftRoundedBar(x, width, height);
279 | return makePath(pathStr, "percentage-bar", null, fill);
280 | }
281 |
282 | let args = {
283 | className: "percentage-bar",
284 | x: x,
285 | y: y,
286 | width: width,
287 | height: height,
288 | fill: fill,
289 | };
290 |
291 | return createSVG("rect", args);
292 | }
293 |
294 | export function heatSquare(
295 | className,
296 | x,
297 | y,
298 | size,
299 | radius,
300 | fill = "none",
301 | data = {}
302 | ) {
303 | let args = {
304 | className: className,
305 | x: x,
306 | y: y,
307 | width: size,
308 | height: size,
309 | rx: radius,
310 | fill: fill,
311 | };
312 |
313 | Object.keys(data).map((key) => {
314 | args[key] = data[key];
315 | });
316 |
317 | return createSVG("rect", args);
318 | }
319 |
320 | export function legendDot(
321 | x,
322 | y,
323 | size,
324 | radius,
325 | fill = "none",
326 | label,
327 | value,
328 | font_size = null,
329 | truncate = false
330 | ) {
331 | label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label;
332 | if (!font_size) font_size = FONT_SIZE;
333 |
334 | let args = {
335 | className: "legend-dot",
336 | x: 0,
337 | y: 4 - size,
338 | height: size,
339 | width: size,
340 | rx: radius,
341 | fill: fill,
342 | };
343 |
344 | let textLabel = createSVG("text", {
345 | className: "legend-dataset-label",
346 | x: size,
347 | y: 0,
348 | dx: font_size + "px",
349 | dy: font_size / 3 + "px",
350 | "font-size": font_size * 1.6 + "px",
351 | "text-anchor": "start",
352 | innerHTML: label,
353 | });
354 |
355 | let textValue = null;
356 | if (value) {
357 | textValue = createSVG("text", {
358 | className: "legend-dataset-value",
359 | x: size,
360 | y: FONT_SIZE + 10,
361 | dx: FONT_SIZE + "px",
362 | dy: FONT_SIZE / 3 + "px",
363 | "font-size": FONT_SIZE * 1.2 + "px",
364 | "text-anchor": "start",
365 | innerHTML: value,
366 | });
367 | }
368 |
369 | let group = createSVG("g", {
370 | transform: `translate(${x}, ${y})`,
371 | });
372 | group.appendChild(createSVG("rect", args));
373 | group.appendChild(textLabel);
374 |
375 | if (value && textValue) {
376 | group.appendChild(textValue);
377 | }
378 |
379 | return group;
380 | }
381 |
382 | export function makeText(className, x, y, content, options = {}) {
383 | let fontSize = options.fontSize || FONT_SIZE;
384 | let dy = options.dy !== undefined ? options.dy : fontSize / 2;
385 | //let fill = options.fill || "var(--charts-label-color)";
386 | let fill = options.fill || "var(--charts-label-color)";
387 | let textAnchor = options.textAnchor || "start";
388 | return createSVG("text", {
389 | className: className,
390 | x: x,
391 | y: y,
392 | dy: dy + "px",
393 | "font-size": fontSize + "px",
394 | fill: fill,
395 | "text-anchor": textAnchor,
396 | innerHTML: content,
397 | });
398 | }
399 |
400 | function makeVertLine(x, label, y1, y2, options = {}) {
401 | if (!options.stroke) options.stroke = BASE_LINE_COLOR;
402 | let l = createSVG("line", {
403 | className: "line-vertical " + options.className,
404 | x1: 0,
405 | x2: 0,
406 | y1: y1,
407 | y2: y2,
408 | styles: {
409 | stroke: options.stroke,
410 | },
411 | });
412 |
413 | let text = createSVG("text", {
414 | x: 0,
415 | y: y1 > y2 ? y1 + LABEL_MARGIN : y1 - LABEL_MARGIN - FONT_SIZE,
416 | dy: FONT_SIZE + "px",
417 | "font-size": FONT_SIZE + "px",
418 | "text-anchor": "middle",
419 | innerHTML: label + "",
420 | });
421 |
422 | let line = createSVG("g", {
423 | transform: `translate(${x}, 0)`,
424 | });
425 |
426 | line.appendChild(l);
427 | line.appendChild(text);
428 |
429 | return line;
430 | }
431 |
432 | function makeHoriLine(y, label, x1, x2, options = {}) {
433 | if (!options.stroke) options.stroke = BASE_LINE_COLOR;
434 | if (!options.lineType) options.lineType = "";
435 | if (!options.alignment) options.alignment = "left";
436 | if (options.shortenNumbers) label = shortenLargeNumber(label);
437 |
438 | let className =
439 | "line-horizontal " +
440 | options.className +
441 | (options.lineType === "dashed" ? "dashed" : "");
442 |
443 | const textXPos =
444 | options.alignment === "left"
445 | ? options.title
446 | ? x1 - LABEL_MARGIN + LABEL_WIDTH
447 | : x1 - LABEL_MARGIN
448 | : options.title
449 | ? x2 + LABEL_MARGIN * 4 - LABEL_WIDTH
450 | : x2 + LABEL_MARGIN * 4;
451 | const lineX1Post = options.title ? x1 + LABEL_WIDTH : x1;
452 | const lineX2Post = options.title ? x2 - LABEL_WIDTH : x2;
453 |
454 | let l = createSVG("line", {
455 | className: className,
456 | x1: lineX1Post,
457 | x2: lineX2Post,
458 | y1: 0,
459 | y2: 0,
460 | styles: {
461 | stroke: options.stroke,
462 | },
463 | });
464 |
465 | let text = createSVG("text", {
466 | x: textXPos,
467 | y: 0,
468 | dy: FONT_SIZE / 2 - 2 + "px",
469 | "font-size": FONT_SIZE + "px",
470 | "text-anchor": x1 < x2 ? "end" : "start",
471 | innerHTML: label + "",
472 | });
473 |
474 | let line = createSVG("g", {
475 | transform: `translate(0, ${y})`,
476 | "stroke-opacity": 1,
477 | });
478 |
479 | if (text === 0 || text === "0") {
480 | line.style.stroke = "rgba(27, 31, 35, 0.6)";
481 | }
482 |
483 | line.appendChild(l);
484 | line.appendChild(text);
485 |
486 | return line;
487 | }
488 |
489 | export function generateAxisLabel(options) {
490 | if (!options.title) return;
491 |
492 | const y =
493 | options.position === "left"
494 | ? (options.height - TOTAL_PADDING) / 2 +
495 | getStringWidth(options.title, 5) / 2
496 | : (options.height - TOTAL_PADDING) / 2 -
497 | getStringWidth(options.title, 5) / 2;
498 | const x = options.position === "left" ? 0 : options.width;
499 | const y2 =
500 | options.position === "left"
501 | ? FONT_SIZE - LABEL_WIDTH
502 | : FONT_SIZE + LABEL_WIDTH * -1;
503 |
504 | const rotation =
505 | options.position === "right" ? `rotate(90)` : `rotate(270)`;
506 |
507 | const labelSvg = createSVG("text", {
508 | className: "chart-label",
509 | x: 0, // getStringWidth(options.title, 5) / 2,
510 | y: 0, // y,
511 | dy: `${y2}px`,
512 | "font-size": `${FONT_SIZE}px`,
513 | "text-anchor": "start",
514 | innerHTML: `${options.title} `,
515 | });
516 |
517 | let wrapper = createSVG("g", {
518 | x: 0,
519 | y: 0,
520 | transformBox: "fill-box",
521 | transform: `translate(${x}, ${y}) ${rotation}`,
522 | className: `test-${options.position}`,
523 | });
524 |
525 | wrapper.appendChild(labelSvg);
526 |
527 | return wrapper;
528 | }
529 |
530 | export function yLine(y, label, width, options = {}) {
531 | if (!isValidNumber(y)) y = 0;
532 |
533 | if (!options.pos) options.pos = "left";
534 | if (!options.offset) options.offset = 0;
535 | if (!options.mode) options.mode = "span";
536 | if (!options.stroke) options.stroke = BASE_LINE_COLOR;
537 | if (!options.className) options.className = "";
538 |
539 | let x1 = -1 * AXIS_TICK_LENGTH;
540 | let x2 = options.mode === "span" ? width + AXIS_TICK_LENGTH : 0;
541 |
542 | if (options.mode === "tick" && options.pos === "right") {
543 | x1 = width + AXIS_TICK_LENGTH;
544 | x2 = width;
545 | }
546 |
547 | let offset = options.pos === "left" ? -1 * options.offset : options.offset;
548 |
549 | // pr_366
550 | //x1 += offset;
551 | //x2 += offset;
552 | x1 += options.offset;
553 | x2 += options.offset;
554 |
555 | if (typeof label === "number") label = round(label);
556 |
557 | return makeHoriLine(y, label, x1, x2, {
558 | stroke: options.stroke,
559 | className: options.className,
560 | lineType: options.lineType,
561 | alignment: options.pos,
562 | title: options.title,
563 | shortenNumbers: options.shortenNumbers,
564 | });
565 | }
566 |
567 | export function xLine(x, label, height, options = {}) {
568 | if (!isValidNumber(x)) x = 0;
569 |
570 | if (!options.pos) options.pos = "bottom";
571 | if (!options.offset) options.offset = 0;
572 | if (!options.mode) options.mode = "span";
573 | if (!options.stroke) options.stroke = BASE_LINE_COLOR;
574 | if (!options.className) options.className = "";
575 |
576 | // Draw X axis line in span/tick mode with optional label
577 | // y2(span)
578 | // |
579 | // |
580 | // x line |
581 | // |
582 | // |
583 | // ---------------------+-- y2(tick)
584 | // |
585 | // y1
586 |
587 | let y1 = height + AXIS_TICK_LENGTH;
588 | let y2 = options.mode === "span" ? -1 * AXIS_TICK_LENGTH : height;
589 |
590 | if (options.mode === "tick" && options.pos === "top") {
591 | // top axis ticks
592 | y1 = -1 * AXIS_TICK_LENGTH;
593 | y2 = 0;
594 | }
595 |
596 | return makeVertLine(x, label, y1, y2, {
597 | stroke: options.stroke,
598 | className: options.className,
599 | lineType: options.lineType,
600 | });
601 | }
602 |
603 | export function yMarker(y, label, width, options = {}) {
604 | if (!isValidNumber(y)) y = 0;
605 |
606 | if (!options.labelPos) options.labelPos = "right";
607 | if (!options.lineType) options.lineType = "dashed";
608 | let x =
609 | options.labelPos === "left"
610 | ? LABEL_MARGIN
611 | : width - getStringWidth(label, 5) - LABEL_MARGIN;
612 |
613 | let labelSvg = createSVG("text", {
614 | className: "chart-label",
615 | x: x,
616 | y: 0,
617 | dy: FONT_SIZE / -2 + "px",
618 | "font-size": FONT_SIZE + "px",
619 | "text-anchor": "start",
620 | innerHTML: label + "",
621 | });
622 |
623 | let line = makeHoriLine(y, "", 0, width, {
624 | stroke: options.stroke || BASE_LINE_COLOR,
625 | className: options.className || "",
626 | lineType: options.lineType,
627 | });
628 |
629 | line.appendChild(labelSvg);
630 |
631 | return line;
632 | }
633 |
634 | export function yRegion(y1, y2, width, label, options = {}) {
635 | // return a group
636 | let height = y1 - y2;
637 |
638 | let rect = createSVG("rect", {
639 | className: `bar mini`, // remove class
640 | styles: {
641 | fill: options.fill || `rgba(228, 234, 239, 0.49)`,
642 | stroke: options.stroke || BASE_LINE_COLOR,
643 | "stroke-dasharray": `${width}, ${height}`,
644 | },
645 | // 'data-point-index': index,
646 | x: 0,
647 | y: 0,
648 | width: width,
649 | height: height,
650 | });
651 |
652 | if (!options.labelPos) options.labelPos = "right";
653 | let x =
654 | options.labelPos === "left"
655 | ? LABEL_MARGIN
656 | : width - getStringWidth(label + "", 4.5) - LABEL_MARGIN;
657 |
658 | let labelSvg = createSVG("text", {
659 | className: "chart-label",
660 | x: x,
661 | y: 0,
662 | dy: FONT_SIZE / -2 + "px",
663 | "font-size": FONT_SIZE + "px",
664 | "text-anchor": "start",
665 | innerHTML: label + "",
666 | });
667 |
668 | let region = createSVG("g", {
669 | transform: `translate(0, ${y2})`,
670 | });
671 |
672 | region.appendChild(rect);
673 | region.appendChild(labelSvg);
674 |
675 | return region;
676 | }
677 |
678 | export function datasetBar(
679 | x,
680 | yTop,
681 | width,
682 | color,
683 | label = "",
684 | index = 0,
685 | offset = 0,
686 | meta = {}
687 | ) {
688 | let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine);
689 | y -= offset;
690 |
691 | if (height === 0) {
692 | height = meta.minHeight;
693 | y -= meta.minHeight;
694 | }
695 |
696 | // Preprocess numbers to avoid svg building errors
697 | if (!isValidNumber(x)) x = 0;
698 | if (!isValidNumber(y)) y = 0;
699 | if (!isValidNumber(height, true)) height = 0;
700 | if (!isValidNumber(width, true)) width = 0;
701 |
702 | // x y h w
703 |
704 | // M{x},{y+r}
705 | // q0,-{r} {r},-{r}
706 | // q{r},0 {r},{r}
707 | // v{h-r}
708 | // h-{w}z
709 |
710 | // let radius = width/2;
711 | // let pathStr = `M${x},${y+radius} q0,-${radius} ${radius},-${radius} q${radius},0 ${radius},${radius} v${height-radius} h-${width}z`
712 |
713 | // let rect = createSVG('path', {
714 | // className: 'bar mini',
715 | // d: pathStr,
716 | // styles: { fill: color },
717 | // x: x,
718 | // y: y,
719 | // 'data-point-index': index,
720 | // });
721 |
722 | let rect = createSVG("rect", {
723 | className: `bar mini`,
724 | style: `fill: ${color}`,
725 | "data-point-index": index,
726 | x: x,
727 | y: y,
728 | width: width,
729 | height: height,
730 | });
731 |
732 | label += "";
733 |
734 | if (!label && !label.length) {
735 | return rect;
736 | } else {
737 | rect.setAttribute("y", 0);
738 | rect.setAttribute("x", 0);
739 | let text = createSVG("text", {
740 | className: "data-point-value",
741 | x: width / 2,
742 | y: 0,
743 | dy: (FONT_SIZE / 2) * -1 + "px",
744 | "font-size": FONT_SIZE + "px",
745 | "text-anchor": "middle",
746 | innerHTML: label,
747 | });
748 |
749 | let group = createSVG("g", {
750 | "data-point-index": index,
751 | transform: `translate(${x}, ${y})`,
752 | });
753 | group.appendChild(rect);
754 | group.appendChild(text);
755 |
756 | return group;
757 | }
758 | }
759 |
760 | export function datasetDot(x, y, radius, color, label = "", index = 0) {
761 | let dot = createSVG("circle", {
762 | style: `fill: ${color}`,
763 | "data-point-index": index,
764 | cx: x,
765 | cy: y,
766 | r: radius,
767 | });
768 |
769 | label += "";
770 |
771 | if (!label && !label.length) {
772 | return dot;
773 | } else {
774 | dot.setAttribute("cy", 0);
775 | dot.setAttribute("cx", 0);
776 |
777 | let text = createSVG("text", {
778 | className: "data-point-value",
779 | x: 0,
780 | y: 0,
781 | dy: (FONT_SIZE / 2) * -1 - radius + "px",
782 | "font-size": FONT_SIZE + "px",
783 | "text-anchor": "middle",
784 | innerHTML: label,
785 | });
786 |
787 | let group = createSVG("g", {
788 | "data-point-index": index,
789 | transform: `translate(${x}, ${y})`,
790 | });
791 | group.appendChild(dot);
792 | group.appendChild(text);
793 |
794 | return group;
795 | }
796 | }
797 |
798 | export function getPaths(xList, yList, color, options = {}, meta = {}) {
799 | let pointsList = yList.map((y, i) => xList[i] + "," + y);
800 | let pointsStr = pointsList.join("L");
801 |
802 | // Spline
803 | if (options.spline) pointsStr = getSplineCurvePointsStr(xList, yList);
804 |
805 | let path = makePath("M" + pointsStr, "line-graph-path", color);
806 |
807 | // HeatLine
808 | if (options.heatline) {
809 | let gradient_id = makeGradient(meta.svgDefs, color);
810 | path.style.stroke = `url(#${gradient_id})`;
811 | }
812 |
813 | let paths = {
814 | path: path,
815 | };
816 |
817 | // Region
818 | if (options.regionFill) {
819 | let gradient_id_region = makeGradient(meta.svgDefs, color, true);
820 |
821 | let pathStr =
822 | "M" +
823 | `${xList[0]},${meta.zeroLine}L` +
824 | pointsStr +
825 | `L${xList.slice(-1)[0]},${meta.zeroLine}`;
826 | paths.region = makePath(
827 | pathStr,
828 | `region-fill`,
829 | "none",
830 | `url(#${gradient_id_region})`
831 | );
832 | }
833 |
834 | return paths;
835 | }
836 |
837 | export let makeOverlay = {
838 | bar: (unit) => {
839 | let transformValue;
840 | if (unit.nodeName !== "rect") {
841 | transformValue = unit.getAttribute("transform");
842 | unit = unit.childNodes[0];
843 | }
844 | let overlay = unit.cloneNode();
845 | overlay.style.fill = "#000000";
846 | overlay.style.opacity = "0.4";
847 |
848 | if (transformValue) {
849 | overlay.setAttribute("transform", transformValue);
850 | }
851 | return overlay;
852 | },
853 |
854 | dot: (unit) => {
855 | let transformValue;
856 | if (unit.nodeName !== "circle") {
857 | transformValue = unit.getAttribute("transform");
858 | unit = unit.childNodes[0];
859 | }
860 | let overlay = unit.cloneNode();
861 | let radius = unit.getAttribute("r");
862 | let fill = unit.getAttribute("fill");
863 | overlay.setAttribute("r", parseInt(radius) + DOT_OVERLAY_SIZE_INCR);
864 | overlay.setAttribute("fill", fill);
865 | overlay.style.opacity = "0.6";
866 |
867 | if (transformValue) {
868 | overlay.setAttribute("transform", transformValue);
869 | }
870 | return overlay;
871 | },
872 |
873 | heat_square: (unit) => {
874 | let transformValue;
875 | if (unit.nodeName !== "circle") {
876 | transformValue = unit.getAttribute("transform");
877 | unit = unit.childNodes[0];
878 | }
879 | let overlay = unit.cloneNode();
880 | let radius = unit.getAttribute("r");
881 | let fill = unit.getAttribute("fill");
882 | overlay.setAttribute("r", parseInt(radius) + DOT_OVERLAY_SIZE_INCR);
883 | overlay.setAttribute("fill", fill);
884 | overlay.style.opacity = "0.6";
885 |
886 | if (transformValue) {
887 | overlay.setAttribute("transform", transformValue);
888 | }
889 | return overlay;
890 | },
891 | };
892 |
893 | export let updateOverlay = {
894 | bar: (unit, overlay) => {
895 | let transformValue;
896 | if (unit.nodeName !== "rect") {
897 | transformValue = unit.getAttribute("transform");
898 | unit = unit.childNodes[0];
899 | }
900 | let attributes = ["x", "y", "width", "height"];
901 | Object.values(unit.attributes)
902 | .filter((attr) => attributes.includes(attr.name) && attr.specified)
903 | .map((attr) => {
904 | overlay.setAttribute(attr.name, attr.nodeValue);
905 | });
906 |
907 | if (transformValue) {
908 | overlay.setAttribute("transform", transformValue);
909 | }
910 | },
911 |
912 | dot: (unit, overlay) => {
913 | let transformValue;
914 | if (unit.nodeName !== "circle") {
915 | transformValue = unit.getAttribute("transform");
916 | unit = unit.childNodes[0];
917 | }
918 | let attributes = ["cx", "cy"];
919 | Object.values(unit.attributes)
920 | .filter((attr) => attributes.includes(attr.name) && attr.specified)
921 | .map((attr) => {
922 | overlay.setAttribute(attr.name, attr.nodeValue);
923 | });
924 |
925 | if (transformValue) {
926 | overlay.setAttribute("transform", transformValue);
927 | }
928 | },
929 |
930 | heat_square: (unit, overlay) => {
931 | let transformValue;
932 | if (unit.nodeName !== "circle") {
933 | transformValue = unit.getAttribute("transform");
934 | unit = unit.childNodes[0];
935 | }
936 | let attributes = ["cx", "cy"];
937 | Object.values(unit.attributes)
938 | .filter((attr) => attributes.includes(attr.name) && attr.specified)
939 | .map((attr) => {
940 | overlay.setAttribute(attr.name, attr.nodeValue);
941 | });
942 |
943 | if (transformValue) {
944 | overlay.setAttribute("transform", transformValue);
945 | }
946 | },
947 | };
948 |
--------------------------------------------------------------------------------
/src/js/utils/export.js:
--------------------------------------------------------------------------------
1 | import { $ } from "../utils/dom";
2 | import { CSSTEXT } from "../../css/chartsCss";
3 |
4 | export function downloadFile(filename, data) {
5 | var a = document.createElement("a");
6 | a.style = "display: none";
7 | var blob = new Blob(data, { type: "image/svg+xml; charset=utf-8" });
8 | var url = window.URL.createObjectURL(blob);
9 | a.href = url;
10 | a.download = filename;
11 | document.body.appendChild(a);
12 | a.click();
13 | setTimeout(function () {
14 | document.body.removeChild(a);
15 | window.URL.revokeObjectURL(url);
16 | }, 300);
17 | }
18 |
19 | export function prepareForExport(svg) {
20 | let clone = svg.cloneNode(true);
21 | clone.classList.add("chart-container");
22 | clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
23 | clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
24 | let styleEl = $.create("style", {
25 | innerHTML: CSSTEXT,
26 | });
27 | clone.insertBefore(styleEl, clone.firstChild);
28 |
29 | let container = $.create("div");
30 | container.appendChild(clone);
31 |
32 | return container.innerHTML;
33 | }
34 |
--------------------------------------------------------------------------------
/src/js/utils/helpers.js:
--------------------------------------------------------------------------------
1 | import { ANGLE_RATIO } from "./constants";
2 |
3 | /**
4 | * Returns the value of a number upto 2 decimal places.
5 | * @param {Number} d Any number
6 | */
7 | export function floatTwo(d) {
8 | return parseFloat(d.toFixed(2));
9 | }
10 |
11 | /**
12 | * Returns whether or not two given arrays are equal.
13 | * @param {Array} arr1 First array
14 | * @param {Array} arr2 Second array
15 | */
16 | export function arraysEqual(arr1, arr2) {
17 | if (arr1.length !== arr2.length) return false;
18 | let areEqual = true;
19 | arr1.map((d, i) => {
20 | if (arr2[i] !== d) areEqual = false;
21 | });
22 | return areEqual;
23 | }
24 |
25 | /**
26 | * Shuffles array in place. ES6 version
27 | * @param {Array} array An array containing the items.
28 | */
29 | export function shuffle(array) {
30 | // Awesomeness: https://bost.ocks.org/mike/shuffle/
31 | // https://stackoverflow.com/a/2450976/6495043
32 | // https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array?noredirect=1&lq=1
33 |
34 | for (let i = array.length - 1; i > 0; i--) {
35 | let j = Math.floor(Math.random() * (i + 1));
36 | [array[i], array[j]] = [array[j], array[i]];
37 | }
38 |
39 | return array;
40 | }
41 |
42 | /**
43 | * Fill an array with extra points
44 | * @param {Array} array Array
45 | * @param {Number} count number of filler elements
46 | * @param {Object} element element to fill with
47 | * @param {Boolean} start fill at start?
48 | */
49 | export function fillArray(array, count, element, start = false) {
50 | if (element == undefined) {
51 | element = start ? array[0] : array[array.length - 1];
52 | }
53 | let fillerArray = new Array(Math.abs(count)).fill(element);
54 | array = start ? fillerArray.concat(array) : array.concat(fillerArray);
55 | return array;
56 | }
57 |
58 | /**
59 | * Returns pixel width of string.
60 | * @param {String} string
61 | * @param {Number} charWidth Width of single char in pixels
62 | */
63 | export function getStringWidth(string, charWidth) {
64 | return (string + "").length * charWidth;
65 | }
66 |
67 | export function bindChange(obj, getFn, setFn) {
68 | return new Proxy(obj, {
69 | set: function (target, prop, value) {
70 | setFn();
71 | return Reflect.set(target, prop, value);
72 | },
73 | get: function (target, prop) {
74 | getFn();
75 | return Reflect.get(target, prop);
76 | },
77 | });
78 | }
79 |
80 | // https://stackoverflow.com/a/29325222
81 | export function getRandomBias(min, max, bias, influence) {
82 | const range = max - min;
83 | const biasValue = range * bias + min;
84 | var rnd = Math.random() * range + min, // random in range
85 | mix = Math.random() * influence; // random mixer
86 | return rnd * (1 - mix) + biasValue * mix; // mix full range and bias
87 | }
88 |
89 | export function getPositionByAngle(angle, radius) {
90 | return {
91 | x: Math.sin(angle * ANGLE_RATIO) * radius,
92 | y: Math.cos(angle * ANGLE_RATIO) * radius,
93 | };
94 | }
95 |
96 | /**
97 | * Check if a number is valid for svg attributes
98 | * @param {object} candidate Candidate to test
99 | * @param {Boolean} nonNegative flag to treat negative number as invalid
100 | */
101 | export function isValidNumber(candidate, nonNegative = false) {
102 | if (Number.isNaN(candidate)) return false;
103 | else if (candidate === undefined) return false;
104 | else if (!Number.isFinite(candidate)) return false;
105 | else if (nonNegative && candidate < 0) return false;
106 | else return true;
107 | }
108 |
109 | /**
110 | * Round a number to the closes precision, max max precision 4
111 | * @param {Number} d Any Number
112 | */
113 | export function round(d) {
114 | // https://floating-point-gui.de/
115 | // https://www.jacklmoore.com/notes/rounding-in-javascript/
116 | return Number(Math.round(d + "e4") + "e-4");
117 | }
118 |
119 | /**
120 | * Creates a deep clone of an object
121 | * @param {Object} candidate Any Object
122 | */
123 | export function deepClone(candidate) {
124 | let cloned, value, key;
125 |
126 | if (candidate instanceof Date) {
127 | return new Date(candidate.getTime());
128 | }
129 |
130 | if (typeof candidate !== "object" || candidate === null) {
131 | return candidate;
132 | }
133 |
134 | cloned = Array.isArray(candidate) ? [] : {};
135 |
136 | for (key in candidate) {
137 | value = candidate[key];
138 |
139 | cloned[key] = deepClone(value);
140 | }
141 |
142 | return cloned;
143 | }
144 |
--------------------------------------------------------------------------------
/src/js/utils/intervals.js:
--------------------------------------------------------------------------------
1 | import { floatTwo } from "./helpers";
2 |
3 | function normalize(x) {
4 | // Calculates mantissa and exponent of a number
5 | // Returns normalized number and exponent
6 | // https://stackoverflow.com/q/9383593/6495043
7 |
8 | if (x === 0) {
9 | return [0, 0];
10 | }
11 | if (isNaN(x)) {
12 | return { mantissa: -6755399441055744, exponent: 972 };
13 | }
14 | var sig = x > 0 ? 1 : -1;
15 | if (!isFinite(x)) {
16 | return { mantissa: sig * 4503599627370496, exponent: 972 };
17 | }
18 |
19 | x = Math.abs(x);
20 | var exp = Math.floor(Math.log10(x));
21 | var man = x / Math.pow(10, exp);
22 |
23 | return [sig * man, exp];
24 | }
25 |
26 | function getChartRangeIntervals(max, min = 0) {
27 | let upperBound = Math.ceil(max);
28 | let lowerBound = Math.floor(min);
29 | let range = upperBound - lowerBound;
30 |
31 | let noOfParts = range;
32 | let partSize = 1;
33 |
34 | // To avoid too many partitions
35 | if (range > 5) {
36 | if (range % 2 !== 0) {
37 | upperBound++;
38 | // Recalc range
39 | range = upperBound - lowerBound;
40 | }
41 | noOfParts = range / 2;
42 | partSize = 2;
43 | }
44 |
45 | // Special case: 1 and 2
46 | if (range <= 2) {
47 | noOfParts = 4;
48 | partSize = range / noOfParts;
49 | }
50 |
51 | // Special case: 0
52 | if (range === 0) {
53 | noOfParts = 5;
54 | partSize = 1;
55 | }
56 |
57 | let intervals = [];
58 | for (var i = 0; i <= noOfParts; i++) {
59 | intervals.push(lowerBound + partSize * i);
60 | }
61 | return intervals;
62 | }
63 |
64 | function getChartIntervals(maxValue, minValue = 0) {
65 | let [normalMaxValue, exponent] = normalize(maxValue);
66 | let normalMinValue = minValue ? minValue / Math.pow(10, exponent) : 0;
67 |
68 | // Allow only 7 significant digits
69 | normalMaxValue = normalMaxValue.toFixed(6);
70 |
71 | let intervals = getChartRangeIntervals(normalMaxValue, normalMinValue);
72 | intervals = intervals.map((value) => {
73 | // For negative exponents we want to divide by 10^-exponent to avoid
74 | // floating point arithmetic bugs. For instance, in javascript
75 | // 6 * 10^-1 == 0.6000000000000001, we instead want 6 / 10^1 == 0.6
76 | if (exponent < 0) {
77 | return value / Math.pow(10, -exponent);
78 | }
79 | return value * Math.pow(10, exponent);
80 | });
81 | return intervals;
82 | }
83 |
84 | export function calcChartIntervals(values, withMinimum = true, overrideCeiling=false, overrideFloor=false) {
85 | //*** Where the magic happens ***
86 |
87 | // Calculates best-fit y intervals from given values
88 | // and returns the interval array
89 |
90 | let maxValue = Math.max(...values);
91 | let minValue = Math.min(...values);
92 |
93 | if (overrideCeiling) {
94 | maxValue = overrideCeiling
95 | }
96 |
97 | if (overrideFloor) {
98 | minValue = overrideFloor
99 | }
100 |
101 | // Exponent to be used for pretty print
102 | let exponent = 0,
103 | intervals = []; // eslint-disable-line no-unused-vars
104 |
105 | function getPositiveFirstIntervals(maxValue, absMinValue) {
106 | let intervals = getChartIntervals(maxValue);
107 |
108 | let intervalSize = intervals[1] - intervals[0];
109 |
110 | // Then unshift the negative values
111 | let value = 0;
112 | for (var i = 1; value < absMinValue; i++) {
113 | value += intervalSize;
114 | intervals.unshift(-1 * value);
115 | }
116 | return intervals;
117 | }
118 |
119 | // CASE I: Both non-negative
120 |
121 | if (maxValue >= 0 && minValue >= 0) {
122 | exponent = normalize(maxValue)[1];
123 | if (!withMinimum) {
124 | intervals = getChartIntervals(maxValue);
125 | } else {
126 | intervals = getChartIntervals(maxValue, minValue);
127 | }
128 | }
129 |
130 | // CASE II: Only minValue negative
131 | else if (maxValue > 0 && minValue < 0) {
132 | // `withMinimum` irrelevant in this case,
133 | // We'll be handling both sides of zero separately
134 | // (both starting from zero)
135 | // Because ceil() and floor() behave differently
136 | // in those two regions
137 |
138 | let absMinValue = Math.abs(minValue);
139 |
140 | if (maxValue >= absMinValue) {
141 | exponent = normalize(maxValue)[1];
142 | intervals = getPositiveFirstIntervals(maxValue, absMinValue);
143 | } else {
144 | // Mirror: maxValue => absMinValue, then change sign
145 | exponent = normalize(absMinValue)[1];
146 | let posIntervals = getPositiveFirstIntervals(absMinValue, maxValue);
147 | intervals = posIntervals.reverse().map((d) => d * -1);
148 | }
149 | }
150 |
151 | // CASE III: Both non-positive
152 | else if (maxValue <= 0 && minValue <= 0) {
153 | // Mirrored Case I:
154 | // Work with positives, then reverse the sign and array
155 |
156 | let pseudoMaxValue = Math.abs(minValue);
157 | let pseudoMinValue = Math.abs(maxValue);
158 |
159 | exponent = normalize(pseudoMaxValue)[1];
160 | if (!withMinimum) {
161 | intervals = getChartIntervals(pseudoMaxValue);
162 | } else {
163 | intervals = getChartIntervals(pseudoMaxValue, pseudoMinValue);
164 | }
165 |
166 | intervals = intervals.reverse().map((d) => d * -1);
167 | }
168 |
169 | return intervals.sort((a, b) => a - b);
170 | }
171 |
172 | export function getZeroIndex(yPts) {
173 | let zeroIndex;
174 | let interval = getIntervalSize(yPts);
175 | if (yPts.indexOf(0) >= 0) {
176 | // the range has a given zero
177 | // zero-line on the chart
178 | zeroIndex = yPts.indexOf(0);
179 | } else if (yPts[0] > 0) {
180 | // Minimum value is positive
181 | // zero-line is off the chart: below
182 | let min = yPts[0];
183 | zeroIndex = (-1 * min) / interval;
184 | } else {
185 | // Maximum value is negative
186 | // zero-line is off the chart: above
187 | let max = yPts[yPts.length - 1];
188 | zeroIndex = (-1 * max) / interval + (yPts.length - 1);
189 | }
190 | return zeroIndex;
191 | }
192 |
193 | export function getRealIntervals(max, noOfIntervals, min = 0, asc = 1) {
194 | let range = max - min;
195 | let part = (range * 1.0) / noOfIntervals;
196 | let intervals = [];
197 |
198 | for (var i = 0; i <= noOfIntervals; i++) {
199 | intervals.push(min + part * i);
200 | }
201 |
202 | return asc ? intervals : intervals.reverse();
203 | }
204 |
205 | export function getIntervalSize(orderedArray) {
206 | return orderedArray[1] - orderedArray[0];
207 | }
208 |
209 | export function getValueRange(orderedArray) {
210 | return orderedArray[orderedArray.length - 1] - orderedArray[0];
211 | }
212 |
213 | export function scale(val, yAxis) {
214 | return floatTwo(yAxis.zeroLine - val * yAxis.scaleMultiplier);
215 | }
216 |
217 | export function isInRange(val, min, max) {
218 | return val > min && val < max;
219 | }
220 |
221 | export function isInRange2D(coord, minCoord, maxCoord) {
222 | return (
223 | isInRange(coord[0], minCoord[0], maxCoord[0]) &&
224 | isInRange(coord[1], minCoord[1], maxCoord[1])
225 | );
226 | }
227 |
228 | export function getClosestInArray(goal, arr, index = false) {
229 | let closest = arr.reduce(function (prev, curr) {
230 | return Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev;
231 | }, []);
232 |
233 | return index ? arr.indexOf(closest) : closest;
234 | }
235 |
236 | export function calcDistribution(values, distributionSize) {
237 | // Assume non-negative values,
238 | // implying distribution minimum at zero
239 |
240 | let dataMaxValue = Math.max(...values);
241 |
242 | let distributionStep = 1 / (distributionSize - 1);
243 | let distribution = [];
244 |
245 | for (var i = 0; i < distributionSize; i++) {
246 | let checkpoint = dataMaxValue * (distributionStep * i);
247 | distribution.push(checkpoint);
248 | }
249 |
250 | return distribution;
251 | }
252 |
253 | export function getMaxCheckpoint(value, distribution) {
254 | return distribution.filter((d) => d < value).length;
255 | }
256 |
--------------------------------------------------------------------------------
/src/js/utils/test/colors.test.js:
--------------------------------------------------------------------------------
1 | const assert = require("assert");
2 | const colors = require("../colors");
3 |
4 | describe("utils.colors", () => {
5 | it("should return #aaabac for RGB()", () => {
6 | assert.equal(colors.getColor("rgb(170, 171, 172)"), "#aaabac");
7 | });
8 | it("should return #ff5858 for the named color red", () => {
9 | assert.equal(colors.getColor("red"), "#ff5858d");
10 | });
11 | it("should return #1a5c29 for the hex color #1a5c29", () => {
12 | assert.equal(colors.getColor("#1a5c29"), "#1a5c29");
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/src/js/utils/test/helpers.test.js:
--------------------------------------------------------------------------------
1 | const assert = require("assert");
2 | const helpers = require("../helpers");
3 |
4 | describe("utils.helpers", () => {
5 | it("should return a value fixed upto 2 decimals", () => {
6 | assert.equal(helpers.floatTwo(1.234), 1.23);
7 | assert.equal(helpers.floatTwo(1.456), 1.46);
8 | assert.equal(helpers.floatTwo(1), 1.0);
9 | });
10 | });
11 |
--------------------------------------------------------------------------------