From f2262fac434816c69f53e414b02ecac55bca405d Mon Sep 17 00:00:00 2001 From: Arjun Choudhary Date: Thu, 24 Nov 2022 12:54:27 +0530 Subject: [PATCH] chore: formatting changes --- .babelrc | 19 +- .eslintrc.json | 53 +- .travis.yml | 2 +- rollup.config.js | 70 +- src/css/charts.scss | 306 +++---- src/css/chartsCss.js | 3 +- src/js/chart.js | 54 +- src/js/charts/AggregationChart.js | 176 ++-- src/js/charts/AxisChart.js | 1202 +++++++++++++------------- src/js/charts/BaseChart.js | 568 ++++++------- src/js/charts/DonutChart.js | 302 ++++--- src/js/charts/Heatmap.js | 519 ++++++------ src/js/charts/MultiAxisChart.js | 304 +++---- src/js/charts/PercentageChart.js | 154 ++-- src/js/charts/PieChart.js | 347 ++++---- src/js/index.js | 12 +- src/js/objects/ChartComponents.js | 836 +++++++++--------- src/js/objects/SvgTip.js | 214 +++-- src/js/utils/animate.js | 154 ++-- src/js/utils/animation.js | 191 +++-- src/js/utils/axis-chart-utils.js | 219 ++--- src/js/utils/colors.js | 94 ++- src/js/utils/constants.js | 138 +-- src/js/utils/date-utils.js | 131 ++- src/js/utils/dom.js | 192 +++-- src/js/utils/draw-utils.js | 162 ++-- src/js/utils/draw.js | 1302 ++++++++++++++++------------- src/js/utils/export.js | 50 +- src/js/utils/helpers.js | 128 +-- src/js/utils/intervals.js | 350 ++++---- src/js/utils/test/colors.test.js | 20 +- src/js/utils/test/helpers.test.js | 12 +- 32 files changed, 4371 insertions(+), 3913 deletions(-) diff --git a/.babelrc b/.babelrc index be3758c..4de775b 100644 --- a/.babelrc +++ b/.babelrc @@ -1,10 +1,13 @@ { - "presets": [ - ["@babel/preset-env", { - "targets": { - "browsers": ["last 2 versions", "safari >= 7"] - }, - "modules": false - }] - ] + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "browsers": ["last 2 versions", "safari >= 7"] + }, + "modules": false + } + ] + ] } diff --git a/.eslintrc.json b/.eslintrc.json index 1a67d2b..f51ad4a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,33 +1,24 @@ { - "env": { - "browser": true, - "es6": true - }, - "extends": "eslint:recommended", - "parserOptions": { - "sourceType": "module" - }, - "rules": { - "indent": [ - "error", - "tab" - ], - "linebreak-style": [ - "error", - "unix" - ], - "semi": [ - "error", - "always" - ], - "no-console": [ - "error", - { - "allow": ["warn", "error"] - } - ] - }, - "globals": { - "ENV": true - } + "env": { + "browser": true, + "es6": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "indent": ["error", "tab"], + "linebreak-style": ["error", "unix"], + "semi": ["error", "always"], + "no-console": [ + "error", + { + "allow": ["warn", "error"] + } + ] + }, + "globals": { + "ENV": true + } } diff --git a/.travis.yml b/.travis.yml index a32df86..d9cf77f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,4 +11,4 @@ script: - make test after_success: - - make coveralls \ No newline at end of file + - make coveralls diff --git a/rollup.config.js b/rollup.config.js index 0b26d91..090356e 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -8,40 +8,40 @@ import bundleSize from "rollup-plugin-bundle-size"; import { terser } from "rollup-plugin-terser"; export default [ - // browser-friendly UMD build - { - input: "src/js/index.js", - output: { - sourcemap: true, - name: "frappe", - file: pkg.browser, - format: "umd", - }, - plugins: [ - commonjs(), - babel({ - exclude: ["node_modules/**"], - }), - terser(), - scss({ output: "dist/frappe-charts.min.css" }), - bundleSize(), - ], - }, + // browser-friendly UMD build + { + input: "src/js/index.js", + output: { + sourcemap: true, + name: "frappe", + file: pkg.browser, + format: "umd", + }, + plugins: [ + commonjs(), + babel({ + exclude: ["node_modules/**"], + }), + terser(), + scss({ output: "dist/frappe-charts.min.css" }), + bundleSize(), + ], + }, - // CommonJS (for Node) and ES module (for bundlers) build. - { - input: "src/js/chart.js", - output: [ - { file: pkg.common, format: "cjs", sourcemap: true }, - { file: pkg.module, format: "es", sourcemap: true }, - ], - plugins: [ - babel({ - exclude: ["node_modules/**"], - }), - terser(), - postcss(), - bundleSize(), - ], - }, + // CommonJS (for Node) and ES module (for bundlers) build. + { + input: "src/js/chart.js", + output: [ + { file: pkg.common, format: "cjs", sourcemap: true }, + { file: pkg.module, format: "es", sourcemap: true }, + ], + plugins: [ + babel({ + exclude: ["node_modules/**"], + }), + terser(), + postcss(), + bundleSize(), + ], + }, ]; diff --git a/src/css/charts.scss b/src/css/charts.scss index 9f0c4f9..7cb36f8 100644 --- a/src/css/charts.scss +++ b/src/css/charts.scss @@ -1,191 +1,191 @@ :root { - --charts-label-color: #313b44; - --charts-axis-line-color: #f4f5f6; + --charts-label-color: #313b44; + --charts-axis-line-color: #f4f5f6; - --charts-tooltip-title: var(--charts-label-color); - --charts-tooltip-label: var(--charts-label-color); - --charts-tooltip-value: #192734; - --charts-tooltip-bg: #ffffff; + --charts-tooltip-title: var(--charts-label-color); + --charts-tooltip-label: var(--charts-label-color); + --charts-tooltip-value: #192734; + --charts-tooltip-bg: #ffffff; - --charts-stroke-width: 2px; - --charts-dataset-circle-stroke: #ffffff; - --charts-dataset-circle-stroke-width: var(--charts-stroke-width); + --charts-stroke-width: 2px; + --charts-dataset-circle-stroke: #ffffff; + --charts-dataset-circle-stroke-width: var(--charts-stroke-width); - --charts-legend-label: var(--charts-label-color); - --charts-legend-value: var(--charts-label-color); + --charts-legend-label: var(--charts-label-color); + --charts-legend-value: var(--charts-label-color); } .chart-container { - position: relative; - /* for absolutely positioned tooltip */ + position: relative; + /* for absolutely positioned tooltip */ - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", - "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", - "Helvetica Neue", sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; - .axis, - .chart-label { - fill: var(--charts-label-color); + .axis, + .chart-label { + fill: var(--charts-label-color); - line { - stroke: var(--charts-axis-line-color); - } - } + line { + stroke: var(--charts-axis-line-color); + } + } - .dataset-units { - circle { - stroke: var(--charts-dataset-circle-stroke); - stroke-width: var(--charts-dataset-circle-stroke-width); - } + .dataset-units { + circle { + stroke: var(--charts-dataset-circle-stroke); + stroke-width: var(--charts-dataset-circle-stroke-width); + } - path { - fill: none; - stroke-opacity: 1; - stroke-width: var(--charts-stroke-width); - } - } + path { + fill: none; + stroke-opacity: 1; + stroke-width: var(--charts-stroke-width); + } + } - .dataset-path { - stroke-width: var(--charts-stroke-width); - } + .dataset-path { + stroke-width: var(--charts-stroke-width); + } - .path-group { - path { - fill: none; - stroke-opacity: 1; - stroke-width: var(--charts-stroke-width); - } - } + .path-group { + path { + fill: none; + stroke-opacity: 1; + stroke-width: var(--charts-stroke-width); + } + } - line.dashed { - stroke-dasharray: 5, 3; - } + line.dashed { + stroke-dasharray: 5, 3; + } - .axis-line { - .specific-value { - text-anchor: start; - } + .axis-line { + .specific-value { + text-anchor: start; + } - .y-line { - text-anchor: end; - } + .y-line { + text-anchor: end; + } - .x-line { - text-anchor: middle; - } - } + .x-line { + text-anchor: middle; + } + } - .legend-dataset-label { - fill: var(--charts-legend-label); - font-weight: 600; - } + .legend-dataset-label { + fill: var(--charts-legend-label); + font-weight: 600; + } - .legend-dataset-value { - fill: var(--charts-legend-value); - } + .legend-dataset-value { + fill: var(--charts-legend-value); + } } .graph-svg-tip { - position: absolute; - z-index: 99999; - padding: 10px; - font-size: 12px; - text-align: center; - background: var(--charts-tooltip-bg); - box-shadow: 0px 1px 4px rgba(17, 43, 66, 0.1), - 0px 2px 6px rgba(17, 43, 66, 0.08), - 0px 40px 30px -30px rgba(17, 43, 66, 0.1); - border-radius: 6px; + position: absolute; + z-index: 99999; + padding: 10px; + font-size: 12px; + text-align: center; + background: var(--charts-tooltip-bg); + box-shadow: 0px 1px 4px rgba(17, 43, 66, 0.1), + 0px 2px 6px rgba(17, 43, 66, 0.08), + 0px 40px 30px -30px rgba(17, 43, 66, 0.1); + border-radius: 6px; - ul { - padding-left: 0; - display: flex; - } + ul { + padding-left: 0; + display: flex; + } - ol { - padding-left: 0; - display: flex; - } + ol { + padding-left: 0; + display: flex; + } - ul.data-point-list { - li { - min-width: 90px; - font-weight: 600; - } - } + ul.data-point-list { + li { + min-width: 90px; + font-weight: 600; + } + } - .svg-pointer { - position: absolute; - height: 12px; - width: 12px; - border-radius: 2px; - background: var(--charts-tooltip-bg); - transform: rotate(45deg); - margin-top: -7px; - margin-left: -6px; - } + .svg-pointer { + position: absolute; + height: 12px; + width: 12px; + border-radius: 2px; + background: var(--charts-tooltip-bg); + transform: rotate(45deg); + margin-top: -7px; + margin-left: -6px; + } - &.comparison { - text-align: left; - padding: 0px; - pointer-events: none; + &.comparison { + text-align: left; + padding: 0px; + pointer-events: none; - .title { - display: block; - padding: 16px; - margin: 0; - color: var(--charts-tooltip-title); - font-weight: 600; - line-height: 1; - pointer-events: none; - text-transform: uppercase; + .title { + display: block; + padding: 16px; + margin: 0; + color: var(--charts-tooltip-title); + font-weight: 600; + line-height: 1; + pointer-events: none; + text-transform: uppercase; - strong { - color: var(--charts-tooltip-value); - } - } + strong { + color: var(--charts-tooltip-value); + } + } - ul { - margin: 0; - white-space: nowrap; - list-style: none; + ul { + margin: 0; + white-space: nowrap; + list-style: none; - &.tooltip-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 5px; - } - } + &.tooltip-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 5px; + } + } - li { - display: inline-block; - display: flex; - flex-direction: row; - font-weight: 600; - line-height: 1; + li { + display: inline-block; + display: flex; + flex-direction: row; + font-weight: 600; + line-height: 1; - padding: 5px 15px 15px 15px; + padding: 5px 15px 15px 15px; - .tooltip-legend { - height: 12px; - width: 12px; - margin-right: 8px; - border-radius: 2px; - } + .tooltip-legend { + height: 12px; + width: 12px; + margin-right: 8px; + border-radius: 2px; + } - .tooltip-label { - margin-top: 4px; - font-size: 11px; - line-height: 1.25; - max-width: 150px; - white-space: normal; + .tooltip-label { + margin-top: 4px; + font-size: 11px; + line-height: 1.25; + max-width: 150px; + white-space: normal; - color: var(--charts-tooltip-label); - } + color: var(--charts-tooltip-label); + } - .tooltip-value { - color: var(--charts-tooltip-value); - } - } - } -} \ No newline at end of file + .tooltip-value { + color: var(--charts-tooltip-value); + } + } + } +} diff --git a/src/css/chartsCss.js b/src/css/chartsCss.js index cf93ce5..da0d5b7 100644 --- a/src/css/chartsCss.js +++ b/src/css/chartsCss.js @@ -1 +1,2 @@ -export const CSSTEXT = ".chart-container{position:relative;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif}.chart-container .axis,.chart-container .chart-label{fill:#555b51}.chart-container .axis line,.chart-container .chart-label line{stroke:#dadada}.chart-container .dataset-units circle{stroke:#fff;stroke-width:2}.chart-container .dataset-units path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container .dataset-path{stroke-width:2px}.chart-container .path-group path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container line.dashed{stroke-dasharray:5,3}.chart-container .axis-line .specific-value{text-anchor:start}.chart-container .axis-line .y-line{text-anchor:end}.chart-container .axis-line .x-line{text-anchor:middle}.chart-container .legend-dataset-text{fill:#6c7680;font-weight:600}.graph-svg-tip{position:absolute;z-index:99999;padding:10px;font-size:12px;color:#959da5;text-align:center;background:rgba(0,0,0,.8);border-radius:3px}.graph-svg-tip ul{padding-left:0;display:flex}.graph-svg-tip ol{padding-left:0;display:flex}.graph-svg-tip ul.data-point-list li{min-width:90px;flex:1;font-weight:600}.graph-svg-tip strong{color:#dfe2e5;font-weight:600}.graph-svg-tip .svg-pointer{position:absolute;height:5px;margin:0 0 0 -5px;content:' ';border:5px solid transparent;}.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}"; \ No newline at end of file +export const CSSTEXT = + ".chart-container{position:relative;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Roboto','Oxygen','Ubuntu','Cantarell','Fira Sans','Droid Sans','Helvetica Neue',sans-serif}.chart-container .axis,.chart-container .chart-label{fill:#555b51}.chart-container .axis line,.chart-container .chart-label line{stroke:#dadada}.chart-container .dataset-units circle{stroke:#fff;stroke-width:2}.chart-container .dataset-units path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container .dataset-path{stroke-width:2px}.chart-container .path-group path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container line.dashed{stroke-dasharray:5,3}.chart-container .axis-line .specific-value{text-anchor:start}.chart-container .axis-line .y-line{text-anchor:end}.chart-container .axis-line .x-line{text-anchor:middle}.chart-container .legend-dataset-text{fill:#6c7680;font-weight:600}.graph-svg-tip{position:absolute;z-index:99999;padding:10px;font-size:12px;color:#959da5;text-align:center;background:rgba(0,0,0,.8);border-radius:3px}.graph-svg-tip ul{padding-left:0;display:flex}.graph-svg-tip ol{padding-left:0;display:flex}.graph-svg-tip ul.data-point-list li{min-width:90px;flex:1;font-weight:600}.graph-svg-tip strong{color:#dfe2e5;font-weight:600}.graph-svg-tip .svg-pointer{position:absolute;height:5px;margin:0 0 0 -5px;content:' ';border:5px solid transparent;}.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}"; diff --git a/src/js/chart.js b/src/js/chart.js index 6606de3..be22d68 100644 --- a/src/js/chart.js +++ b/src/js/chart.js @@ -1,40 +1,40 @@ -import '../css/charts.scss'; +import "../css/charts.scss"; // import MultiAxisChart from './charts/MultiAxisChart'; -import PercentageChart from './charts/PercentageChart'; -import PieChart from './charts/PieChart'; -import Heatmap from './charts/Heatmap'; -import AxisChart from './charts/AxisChart'; -import DonutChart from './charts/DonutChart'; +import PercentageChart from "./charts/PercentageChart"; +import PieChart from "./charts/PieChart"; +import Heatmap from "./charts/Heatmap"; +import AxisChart from "./charts/AxisChart"; +import DonutChart from "./charts/DonutChart"; const chartTypes = { - bar: AxisChart, - line: AxisChart, - // multiaxis: MultiAxisChart, - percentage: PercentageChart, - heatmap: Heatmap, - pie: PieChart, - donut: DonutChart, + bar: AxisChart, + line: AxisChart, + // multiaxis: MultiAxisChart, + percentage: PercentageChart, + heatmap: Heatmap, + pie: PieChart, + donut: DonutChart, }; -function getChartByType(chartType = 'line', parent, options) { - if (chartType === 'axis-mixed') { - options.type = 'line'; - return new AxisChart(parent, options); - } +function getChartByType(chartType = "line", parent, options) { + if (chartType === "axis-mixed") { + options.type = "line"; + return new AxisChart(parent, options); + } - if (!chartTypes[chartType]) { - console.error("Undefined chart type: " + chartType); - return; - } + if (!chartTypes[chartType]) { + console.error("Undefined chart type: " + chartType); + return; + } - return new chartTypes[chartType](parent, options); + return new chartTypes[chartType](parent, options); } class Chart { - constructor(parent, options) { - return getChartByType(options.type, parent, options); - } + constructor(parent, options) { + return getChartByType(options.type, parent, options); + } } -export { Chart, PercentageChart, PieChart, Heatmap, AxisChart }; \ No newline at end of file +export { Chart, PercentageChart, PieChart, Heatmap, AxisChart }; diff --git a/src/js/charts/AggregationChart.js b/src/js/charts/AggregationChart.js index c5d146a..1af47d3 100644 --- a/src/js/charts/AggregationChart.js +++ b/src/js/charts/AggregationChart.js @@ -1,97 +1,109 @@ -import BaseChart from './BaseChart'; -import { truncateString } from '../utils/draw-utils'; -import { legendDot } from '../utils/draw'; -import { round } from '../utils/helpers'; -import { getExtraWidth } from '../utils/constants'; +import BaseChart from "./BaseChart"; +import { truncateString } from "../utils/draw-utils"; +import { legendDot } from "../utils/draw"; +import { round } from "../utils/helpers"; +import { getExtraWidth } from "../utils/constants"; export default class AggregationChart extends BaseChart { - constructor(parent, args) { - super(parent, args); - } + constructor(parent, args) { + super(parent, args); + } - configure(args) { - super.configure(args); + configure(args) { + super.configure(args); - this.config.formatTooltipY = (args.tooltipOptions || {}).formatTooltipY; - this.config.maxSlices = args.maxSlices || 20; - this.config.maxLegendPoints = args.maxLegendPoints || 20; - } + this.config.formatTooltipY = (args.tooltipOptions || {}).formatTooltipY; + this.config.maxSlices = args.maxSlices || 20; + this.config.maxLegendPoints = args.maxLegendPoints || 20; + } - calc() { - let s = this.state; - let maxSlices = this.config.maxSlices; - s.sliceTotals = []; + calc() { + let s = this.state; + let maxSlices = this.config.maxSlices; + s.sliceTotals = []; - let allTotals = this.data.labels.map((label, i) => { - let total = 0; - this.data.datasets.map(e => { - total += e.values[i]; - }); - return [total, label]; - }).filter(d => { return d[0] >= 0; }); // keep only positive results + let allTotals = this.data.labels + .map((label, i) => { + let total = 0; + this.data.datasets.map((e) => { + total += e.values[i]; + }); + return [total, label]; + }) + .filter((d) => { + return d[0] >= 0; + }); // keep only positive results - let totals = allTotals; - if(allTotals.length > maxSlices) { - // Prune and keep a grey area for rest as per maxSlices - allTotals.sort((a, b) => { return b[0] - a[0]; }); + let totals = allTotals; + if (allTotals.length > maxSlices) { + // Prune and keep a grey area for rest as per maxSlices + allTotals.sort((a, b) => { + return b[0] - a[0]; + }); - totals = allTotals.slice(0, maxSlices-1); - let remaining = allTotals.slice(maxSlices-1); + totals = allTotals.slice(0, maxSlices - 1); + let remaining = allTotals.slice(maxSlices - 1); - let sumOfRemaining = 0; - remaining.map(d => {sumOfRemaining += d[0];}); - totals.push([sumOfRemaining, 'Rest']); - this.colors[maxSlices-1] = 'grey'; - } + let sumOfRemaining = 0; + remaining.map((d) => { + sumOfRemaining += d[0]; + }); + totals.push([sumOfRemaining, "Rest"]); + this.colors[maxSlices - 1] = "grey"; + } - s.labels = []; - totals.map(d => { - s.sliceTotals.push(round(d[0])); - s.labels.push(d[1]); - }); + s.labels = []; + totals.map((d) => { + s.sliceTotals.push(round(d[0])); + s.labels.push(d[1]); + }); - s.grandTotal = s.sliceTotals.reduce((a, b) => a + b, 0); + s.grandTotal = s.sliceTotals.reduce((a, b) => a + b, 0); - this.center = { - x: this.width / 2, - y: this.height / 2 - }; - } + this.center = { + x: this.width / 2, + y: this.height / 2, + }; + } - renderLegend() { - let s = this.state; - this.legendArea.textContent = ''; - this.legendTotals = s.sliceTotals.slice(0, this.config.maxLegendPoints); + renderLegend() { + let s = this.state; + this.legendArea.textContent = ""; + this.legendTotals = s.sliceTotals.slice(0, this.config.maxLegendPoints); - let count = 0; - let y = 0; - this.legendTotals.map((d, i) => { - let barWidth = 150; - let divisor = Math.floor( - (this.width - getExtraWidth(this.measures))/barWidth - ); - if (this.legendTotals.length < divisor) { - barWidth = this.width/this.legendTotals.length; - } - if(count > divisor) { - count = 0; - y += 60; - } - let x = barWidth * count + 5; - let label = this.config.truncateLegends ? truncateString(s.labels[i], barWidth/10) : s.labels[i]; - let formatted = this.config.formatTooltipY ? this.config.formatTooltipY(d) : d; - let dot = legendDot( - x, - y, - 12, - 3, - this.colors[i], - `${label}: ${formatted}`, - d, - false - ); - this.legendArea.appendChild(dot); - count++; - }); - } + let count = 0; + let y = 0; + this.legendTotals.map((d, i) => { + let barWidth = 150; + let divisor = Math.floor( + (this.width - getExtraWidth(this.measures)) / barWidth + ); + if (this.legendTotals.length < divisor) { + barWidth = this.width / this.legendTotals.length; + } + if (count > divisor) { + count = 0; + y += 60; + } + let x = barWidth * count + 5; + let label = this.config.truncateLegends + ? truncateString(s.labels[i], barWidth / 10) + : s.labels[i]; + let formatted = this.config.formatTooltipY + ? this.config.formatTooltipY(d) + : d; + let dot = legendDot( + x, + y, + 12, + 3, + this.colors[i], + `${label}: ${formatted}`, + d, + false + ); + this.legendArea.appendChild(dot); + count++; + }); + } } diff --git a/src/js/charts/AxisChart.js b/src/js/charts/AxisChart.js index 9c06bbd..3e3da20 100644 --- a/src/js/charts/AxisChart.js +++ b/src/js/charts/AxisChart.js @@ -1,590 +1,618 @@ -import BaseChart from './BaseChart'; -import { dataPrep, zeroDataPrep, getShortenedLabels } from '../utils/axis-chart-utils'; -import { AXIS_LEGEND_BAR_SIZE } from '../utils/constants'; -import { getComponent } from '../objects/ChartComponents'; -import { getOffset, fire } from '../utils/dom'; -import { calcChartIntervals, getIntervalSize, getValueRange, getZeroIndex, scale, getClosestInArray } from '../utils/intervals'; -import { floatTwo } from '../utils/helpers'; -import { makeOverlay, updateOverlay, legendBar } from '../utils/draw'; -import { getTopOffset, getLeftOffset, MIN_BAR_PERCENT_HEIGHT, BAR_CHART_SPACE_RATIO, - LINE_CHART_DOT_SIZE } from '../utils/constants'; +import BaseChart from "./BaseChart"; +import { + dataPrep, + zeroDataPrep, + getShortenedLabels, +} from "../utils/axis-chart-utils"; +import { AXIS_LEGEND_BAR_SIZE } from "../utils/constants"; +import { getComponent } from "../objects/ChartComponents"; +import { getOffset, fire } from "../utils/dom"; +import { + calcChartIntervals, + getIntervalSize, + getValueRange, + getZeroIndex, + scale, + getClosestInArray, +} from "../utils/intervals"; +import { floatTwo } from "../utils/helpers"; +import { makeOverlay, updateOverlay, legendBar } from "../utils/draw"; +import { + getTopOffset, + getLeftOffset, + MIN_BAR_PERCENT_HEIGHT, + BAR_CHART_SPACE_RATIO, + LINE_CHART_DOT_SIZE, +} from "../utils/constants"; export default class AxisChart extends BaseChart { - constructor(parent, args) { - super(parent, args); - - this.barOptions = args.barOptions || {}; - this.lineOptions = args.lineOptions || {}; - - this.type = args.type || 'line'; - this.init = 1; - - this.setup(); - } - - setMeasures() { - if(this.data.datasets.length <= 1) { - this.config.showLegend = 0; - this.measures.paddings.bottom = 30; - } - } - - configure(options) { - super.configure(options); - - options.axisOptions = options.axisOptions || {}; - options.tooltipOptions = options.tooltipOptions || {}; - - this.config.xAxisMode = options.axisOptions.xAxisMode || 'span'; - this.config.yAxisMode = options.axisOptions.yAxisMode || 'span'; - this.config.xIsSeries = options.axisOptions.xIsSeries || 0; - this.config.shortenYAxisNumbers = options.axisOptions.shortenYAxisNumbers || 0; - - this.config.formatTooltipX = options.tooltipOptions.formatTooltipX; - this.config.formatTooltipY = options.tooltipOptions.formatTooltipY; - - this.config.valuesOverPoints = options.valuesOverPoints; - } - - prepareData(data=this.data) { - return dataPrep(data, this.type); - } - - prepareFirstData(data=this.data) { - return zeroDataPrep(data); - } - - calc(onlyWidthChange = false) { - this.calcXPositions(); - if(!onlyWidthChange) { - this.calcYAxisParameters(this.getAllYValues(), this.type === 'line'); - } - this.makeDataByIndex(); - } - - calcXPositions() { - let s = this.state; - let labels = this.data.labels; - s.datasetLength = labels.length; - - s.unitWidth = this.width/(s.datasetLength); - // Default, as per bar, and mixed. Only line will be a special case - s.xOffset = s.unitWidth/2; - - // // For a pure Line Chart - // s.unitWidth = this.width/(s.datasetLength - 1); - // s.xOffset = 0; - - s.xAxis = { - labels: labels, - positions: labels.map((d, i) => - floatTwo(s.xOffset + i * s.unitWidth) - ) - }; - } - - calcYAxisParameters(dataValues, withMinimum = 'false') { - const yPts = calcChartIntervals(dataValues, withMinimum); - const scaleMultiplier = this.height / getValueRange(yPts); - const intervalHeight = getIntervalSize(yPts) * scaleMultiplier; - const zeroLine = this.height - (getZeroIndex(yPts) * intervalHeight); - - this.state.yAxis = { - labels: yPts, - positions: yPts.map(d => zeroLine - d * scaleMultiplier), - scaleMultiplier: scaleMultiplier, - zeroLine: zeroLine, - }; - - // Dependent if above changes - this.calcDatasetPoints(); - this.calcYExtremes(); - this.calcYRegions(); - } - - calcDatasetPoints() { - let s = this.state; - let scaleAll = values => values.map(val => scale(val, s.yAxis)); - - s.datasets = this.data.datasets.map((d, i) => { - let values = d.values; - let cumulativeYs = d.cumulativeYs || []; - return { - name: d.name && d.name.replace(/<|>|&/g, (char) => char == '&' ? '&' : char == '<' ? '<' : '>'), - index: i, - chartType: d.chartType, - - values: values, - yPositions: scaleAll(values), - - cumulativeYs: cumulativeYs, - cumulativeYPos: scaleAll(cumulativeYs), - }; - }); - } - - calcYExtremes() { - let s = this.state; - if(this.barOptions.stacked) { - s.yExtremes = s.datasets[s.datasets.length - 1].cumulativeYPos; - return; - } - s.yExtremes = new Array(s.datasetLength).fill(9999); - s.datasets.map(d => { - d.yPositions.map((pos, j) => { - if(pos < s.yExtremes[j]) { - s.yExtremes[j] = pos; - } - }); - }); - } - - calcYRegions() { - let s = this.state; - if(this.data.yMarkers) { - this.state.yMarkers = this.data.yMarkers.map(d => { - d.position = scale(d.value, s.yAxis); - if(!d.options) d.options = {}; - // if(!d.label.includes(':')) { - // d.label += ': ' + d.value; - // } - return d; - }); - } - if(this.data.yRegions) { - this.state.yRegions = this.data.yRegions.map(d => { - d.startPos = scale(d.start, s.yAxis); - d.endPos = scale(d.end, s.yAxis); - if(!d.options) d.options = {}; - return d; - }); - } - } - - getAllYValues() { - let key = 'values'; - - if(this.barOptions.stacked) { - key = 'cumulativeYs'; - let cumulative = new Array(this.state.datasetLength).fill(0); - this.data.datasets.map((d, i) => { - let values = this.data.datasets[i].values; - d[key] = cumulative = cumulative.map((c, i) => c + values[i]); - }); - } - - let allValueLists = this.data.datasets.map(d => d[key]); - if(this.data.yMarkers) { - allValueLists.push(this.data.yMarkers.map(d => d.value)); - } - if(this.data.yRegions) { - this.data.yRegions.map(d => { - allValueLists.push([d.end, d.start]); - }); - } - - return [].concat(...allValueLists); - } - - setupComponents() { - let componentConfigs = [ - [ - 'yAxis', - { - mode: this.config.yAxisMode, - width: this.width, - shortenNumbers: this.config.shortenYAxisNumbers - // pos: 'right' - }, - function() { - return this.state.yAxis; - }.bind(this) - ], - - [ - 'xAxis', - { - mode: this.config.xAxisMode, - height: this.height, - // pos: 'right' - }, - function() { - let s = this.state; - s.xAxis.calcLabels = getShortenedLabels(this.width, - s.xAxis.labels, this.config.xIsSeries); - - return s.xAxis; - }.bind(this) - ], - - [ - 'yRegions', - { - width: this.width, - pos: 'right' - }, - function() { - return this.state.yRegions; - }.bind(this) - ], - ]; - - let barDatasets = this.state.datasets.filter(d => d.chartType === 'bar'); - let lineDatasets = this.state.datasets.filter(d => d.chartType === 'line'); - - let barsConfigs = barDatasets.map(d => { - let index = d.index; - return [ - 'barGraph' + '-' + d.index, - { - index: index, - color: this.colors[index], - stacked: this.barOptions.stacked, - - // same for all datasets - valuesOverPoints: this.config.valuesOverPoints, - minHeight: this.height * MIN_BAR_PERCENT_HEIGHT, - }, - function() { - let s = this.state; - let d = s.datasets[index]; - let stacked = this.barOptions.stacked; - - let spaceRatio = this.barOptions.spaceRatio || BAR_CHART_SPACE_RATIO; - let barsWidth = s.unitWidth * (1 - spaceRatio); - let barWidth = barsWidth/(stacked ? 1 : barDatasets.length); - - let xPositions = s.xAxis.positions.map(x => x - barsWidth/2); - if(!stacked) { - xPositions = xPositions.map(p => p + barWidth * index); - } - - let labels = new Array(s.datasetLength).fill(''); - if(this.config.valuesOverPoints) { - if(stacked && d.index === s.datasets.length - 1) { - labels = d.cumulativeYs; - } else { - labels = d.values; - } - } - - let offsets = new Array(s.datasetLength).fill(0); - if(stacked) { - offsets = d.yPositions.map((y, j) => y - d.cumulativeYPos[j]); - } - - return { - xPositions: xPositions, - yPositions: d.yPositions, - offsets: offsets, - // values: d.values, - labels: labels, - - zeroLine: s.yAxis.zeroLine, - barsWidth: barsWidth, - barWidth: barWidth, - }; - }.bind(this) - ]; - }); - - let lineConfigs = lineDatasets.map(d => { - let index = d.index; - return [ - 'lineGraph' + '-' + d.index, - { - index: index, - color: this.colors[index], - svgDefs: this.svgDefs, - heatline: this.lineOptions.heatline, - regionFill: this.lineOptions.regionFill, - spline: this.lineOptions.spline, - hideDots: this.lineOptions.hideDots, - hideLine: this.lineOptions.hideLine, - - // same for all datasets - valuesOverPoints: this.config.valuesOverPoints, - }, - function() { - let s = this.state; - let d = s.datasets[index]; - let minLine = s.yAxis.positions[0] < s.yAxis.zeroLine - ? s.yAxis.positions[0] : s.yAxis.zeroLine; - - return { - xPositions: s.xAxis.positions, - yPositions: d.yPositions, - - values: d.values, - - zeroLine: minLine, - radius: this.lineOptions.dotSize || LINE_CHART_DOT_SIZE, - }; - }.bind(this) - ]; - }); - - let markerConfigs = [ - [ - 'yMarkers', - { - width: this.width, - pos: 'right' - }, - function() { - return this.state.yMarkers; - }.bind(this) - ] - ]; - - componentConfigs = componentConfigs.concat(barsConfigs, lineConfigs, markerConfigs); - - let optionals = ['yMarkers', 'yRegions']; - this.dataUnitComponents = []; - - this.components = new Map(componentConfigs - .filter(args => !optionals.includes(args[0]) || this.state[args[0]]) - .map(args => { - let component = getComponent(...args); - if(args[0].includes('lineGraph') || args[0].includes('barGraph')) { - this.dataUnitComponents.push(component); - } - return [args[0], component]; - })); - } - - makeDataByIndex() { - this.dataByIndex = {}; - - let s = this.state; - let formatX = this.config.formatTooltipX; - let formatY = this.config.formatTooltipY; - let titles = s.xAxis.labels; - - titles.map((label, index) => { - let values = this.state.datasets.map((set, i) => { - let value = set.values[index]; - return { - title: set.name, - value: value, - yPos: set.yPositions[index], - color: this.colors[i], - formatted: formatY ? formatY(value) : value, - }; - }); - - this.dataByIndex[index] = { - label: label, - formattedLabel: formatX ? formatX(label) : label, - xPos: s.xAxis.positions[index], - values: values, - yExtreme: s.yExtremes[index], - }; - }); - } - - bindTooltip() { - // NOTE: could be in tooltip itself, as it is a given functionality for its parent - this.container.addEventListener('mousemove', (e) => { - let m = this.measures; - let o = getOffset(this.container); - let relX = e.pageX - o.left - getLeftOffset(m); - let relY = e.pageY - o.top; - - if(relY < this.height + getTopOffset(m) - && relY > getTopOffset(m)) { - this.mapTooltipXPosition(relX); - } else { - this.tip.hideTip(); - } - }); - } - - mapTooltipXPosition(relX) { - let s = this.state; - if(!s.yExtremes) return; - - let index = getClosestInArray(relX, s.xAxis.positions, true); - if (index >= 0) { - let dbi = this.dataByIndex[index]; - - this.tip.setValues( - dbi.xPos + this.tip.offset.x, - dbi.yExtreme + this.tip.offset.y, - {name: dbi.formattedLabel, value: ''}, - dbi.values, - index - ); - - this.tip.showTip(); - } - } - - renderLegend() { - let s = this.data; - if(s.datasets.length > 1) { - this.legendArea.textContent = ''; - s.datasets.map((d, i) => { - let barWidth = AXIS_LEGEND_BAR_SIZE; - // let rightEndPoint = this.baseWidth - this.measures.margins.left - this.measures.margins.right; - // let multiplier = s.datasets.length - i; - let rect = legendBar( - // rightEndPoint - multiplier * barWidth, // To right align - barWidth * i, - '0', - barWidth, - this.colors[i], - d.name, - this.config.truncateLegends); - this.legendArea.appendChild(rect); - }); - } - } - - - - // Overlay - makeOverlay() { - if(this.init) { - this.init = 0; - return; - } - if(this.overlayGuides) { - this.overlayGuides.forEach(g => { - let o = g.overlay; - o.parentNode.removeChild(o); - }); - } - - this.overlayGuides = this.dataUnitComponents.map(c => { - return { - type: c.unitType, - overlay: undefined, - units: c.units, - }; - }); - - if(this.state.currentIndex === undefined) { - this.state.currentIndex = this.state.datasetLength - 1; - } - - // Render overlays - this.overlayGuides.map(d => { - let currentUnit = d.units[this.state.currentIndex]; - - d.overlay = makeOverlay[d.type](currentUnit); - this.drawArea.appendChild(d.overlay); - }); - } - - updateOverlayGuides() { - if(this.overlayGuides) { - this.overlayGuides.forEach(g => { - let o = g.overlay; - o.parentNode.removeChild(o); - }); - } - } - - bindOverlay() { - this.parent.addEventListener('data-select', () => { - this.updateOverlay(); - }); - } - - bindUnits() { - this.dataUnitComponents.map(c => { - c.units.map(unit => { - unit.addEventListener('click', () => { - let index = unit.getAttribute('data-point-index'); - this.setCurrentDataPoint(index); - }); - }); - }); - - // Note: Doesn't work as tooltip is absolutely positioned - this.tip.container.addEventListener('click', () => { - let index = this.tip.container.getAttribute('data-point-index'); - this.setCurrentDataPoint(index); - }); - } - - updateOverlay() { - this.overlayGuides.map(d => { - let currentUnit = d.units[this.state.currentIndex]; - updateOverlay[d.type](currentUnit, d.overlay); - }); - } - - onLeftArrow() { - this.setCurrentDataPoint(this.state.currentIndex - 1); - } - - onRightArrow() { - this.setCurrentDataPoint(this.state.currentIndex + 1); - } - - getDataPoint(index=this.state.currentIndex) { - let s = this.state; - let data_point = { - index: index, - label: s.xAxis.labels[index], - values: s.datasets.map(d => d.values[index]) - }; - return data_point; - } - - setCurrentDataPoint(index) { - let s = this.state; - index = parseInt(index); - if(index < 0) index = 0; - if(index >= s.xAxis.labels.length) index = s.xAxis.labels.length - 1; - if(index === s.currentIndex) return; - s.currentIndex = index; - fire(this.parent, "data-select", this.getDataPoint()); - } - - - - // API - addDataPoint(label, datasetValues, index=this.state.datasetLength) { - super.addDataPoint(label, datasetValues, index); - this.data.labels.splice(index, 0, label); - this.data.datasets.map((d, i) => { - d.values.splice(index, 0, datasetValues[i]); - }); - this.update(this.data); - } - - removeDataPoint(index = this.state.datasetLength-1) { - if (this.data.labels.length <= 1) { - return; - } - super.removeDataPoint(index); - this.data.labels.splice(index, 1); - this.data.datasets.map(d => { - d.values.splice(index, 1); - }); - this.update(this.data); - } - - updateDataset(datasetValues, index=0) { - this.data.datasets[index].values = datasetValues; - this.update(this.data); - } - // addDataset(dataset, index) {} - // removeDataset(index = 0) {} - - updateDatasets(datasets) { - this.data.datasets.map((d, i) => { - if(datasets[i]) { - d.values = datasets[i]; - } - }); - this.update(this.data); - } - - // updateDataPoint(dataPoint, index = 0) {} - // addDataPoint(dataPoint, index = 0) {} - // removeDataPoint(index = 0) {} + constructor(parent, args) { + super(parent, args); + + this.barOptions = args.barOptions || {}; + this.lineOptions = args.lineOptions || {}; + + this.type = args.type || "line"; + this.init = 1; + + this.setup(); + } + + setMeasures() { + if (this.data.datasets.length <= 1) { + this.config.showLegend = 0; + this.measures.paddings.bottom = 30; + } + } + + configure(options) { + super.configure(options); + + options.axisOptions = options.axisOptions || {}; + options.tooltipOptions = options.tooltipOptions || {}; + + this.config.xAxisMode = options.axisOptions.xAxisMode || "span"; + this.config.yAxisMode = options.axisOptions.yAxisMode || "span"; + this.config.xIsSeries = options.axisOptions.xIsSeries || 0; + this.config.shortenYAxisNumbers = + options.axisOptions.shortenYAxisNumbers || 0; + + this.config.formatTooltipX = options.tooltipOptions.formatTooltipX; + this.config.formatTooltipY = options.tooltipOptions.formatTooltipY; + + this.config.valuesOverPoints = options.valuesOverPoints; + } + + prepareData(data = this.data) { + return dataPrep(data, this.type); + } + + prepareFirstData(data = this.data) { + return zeroDataPrep(data); + } + + calc(onlyWidthChange = false) { + this.calcXPositions(); + if (!onlyWidthChange) { + this.calcYAxisParameters(this.getAllYValues(), this.type === "line"); + } + this.makeDataByIndex(); + } + + calcXPositions() { + let s = this.state; + let labels = this.data.labels; + s.datasetLength = labels.length; + + s.unitWidth = this.width / s.datasetLength; + // Default, as per bar, and mixed. Only line will be a special case + s.xOffset = s.unitWidth / 2; + + // // For a pure Line Chart + // s.unitWidth = this.width/(s.datasetLength - 1); + // s.xOffset = 0; + + s.xAxis = { + labels: labels, + positions: labels.map((d, i) => floatTwo(s.xOffset + i * s.unitWidth)), + }; + } + + calcYAxisParameters(dataValues, withMinimum = "false") { + const yPts = calcChartIntervals(dataValues, withMinimum); + const scaleMultiplier = this.height / getValueRange(yPts); + const intervalHeight = getIntervalSize(yPts) * scaleMultiplier; + const zeroLine = this.height - getZeroIndex(yPts) * intervalHeight; + + this.state.yAxis = { + labels: yPts, + positions: yPts.map((d) => zeroLine - d * scaleMultiplier), + scaleMultiplier: scaleMultiplier, + zeroLine: zeroLine, + }; + + // Dependent if above changes + this.calcDatasetPoints(); + this.calcYExtremes(); + this.calcYRegions(); + } + + calcDatasetPoints() { + let s = this.state; + let scaleAll = (values) => values.map((val) => scale(val, s.yAxis)); + + s.datasets = this.data.datasets.map((d, i) => { + let values = d.values; + let cumulativeYs = d.cumulativeYs || []; + return { + name: + d.name && + d.name.replace(/<|>|&/g, (char) => + char == "&" ? "&" : char == "<" ? "<" : ">" + ), + index: i, + chartType: d.chartType, + + values: values, + yPositions: scaleAll(values), + + cumulativeYs: cumulativeYs, + cumulativeYPos: scaleAll(cumulativeYs), + }; + }); + } + + calcYExtremes() { + let s = this.state; + if (this.barOptions.stacked) { + s.yExtremes = s.datasets[s.datasets.length - 1].cumulativeYPos; + return; + } + s.yExtremes = new Array(s.datasetLength).fill(9999); + s.datasets.map((d) => { + d.yPositions.map((pos, j) => { + if (pos < s.yExtremes[j]) { + s.yExtremes[j] = pos; + } + }); + }); + } + + calcYRegions() { + let s = this.state; + if (this.data.yMarkers) { + this.state.yMarkers = this.data.yMarkers.map((d) => { + d.position = scale(d.value, s.yAxis); + if (!d.options) d.options = {}; + // if(!d.label.includes(':')) { + // d.label += ': ' + d.value; + // } + return d; + }); + } + if (this.data.yRegions) { + this.state.yRegions = this.data.yRegions.map((d) => { + d.startPos = scale(d.start, s.yAxis); + d.endPos = scale(d.end, s.yAxis); + if (!d.options) d.options = {}; + return d; + }); + } + } + + getAllYValues() { + let key = "values"; + + if (this.barOptions.stacked) { + key = "cumulativeYs"; + let cumulative = new Array(this.state.datasetLength).fill(0); + this.data.datasets.map((d, i) => { + let values = this.data.datasets[i].values; + d[key] = cumulative = cumulative.map((c, i) => c + values[i]); + }); + } + + let allValueLists = this.data.datasets.map((d) => d[key]); + if (this.data.yMarkers) { + allValueLists.push(this.data.yMarkers.map((d) => d.value)); + } + if (this.data.yRegions) { + this.data.yRegions.map((d) => { + allValueLists.push([d.end, d.start]); + }); + } + + return [].concat(...allValueLists); + } + + setupComponents() { + let componentConfigs = [ + [ + "yAxis", + { + mode: this.config.yAxisMode, + width: this.width, + shortenNumbers: this.config.shortenYAxisNumbers, + // pos: 'right' + }, + function () { + return this.state.yAxis; + }.bind(this), + ], + + [ + "xAxis", + { + mode: this.config.xAxisMode, + height: this.height, + // pos: 'right' + }, + function () { + let s = this.state; + s.xAxis.calcLabels = getShortenedLabels( + this.width, + s.xAxis.labels, + this.config.xIsSeries + ); + + return s.xAxis; + }.bind(this), + ], + + [ + "yRegions", + { + width: this.width, + pos: "right", + }, + function () { + return this.state.yRegions; + }.bind(this), + ], + ]; + + let barDatasets = this.state.datasets.filter((d) => d.chartType === "bar"); + let lineDatasets = this.state.datasets.filter( + (d) => d.chartType === "line" + ); + + let barsConfigs = barDatasets.map((d) => { + let index = d.index; + return [ + "barGraph" + "-" + d.index, + { + index: index, + color: this.colors[index], + stacked: this.barOptions.stacked, + + // same for all datasets + valuesOverPoints: this.config.valuesOverPoints, + minHeight: this.height * MIN_BAR_PERCENT_HEIGHT, + }, + function () { + let s = this.state; + let d = s.datasets[index]; + let stacked = this.barOptions.stacked; + + let spaceRatio = this.barOptions.spaceRatio || BAR_CHART_SPACE_RATIO; + let barsWidth = s.unitWidth * (1 - spaceRatio); + let barWidth = barsWidth / (stacked ? 1 : barDatasets.length); + + let xPositions = s.xAxis.positions.map((x) => x - barsWidth / 2); + if (!stacked) { + xPositions = xPositions.map((p) => p + barWidth * index); + } + + let labels = new Array(s.datasetLength).fill(""); + if (this.config.valuesOverPoints) { + if (stacked && d.index === s.datasets.length - 1) { + labels = d.cumulativeYs; + } else { + labels = d.values; + } + } + + let offsets = new Array(s.datasetLength).fill(0); + if (stacked) { + offsets = d.yPositions.map((y, j) => y - d.cumulativeYPos[j]); + } + + return { + xPositions: xPositions, + yPositions: d.yPositions, + offsets: offsets, + // values: d.values, + labels: labels, + + zeroLine: s.yAxis.zeroLine, + barsWidth: barsWidth, + barWidth: barWidth, + }; + }.bind(this), + ]; + }); + + let lineConfigs = lineDatasets.map((d) => { + let index = d.index; + return [ + "lineGraph" + "-" + d.index, + { + index: index, + color: this.colors[index], + svgDefs: this.svgDefs, + heatline: this.lineOptions.heatline, + regionFill: this.lineOptions.regionFill, + spline: this.lineOptions.spline, + hideDots: this.lineOptions.hideDots, + hideLine: this.lineOptions.hideLine, + + // same for all datasets + valuesOverPoints: this.config.valuesOverPoints, + }, + function () { + let s = this.state; + let d = s.datasets[index]; + let minLine = + s.yAxis.positions[0] < s.yAxis.zeroLine + ? s.yAxis.positions[0] + : s.yAxis.zeroLine; + + return { + xPositions: s.xAxis.positions, + yPositions: d.yPositions, + + values: d.values, + + zeroLine: minLine, + radius: this.lineOptions.dotSize || LINE_CHART_DOT_SIZE, + }; + }.bind(this), + ]; + }); + + let markerConfigs = [ + [ + "yMarkers", + { + width: this.width, + pos: "right", + }, + function () { + return this.state.yMarkers; + }.bind(this), + ], + ]; + + componentConfigs = componentConfigs.concat( + barsConfigs, + lineConfigs, + markerConfigs + ); + + let optionals = ["yMarkers", "yRegions"]; + this.dataUnitComponents = []; + + this.components = new Map( + componentConfigs + .filter((args) => !optionals.includes(args[0]) || this.state[args[0]]) + .map((args) => { + let component = getComponent(...args); + if (args[0].includes("lineGraph") || args[0].includes("barGraph")) { + this.dataUnitComponents.push(component); + } + return [args[0], component]; + }) + ); + } + + makeDataByIndex() { + this.dataByIndex = {}; + + let s = this.state; + let formatX = this.config.formatTooltipX; + let formatY = this.config.formatTooltipY; + let titles = s.xAxis.labels; + + titles.map((label, index) => { + let values = this.state.datasets.map((set, i) => { + let value = set.values[index]; + return { + title: set.name, + value: value, + yPos: set.yPositions[index], + color: this.colors[i], + formatted: formatY ? formatY(value) : value, + }; + }); + + this.dataByIndex[index] = { + label: label, + formattedLabel: formatX ? formatX(label) : label, + xPos: s.xAxis.positions[index], + values: values, + yExtreme: s.yExtremes[index], + }; + }); + } + + bindTooltip() { + // NOTE: could be in tooltip itself, as it is a given functionality for its parent + this.container.addEventListener("mousemove", (e) => { + let m = this.measures; + let o = getOffset(this.container); + let relX = e.pageX - o.left - getLeftOffset(m); + let relY = e.pageY - o.top; + + if (relY < this.height + getTopOffset(m) && relY > getTopOffset(m)) { + this.mapTooltipXPosition(relX); + } else { + this.tip.hideTip(); + } + }); + } + + mapTooltipXPosition(relX) { + let s = this.state; + if (!s.yExtremes) return; + + let index = getClosestInArray(relX, s.xAxis.positions, true); + if (index >= 0) { + let dbi = this.dataByIndex[index]; + + this.tip.setValues( + dbi.xPos + this.tip.offset.x, + dbi.yExtreme + this.tip.offset.y, + { name: dbi.formattedLabel, value: "" }, + dbi.values, + index + ); + + this.tip.showTip(); + } + } + + renderLegend() { + let s = this.data; + if (s.datasets.length > 1) { + this.legendArea.textContent = ""; + s.datasets.map((d, i) => { + let barWidth = AXIS_LEGEND_BAR_SIZE; + // let rightEndPoint = this.baseWidth - this.measures.margins.left - this.measures.margins.right; + // let multiplier = s.datasets.length - i; + let rect = legendBar( + // rightEndPoint - multiplier * barWidth, // To right align + barWidth * i, + "0", + barWidth, + this.colors[i], + d.name, + this.config.truncateLegends + ); + this.legendArea.appendChild(rect); + }); + } + } + + // Overlay + makeOverlay() { + if (this.init) { + this.init = 0; + return; + } + if (this.overlayGuides) { + this.overlayGuides.forEach((g) => { + let o = g.overlay; + o.parentNode.removeChild(o); + }); + } + + this.overlayGuides = this.dataUnitComponents.map((c) => { + return { + type: c.unitType, + overlay: undefined, + units: c.units, + }; + }); + + if (this.state.currentIndex === undefined) { + this.state.currentIndex = this.state.datasetLength - 1; + } + + // Render overlays + this.overlayGuides.map((d) => { + let currentUnit = d.units[this.state.currentIndex]; + + d.overlay = makeOverlay[d.type](currentUnit); + this.drawArea.appendChild(d.overlay); + }); + } + + updateOverlayGuides() { + if (this.overlayGuides) { + this.overlayGuides.forEach((g) => { + let o = g.overlay; + o.parentNode.removeChild(o); + }); + } + } + + bindOverlay() { + this.parent.addEventListener("data-select", () => { + this.updateOverlay(); + }); + } + + bindUnits() { + this.dataUnitComponents.map((c) => { + c.units.map((unit) => { + unit.addEventListener("click", () => { + let index = unit.getAttribute("data-point-index"); + this.setCurrentDataPoint(index); + }); + }); + }); + + // Note: Doesn't work as tooltip is absolutely positioned + this.tip.container.addEventListener("click", () => { + let index = this.tip.container.getAttribute("data-point-index"); + this.setCurrentDataPoint(index); + }); + } + + updateOverlay() { + this.overlayGuides.map((d) => { + let currentUnit = d.units[this.state.currentIndex]; + updateOverlay[d.type](currentUnit, d.overlay); + }); + } + + onLeftArrow() { + this.setCurrentDataPoint(this.state.currentIndex - 1); + } + + onRightArrow() { + this.setCurrentDataPoint(this.state.currentIndex + 1); + } + + getDataPoint(index = this.state.currentIndex) { + let s = this.state; + let data_point = { + index: index, + label: s.xAxis.labels[index], + values: s.datasets.map((d) => d.values[index]), + }; + return data_point; + } + + setCurrentDataPoint(index) { + let s = this.state; + index = parseInt(index); + if (index < 0) index = 0; + if (index >= s.xAxis.labels.length) index = s.xAxis.labels.length - 1; + if (index === s.currentIndex) return; + s.currentIndex = index; + fire(this.parent, "data-select", this.getDataPoint()); + } + + // API + addDataPoint(label, datasetValues, index = this.state.datasetLength) { + super.addDataPoint(label, datasetValues, index); + this.data.labels.splice(index, 0, label); + this.data.datasets.map((d, i) => { + d.values.splice(index, 0, datasetValues[i]); + }); + this.update(this.data); + } + + removeDataPoint(index = this.state.datasetLength - 1) { + if (this.data.labels.length <= 1) { + return; + } + super.removeDataPoint(index); + this.data.labels.splice(index, 1); + this.data.datasets.map((d) => { + d.values.splice(index, 1); + }); + this.update(this.data); + } + + updateDataset(datasetValues, index = 0) { + this.data.datasets[index].values = datasetValues; + this.update(this.data); + } + // addDataset(dataset, index) {} + // removeDataset(index = 0) {} + + updateDatasets(datasets) { + this.data.datasets.map((d, i) => { + if (datasets[i]) { + d.values = datasets[i]; + } + }); + this.update(this.data); + } + + // updateDataPoint(dataPoint, index = 0) {} + // addDataPoint(dataPoint, index = 0) {} + // removeDataPoint(index = 0) {} } diff --git a/src/js/charts/BaseChart.js b/src/js/charts/BaseChart.js index e6124ad..da89a96 100644 --- a/src/js/charts/BaseChart.js +++ b/src/js/charts/BaseChart.js @@ -1,25 +1,25 @@ import SvgTip from "../objects/SvgTip"; import { - $, - isElementInViewport, - getElementContentWidth, - isHidden, + $, + isElementInViewport, + getElementContentWidth, + isHidden, } from "../utils/dom"; import { - makeSVGContainer, - makeSVGDefs, - makeSVGGroup, - makeText, + makeSVGContainer, + makeSVGDefs, + makeSVGGroup, + makeText, } from "../utils/draw"; import { - BASE_MEASURES, - getExtraHeight, - getExtraWidth, - getTopOffset, - getLeftOffset, - INIT_CHART_UPDATE_TIMEOUT, - CHART_POST_ANIMATE_TIMEOUT, - DEFAULT_COLORS, + BASE_MEASURES, + getExtraHeight, + getExtraWidth, + getTopOffset, + getLeftOffset, + INIT_CHART_UPDATE_TIMEOUT, + CHART_POST_ANIMATE_TIMEOUT, + DEFAULT_COLORS, } from "../utils/constants"; import { getColor, isValidColor } from "../utils/colors"; import { runSMILAnimation } from "../utils/animation"; @@ -27,330 +27,330 @@ import { downloadFile, prepareForExport } from "../utils/export"; import { deepClone } from "../utils/helpers"; export default class BaseChart { - constructor(parent, options) { - // deepclone options to avoid making changes to orignal object - options = deepClone(options); + constructor(parent, options) { + // deepclone options to avoid making changes to orignal object + options = deepClone(options); - this.parent = - typeof parent === "string" ? document.querySelector(parent) : parent; + this.parent = + typeof parent === "string" ? document.querySelector(parent) : parent; - if (!(this.parent instanceof HTMLElement)) { - throw new Error("No `parent` element to render on was provided."); - } + if (!(this.parent instanceof HTMLElement)) { + throw new Error("No `parent` element to render on was provided."); + } - this.rawChartArgs = options; + this.rawChartArgs = options; - this.title = options.title || ""; - this.type = options.type || ""; + this.title = options.title || ""; + this.type = options.type || ""; - this.realData = this.prepareData(options.data); - this.data = this.prepareFirstData(this.realData); + this.realData = this.prepareData(options.data); + this.data = this.prepareFirstData(this.realData); - this.colors = this.validateColors(options.colors, this.type); + this.colors = this.validateColors(options.colors, this.type); - this.config = { - showTooltip: 1, // calculate - showLegend: - typeof options.showLegend !== "undefined" ? options.showLegend : 1, - isNavigable: options.isNavigable || 0, - animate: typeof options.animate !== "undefined" ? options.animate : 1, - truncateLegends: - typeof options.truncateLegends !== "undefined" - ? options.truncateLegends - : 1, - }; + this.config = { + showTooltip: 1, // calculate + showLegend: + typeof options.showLegend !== "undefined" ? options.showLegend : 1, + isNavigable: options.isNavigable || 0, + animate: typeof options.animate !== "undefined" ? options.animate : 1, + truncateLegends: + typeof options.truncateLegends !== "undefined" + ? options.truncateLegends + : 1, + }; - this.measures = JSON.parse(JSON.stringify(BASE_MEASURES)); - let m = this.measures; - this.setMeasures(options); - if (!this.title.length) { - m.titleHeight = 0; - } - if (!this.config.showLegend) m.legendHeight = 0; - this.argHeight = options.height || m.baseHeight; + this.measures = JSON.parse(JSON.stringify(BASE_MEASURES)); + let m = this.measures; + this.setMeasures(options); + if (!this.title.length) { + m.titleHeight = 0; + } + if (!this.config.showLegend) m.legendHeight = 0; + this.argHeight = options.height || m.baseHeight; - this.state = {}; - this.options = {}; + this.state = {}; + this.options = {}; - this.initTimeout = INIT_CHART_UPDATE_TIMEOUT; + this.initTimeout = INIT_CHART_UPDATE_TIMEOUT; - if (this.config.isNavigable) { - this.overlays = []; - } + if (this.config.isNavigable) { + this.overlays = []; + } - this.configure(options); - } + this.configure(options); + } - prepareData(data) { - return data; - } + prepareData(data) { + return data; + } - prepareFirstData(data) { - return data; - } + prepareFirstData(data) { + return data; + } - validateColors(colors, type) { - const validColors = []; - colors = (colors || []).concat(DEFAULT_COLORS[type]); - colors.forEach((string) => { - const color = getColor(string); - if (!isValidColor(color)) { - console.warn('"' + string + '" is not a valid color.'); - } else { - validColors.push(color); - } - }); - return validColors; - } + validateColors(colors, type) { + const validColors = []; + colors = (colors || []).concat(DEFAULT_COLORS[type]); + colors.forEach((string) => { + const color = getColor(string); + if (!isValidColor(color)) { + console.warn('"' + string + '" is not a valid color.'); + } else { + validColors.push(color); + } + }); + return validColors; + } - setMeasures() { - // Override measures, including those for title and legend - // set config for legend and title - } + setMeasures() { + // Override measures, including those for title and legend + // set config for legend and title + } - configure() { - let height = this.argHeight; - this.baseHeight = height; - this.height = height - getExtraHeight(this.measures); + configure() { + let height = this.argHeight; + this.baseHeight = height; + this.height = height - getExtraHeight(this.measures); - // Bind window events - this.boundDrawFn = () => this.draw(true); - // Look into improving responsiveness - //if (ResizeObserver) { - // this.resizeObserver = new ResizeObserver(this.boundDrawFn); - // this.resizeObserver.observe(this.parent); - //} - window.addEventListener("resize", this.boundDrawFn); - window.addEventListener("orientationchange", this.boundDrawFn); - } + // Bind window events + this.boundDrawFn = () => this.draw(true); + // Look into improving responsiveness + //if (ResizeObserver) { + // this.resizeObserver = new ResizeObserver(this.boundDrawFn); + // this.resizeObserver.observe(this.parent); + //} + window.addEventListener("resize", this.boundDrawFn); + window.addEventListener("orientationchange", this.boundDrawFn); + } - destroy() { - //if (this.resizeObserver) this.resizeObserver.disconnect(); - window.removeEventListener("resize", this.boundDrawFn); - window.removeEventListener("orientationchange", this.boundDrawFn); - } + destroy() { + //if (this.resizeObserver) this.resizeObserver.disconnect(); + window.removeEventListener("resize", this.boundDrawFn); + window.removeEventListener("orientationchange", this.boundDrawFn); + } - // Has to be called manually - setup() { - this.makeContainer(); - this.updateWidth(); - this.makeTooltip(); + // Has to be called manually + setup() { + this.makeContainer(); + this.updateWidth(); + this.makeTooltip(); - this.draw(false, true); - } + this.draw(false, true); + } - makeContainer() { - // Chart needs a dedicated parent element - this.parent.innerHTML = ""; + makeContainer() { + // Chart needs a dedicated parent element + this.parent.innerHTML = ""; - let args = { - inside: this.parent, - className: "chart-container", - }; + let args = { + inside: this.parent, + className: "chart-container", + }; - if (this.independentWidth) { - args.styles = { width: this.independentWidth + "px" }; - } + if (this.independentWidth) { + args.styles = { width: this.independentWidth + "px" }; + } - this.container = $.create("div", args); - } + this.container = $.create("div", args); + } - makeTooltip() { - this.tip = new SvgTip({ - parent: this.container, - colors: this.colors, - }); - this.bindTooltip(); - } + makeTooltip() { + this.tip = new SvgTip({ + parent: this.container, + colors: this.colors, + }); + this.bindTooltip(); + } - bindTooltip() {} + bindTooltip() {} - draw(onlyWidthChange = false, init = false) { - if (onlyWidthChange && isHidden(this.parent)) { - // Don't update anything if the chart is hidden - return; - } - this.updateWidth(); + draw(onlyWidthChange = false, init = false) { + if (onlyWidthChange && isHidden(this.parent)) { + // Don't update anything if the chart is hidden + return; + } + this.updateWidth(); - this.calc(onlyWidthChange); - this.makeChartArea(); - this.setupComponents(); + this.calc(onlyWidthChange); + this.makeChartArea(); + this.setupComponents(); - this.components.forEach((c) => c.setup(this.drawArea)); - // this.components.forEach(c => c.make()); - this.render(this.components, false); + this.components.forEach((c) => c.setup(this.drawArea)); + // this.components.forEach(c => c.make()); + this.render(this.components, false); - if (init) { - this.data = this.realData; - setTimeout(() => { - this.update(this.data); - }, this.initTimeout); - } + if (init) { + this.data = this.realData; + setTimeout(() => { + this.update(this.data); + }, this.initTimeout); + } - this.renderLegend(); + this.renderLegend(); - this.setupNavigation(init); - } + this.setupNavigation(init); + } - calc() {} // builds state + calc() {} // builds state - updateWidth() { - this.baseWidth = getElementContentWidth(this.parent); - this.width = this.baseWidth - getExtraWidth(this.measures); - } + updateWidth() { + this.baseWidth = getElementContentWidth(this.parent); + this.width = this.baseWidth - getExtraWidth(this.measures); + } - makeChartArea() { - if (this.svg) { - this.container.removeChild(this.svg); - } - let m = this.measures; + makeChartArea() { + if (this.svg) { + this.container.removeChild(this.svg); + } + let m = this.measures; - this.svg = makeSVGContainer( - this.container, - "frappe-chart chart", - this.baseWidth, - this.baseHeight - ); - this.svgDefs = makeSVGDefs(this.svg); + this.svg = makeSVGContainer( + this.container, + "frappe-chart chart", + this.baseWidth, + this.baseHeight + ); + this.svgDefs = makeSVGDefs(this.svg); - if (this.title.length) { - this.titleEL = makeText( - "title", - m.margins.left, - m.margins.top, - this.title, - { - fontSize: m.titleFontSize, - fill: "#666666", - dy: m.titleFontSize, - } - ); - } + if (this.title.length) { + this.titleEL = makeText( + "title", + m.margins.left, + m.margins.top, + this.title, + { + fontSize: m.titleFontSize, + fill: "#666666", + dy: m.titleFontSize, + } + ); + } - let top = getTopOffset(m); - this.drawArea = makeSVGGroup( - this.type + "-chart chart-draw-area", - `translate(${getLeftOffset(m)}, ${top})` - ); + let top = getTopOffset(m); + this.drawArea = makeSVGGroup( + this.type + "-chart chart-draw-area", + `translate(${getLeftOffset(m)}, ${top})` + ); - if (this.config.showLegend) { - top += this.height + m.paddings.bottom; - this.legendArea = makeSVGGroup( - "chart-legend", - `translate(${getLeftOffset(m)}, ${top})` - ); - } + if (this.config.showLegend) { + top += this.height + m.paddings.bottom; + this.legendArea = makeSVGGroup( + "chart-legend", + `translate(${getLeftOffset(m)}, ${top})` + ); + } - if (this.title.length) { - this.svg.appendChild(this.titleEL); - } - this.svg.appendChild(this.drawArea); - if (this.config.showLegend) { - this.svg.appendChild(this.legendArea); - } + if (this.title.length) { + this.svg.appendChild(this.titleEL); + } + this.svg.appendChild(this.drawArea); + if (this.config.showLegend) { + this.svg.appendChild(this.legendArea); + } - this.updateTipOffset(getLeftOffset(m), getTopOffset(m)); - } + this.updateTipOffset(getLeftOffset(m), getTopOffset(m)); + } - updateTipOffset(x, y) { - this.tip.offset = { - x: x, - y: y, - }; - } + updateTipOffset(x, y) { + this.tip.offset = { + x: x, + y: y, + }; + } - setupComponents() { - this.components = new Map(); - } + setupComponents() { + this.components = new Map(); + } - update(data) { - if (!data) { - console.error("No data to update."); - } - this.data = this.prepareData(data); - this.calc(); // builds state - this.render(this.components, this.config.animate); - this.renderLegend(); - } + update(data) { + if (!data) { + console.error("No data to update."); + } + this.data = this.prepareData(data); + this.calc(); // builds state + this.render(this.components, this.config.animate); + this.renderLegend(); + } - render(components = this.components, animate = true) { - if (this.config.isNavigable) { - // Remove all existing overlays - this.overlays.map((o) => o.parentNode.removeChild(o)); - // ref.parentNode.insertBefore(element, ref); - } - let elementsToAnimate = []; - // Can decouple to this.refreshComponents() first to save animation timeout - components.forEach((c) => { - elementsToAnimate = elementsToAnimate.concat(c.update(animate)); - }); - if (elementsToAnimate.length > 0) { - runSMILAnimation(this.container, this.svg, elementsToAnimate); - setTimeout(() => { - components.forEach((c) => c.make()); - this.updateNav(); - }, CHART_POST_ANIMATE_TIMEOUT); - } else { - components.forEach((c) => c.make()); - this.updateNav(); - } - } + render(components = this.components, animate = true) { + if (this.config.isNavigable) { + // Remove all existing overlays + this.overlays.map((o) => o.parentNode.removeChild(o)); + // ref.parentNode.insertBefore(element, ref); + } + let elementsToAnimate = []; + // Can decouple to this.refreshComponents() first to save animation timeout + components.forEach((c) => { + elementsToAnimate = elementsToAnimate.concat(c.update(animate)); + }); + if (elementsToAnimate.length > 0) { + runSMILAnimation(this.container, this.svg, elementsToAnimate); + setTimeout(() => { + components.forEach((c) => c.make()); + this.updateNav(); + }, CHART_POST_ANIMATE_TIMEOUT); + } else { + components.forEach((c) => c.make()); + this.updateNav(); + } + } - updateNav() { - if (this.config.isNavigable) { - this.makeOverlay(); - this.bindUnits(); - } - } + updateNav() { + if (this.config.isNavigable) { + this.makeOverlay(); + this.bindUnits(); + } + } - renderLegend() {} + renderLegend() {} - setupNavigation(init = false) { - if (!this.config.isNavigable) return; + setupNavigation(init = false) { + if (!this.config.isNavigable) return; - if (init) { - this.bindOverlay(); + if (init) { + this.bindOverlay(); - this.keyActions = { - 13: this.onEnterKey.bind(this), - 37: this.onLeftArrow.bind(this), - 38: this.onUpArrow.bind(this), - 39: this.onRightArrow.bind(this), - 40: this.onDownArrow.bind(this), - }; + this.keyActions = { + 13: this.onEnterKey.bind(this), + 37: this.onLeftArrow.bind(this), + 38: this.onUpArrow.bind(this), + 39: this.onRightArrow.bind(this), + 40: this.onDownArrow.bind(this), + }; - document.addEventListener("keydown", (e) => { - if (isElementInViewport(this.container)) { - e = e || window.event; - if (this.keyActions[e.keyCode]) { - this.keyActions[e.keyCode](); - } - } - }); - } - } + document.addEventListener("keydown", (e) => { + if (isElementInViewport(this.container)) { + e = e || window.event; + if (this.keyActions[e.keyCode]) { + this.keyActions[e.keyCode](); + } + } + }); + } + } - makeOverlay() {} - updateOverlay() {} - bindOverlay() {} - bindUnits() {} + makeOverlay() {} + updateOverlay() {} + bindOverlay() {} + bindUnits() {} - onLeftArrow() {} - onRightArrow() {} - onUpArrow() {} - onDownArrow() {} - onEnterKey() {} + onLeftArrow() {} + onRightArrow() {} + onUpArrow() {} + onDownArrow() {} + onEnterKey() {} - addDataPoint() {} - removeDataPoint() {} + addDataPoint() {} + removeDataPoint() {} - getDataPoint() {} - setCurrentDataPoint() {} + getDataPoint() {} + setCurrentDataPoint() {} - updateDataset() {} + updateDataset() {} - export() { - let chartSvg = prepareForExport(this.svg); - downloadFile(this.title || "Chart", [chartSvg]); - } + export() { + let chartSvg = prepareForExport(this.svg); + downloadFile(this.title || "Chart", [chartSvg]); + } } diff --git a/src/js/charts/DonutChart.js b/src/js/charts/DonutChart.js index b0ae67b..7c61e8a 100644 --- a/src/js/charts/DonutChart.js +++ b/src/js/charts/DonutChart.js @@ -1,161 +1,185 @@ -import AggregationChart from './AggregationChart'; -import { getComponent } from '../objects/ChartComponents'; -import { getOffset } from '../utils/dom'; -import { getPositionByAngle } from '../utils/helpers'; -import { makeArcStrokePathStr, makeStrokeCircleStr } from '../utils/draw'; -import { lightenDarkenColor } from '../utils/colors'; -import { transform } from '../utils/animation'; -import { FULL_ANGLE } from '../utils/constants'; +import AggregationChart from "./AggregationChart"; +import { getComponent } from "../objects/ChartComponents"; +import { getOffset } from "../utils/dom"; +import { getPositionByAngle } from "../utils/helpers"; +import { makeArcStrokePathStr, makeStrokeCircleStr } from "../utils/draw"; +import { lightenDarkenColor } from "../utils/colors"; +import { transform } from "../utils/animation"; +import { FULL_ANGLE } from "../utils/constants"; export default class DonutChart extends AggregationChart { - constructor(parent, args) { - super(parent, args); - this.type = 'donut'; - this.initTimeout = 0; - this.init = 1; + constructor(parent, args) { + super(parent, args); + this.type = "donut"; + this.initTimeout = 0; + this.init = 1; - this.setup(); - } + this.setup(); + } - configure(args) { - super.configure(args); - this.mouseMove = this.mouseMove.bind(this); - this.mouseLeave = this.mouseLeave.bind(this); + configure(args) { + super.configure(args); + this.mouseMove = this.mouseMove.bind(this); + this.mouseLeave = this.mouseLeave.bind(this); - this.hoverRadio = args.hoverRadio || 0.1; - this.config.startAngle = args.startAngle || 0; + this.hoverRadio = args.hoverRadio || 0.1; + this.config.startAngle = args.startAngle || 0; - this.clockWise = args.clockWise || false; - this.strokeWidth = args.strokeWidth || 30; - } + this.clockWise = args.clockWise || false; + this.strokeWidth = args.strokeWidth || 30; + } - calc() { - super.calc(); - let s = this.state; - this.radius = - this.height > this.width - ? this.center.x - this.strokeWidth / 2 - : this.center.y - this.strokeWidth / 2; + calc() { + super.calc(); + let s = this.state; + this.radius = + this.height > this.width + ? this.center.x - this.strokeWidth / 2 + : this.center.y - this.strokeWidth / 2; - const { radius, clockWise } = this; + const { radius, clockWise } = this; - const prevSlicesProperties = s.slicesProperties || []; - s.sliceStrings = []; - s.slicesProperties = []; - let curAngle = 180 - this.config.startAngle; + const prevSlicesProperties = s.slicesProperties || []; + s.sliceStrings = []; + s.slicesProperties = []; + let curAngle = 180 - this.config.startAngle; - s.sliceTotals.map((total, i) => { - const startAngle = curAngle; - const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE; - const largeArc = originDiffAngle > 180 ? 1: 0; - const diffAngle = clockWise ? -originDiffAngle : originDiffAngle; - const endAngle = curAngle = curAngle + diffAngle; - const startPosition = getPositionByAngle(startAngle, radius); - const endPosition = getPositionByAngle(endAngle, radius); + s.sliceTotals.map((total, i) => { + const startAngle = curAngle; + const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE; + const largeArc = originDiffAngle > 180 ? 1 : 0; + const diffAngle = clockWise ? -originDiffAngle : originDiffAngle; + const endAngle = (curAngle = curAngle + diffAngle); + const startPosition = getPositionByAngle(startAngle, radius); + const endPosition = getPositionByAngle(endAngle, radius); - const prevProperty = this.init && prevSlicesProperties[i]; + const prevProperty = this.init && prevSlicesProperties[i]; - let curStart,curEnd; - if(this.init) { - curStart = prevProperty ? prevProperty.startPosition : startPosition; - curEnd = prevProperty ? prevProperty.endPosition : startPosition; - } else { - curStart = startPosition; - curEnd = endPosition; - } - const curPath = - originDiffAngle === 360 - ? makeStrokeCircleStr(curStart, curEnd, this.center, this.radius, this.clockWise, largeArc) - : makeArcStrokePathStr(curStart, curEnd, this.center, this.radius, this.clockWise, largeArc); + let curStart, curEnd; + if (this.init) { + curStart = prevProperty ? prevProperty.startPosition : startPosition; + curEnd = prevProperty ? prevProperty.endPosition : startPosition; + } else { + curStart = startPosition; + curEnd = endPosition; + } + const curPath = + originDiffAngle === 360 + ? makeStrokeCircleStr( + curStart, + curEnd, + this.center, + this.radius, + this.clockWise, + largeArc + ) + : makeArcStrokePathStr( + curStart, + curEnd, + this.center, + this.radius, + this.clockWise, + largeArc + ); - s.sliceStrings.push(curPath); - s.slicesProperties.push({ - startPosition, - endPosition, - value: total, - total: s.grandTotal, - startAngle, - endAngle, - angle: diffAngle - }); + s.sliceStrings.push(curPath); + s.slicesProperties.push({ + startPosition, + endPosition, + value: total, + total: s.grandTotal, + startAngle, + endAngle, + angle: diffAngle, + }); + }); + this.init = 0; + } - }); - this.init = 0; - } + setupComponents() { + let s = this.state; - setupComponents() { - let s = this.state; + let componentConfigs = [ + [ + "donutSlices", + {}, + function () { + return { + sliceStrings: s.sliceStrings, + colors: this.colors, + strokeWidth: this.strokeWidth, + }; + }.bind(this), + ], + ]; - let componentConfigs = [ - [ - 'donutSlices', - { }, - function() { - return { - sliceStrings: s.sliceStrings, - colors: this.colors, - strokeWidth: this.strokeWidth, - }; - }.bind(this) - ] - ]; + this.components = new Map( + componentConfigs.map((args) => { + let component = getComponent(...args); + return [args[0], component]; + }) + ); + } - this.components = new Map(componentConfigs - .map(args => { - let component = getComponent(...args); - return [args[0], component]; - })); - } + calTranslateByAngle(property) { + const { radius, hoverRadio } = this; + const position = getPositionByAngle( + property.startAngle + property.angle / 2, + radius + ); + return `translate3d(${position.x * hoverRadio}px,${ + position.y * hoverRadio + }px,0)`; + } - calTranslateByAngle(property){ - const{ radius, hoverRadio } = this; - const position = getPositionByAngle(property.startAngle+(property.angle / 2),radius); - return `translate3d(${(position.x) * hoverRadio}px,${(position.y) * hoverRadio}px,0)`; - } + hoverSlice(path, i, flag, e) { + if (!path) return; + const color = this.colors[i]; + if (flag) { + transform(path, this.calTranslateByAngle(this.state.slicesProperties[i])); + path.style.stroke = lightenDarkenColor(color, 50); + let g_off = getOffset(this.svg); + let x = e.pageX - g_off.left + 10; + let y = e.pageY - g_off.top - 10; + let title = + (this.formatted_labels && this.formatted_labels.length > 0 + ? this.formatted_labels[i] + : this.state.labels[i]) + ": "; + let percent = ( + (this.state.sliceTotals[i] * 100) / + this.state.grandTotal + ).toFixed(1); + this.tip.setValues(x, y, { name: title, value: percent + "%" }); + this.tip.showTip(); + } else { + transform(path, "translate3d(0,0,0)"); + this.tip.hideTip(); + path.style.stroke = color; + } + } - hoverSlice(path,i,flag,e){ - if(!path) return; - const color = this.colors[i]; - if(flag) { - transform(path, this.calTranslateByAngle(this.state.slicesProperties[i])); - path.style.stroke = lightenDarkenColor(color, 50); - let g_off = getOffset(this.svg); - let x = e.pageX - g_off.left + 10; - let y = e.pageY - g_off.top - 10; - let title = (this.formatted_labels && this.formatted_labels.length > 0 - ? this.formatted_labels[i] : this.state.labels[i]) + ': '; - let percent = (this.state.sliceTotals[i] * 100 / this.state.grandTotal).toFixed(1); - this.tip.setValues(x, y, {name: title, value: percent + "%"}); - this.tip.showTip(); - } else { - transform(path,'translate3d(0,0,0)'); - this.tip.hideTip(); - path.style.stroke = color; - } - } + bindTooltip() { + this.container.addEventListener("mousemove", this.mouseMove); + this.container.addEventListener("mouseleave", this.mouseLeave); + } - bindTooltip() { - this.container.addEventListener('mousemove', this.mouseMove); - this.container.addEventListener('mouseleave', this.mouseLeave); - } + mouseMove(e) { + const target = e.target; + let slices = this.components.get("donutSlices").store; + let prevIndex = this.curActiveSliceIndex; + let prevAcitve = this.curActiveSlice; + if (slices.includes(target)) { + let i = slices.indexOf(target); + this.hoverSlice(prevAcitve, prevIndex, false); + this.curActiveSlice = target; + this.curActiveSliceIndex = i; + this.hoverSlice(target, i, true, e); + } else { + this.mouseLeave(); + } + } - mouseMove(e){ - const target = e.target; - let slices = this.components.get('donutSlices').store; - let prevIndex = this.curActiveSliceIndex; - let prevAcitve = this.curActiveSlice; - if(slices.includes(target)) { - let i = slices.indexOf(target); - this.hoverSlice(prevAcitve, prevIndex,false); - this.curActiveSlice = target; - this.curActiveSliceIndex = i; - this.hoverSlice(target, i, true, e); - } else { - this.mouseLeave(); - } - } - - mouseLeave(){ - this.hoverSlice(this.curActiveSlice,this.curActiveSliceIndex,false); - } + mouseLeave() { + this.hoverSlice(this.curActiveSlice, this.curActiveSliceIndex, false); + } } diff --git a/src/js/charts/Heatmap.js b/src/js/charts/Heatmap.js index 9f1447a..a437c09 100644 --- a/src/js/charts/Heatmap.js +++ b/src/js/charts/Heatmap.js @@ -1,302 +1,337 @@ -import BaseChart from './BaseChart'; -import { getComponent } from '../objects/ChartComponents'; -import { makeText, heatSquare } from '../utils/draw'; -import { DAY_NAMES_SHORT, toMidnightUTC, addDays, areInSameMonth, getLastDateInMonth, setDayToSunday, getYyyyMmDd, getWeeksBetween, getMonthName, clone, - NO_OF_MILLIS, NO_OF_YEAR_MONTHS, NO_OF_DAYS_IN_WEEK } from '../utils/date-utils'; -import { calcDistribution, getMaxCheckpoint } from '../utils/intervals'; -import { getExtraHeight, getExtraWidth, HEATMAP_DISTRIBUTION_SIZE, HEATMAP_SQUARE_SIZE, - HEATMAP_GUTTER_SIZE } from '../utils/constants'; +import BaseChart from "./BaseChart"; +import { getComponent } from "../objects/ChartComponents"; +import { makeText, heatSquare } from "../utils/draw"; +import { + DAY_NAMES_SHORT, + toMidnightUTC, + addDays, + areInSameMonth, + getLastDateInMonth, + setDayToSunday, + getYyyyMmDd, + getWeeksBetween, + getMonthName, + clone, + NO_OF_MILLIS, + NO_OF_YEAR_MONTHS, + NO_OF_DAYS_IN_WEEK, +} from "../utils/date-utils"; +import { calcDistribution, getMaxCheckpoint } from "../utils/intervals"; +import { + getExtraHeight, + getExtraWidth, + HEATMAP_DISTRIBUTION_SIZE, + HEATMAP_SQUARE_SIZE, + HEATMAP_GUTTER_SIZE, +} from "../utils/constants"; const COL_WIDTH = HEATMAP_SQUARE_SIZE + HEATMAP_GUTTER_SIZE; const ROW_HEIGHT = COL_WIDTH; // const DAY_INCR = 1; export default class Heatmap extends BaseChart { - constructor(parent, options) { - super(parent, options); - this.type = 'heatmap'; + constructor(parent, options) { + super(parent, options); + this.type = "heatmap"; - this.countLabel = options.countLabel || ''; + this.countLabel = options.countLabel || ""; - let validStarts = ['Sunday', 'Monday']; - let startSubDomain = validStarts.includes(options.startSubDomain) - ? options.startSubDomain : 'Sunday'; - this.startSubDomainIndex = validStarts.indexOf(startSubDomain); + let validStarts = ["Sunday", "Monday"]; + let startSubDomain = validStarts.includes(options.startSubDomain) + ? options.startSubDomain + : "Sunday"; + this.startSubDomainIndex = validStarts.indexOf(startSubDomain); - this.setup(); - } + this.setup(); + } - setMeasures(options) { - let m = this.measures; - this.discreteDomains = options.discreteDomains === 0 ? 0 : 1; + setMeasures(options) { + let m = this.measures; + this.discreteDomains = options.discreteDomains === 0 ? 0 : 1; - m.paddings.top = ROW_HEIGHT * 3; - m.paddings.bottom = 0; - m.legendHeight = ROW_HEIGHT * 2; - m.baseHeight = ROW_HEIGHT * NO_OF_DAYS_IN_WEEK - + getExtraHeight(m); + m.paddings.top = ROW_HEIGHT * 3; + m.paddings.bottom = 0; + m.legendHeight = ROW_HEIGHT * 2; + m.baseHeight = ROW_HEIGHT * NO_OF_DAYS_IN_WEEK + getExtraHeight(m); - let d = this.data; - let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0; - this.independentWidth = (getWeeksBetween(d.start, d.end) - + spacing) * COL_WIDTH + getExtraWidth(m); - } + let d = this.data; + let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0; + this.independentWidth = + (getWeeksBetween(d.start, d.end) + spacing) * COL_WIDTH + + getExtraWidth(m); + } - updateWidth() { - let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0; - let noOfWeeks = this.state.noOfWeeks ? this.state.noOfWeeks : 52; - this.baseWidth = (noOfWeeks + spacing) * COL_WIDTH - + getExtraWidth(this.measures); - } + updateWidth() { + let spacing = this.discreteDomains ? NO_OF_YEAR_MONTHS : 0; + let noOfWeeks = this.state.noOfWeeks ? this.state.noOfWeeks : 52; + this.baseWidth = + (noOfWeeks + spacing) * COL_WIDTH + getExtraWidth(this.measures); + } - prepareData(data=this.data) { - if(data.start && data.end && data.start > data.end) { - throw new Error('Start date cannot be greater than end date.'); - } + prepareData(data = this.data) { + if (data.start && data.end && data.start > data.end) { + throw new Error("Start date cannot be greater than end date."); + } - if(!data.start) { - data.start = new Date(); - data.start.setFullYear( data.start.getFullYear() - 1 ); - } - data.start = toMidnightUTC(data.start); + if (!data.start) { + data.start = new Date(); + data.start.setFullYear(data.start.getFullYear() - 1); + } + data.start = toMidnightUTC(data.start); - if(!data.end) { - data.end = new Date(); - } - data.end = toMidnightUTC(data.end); + if (!data.end) { + data.end = new Date(); + } + data.end = toMidnightUTC(data.end); - data.dataPoints = data.dataPoints || {}; + data.dataPoints = data.dataPoints || {}; - if(parseInt(Object.keys(data.dataPoints)[0]) > 100000) { - let points = {}; - Object.keys(data.dataPoints).forEach(timestampSec => { - let date = new Date(timestampSec * NO_OF_MILLIS); - points[getYyyyMmDd(date)] = data.dataPoints[timestampSec]; - }); - data.dataPoints = points; - } + if (parseInt(Object.keys(data.dataPoints)[0]) > 100000) { + let points = {}; + Object.keys(data.dataPoints).forEach((timestampSec) => { + let date = new Date(timestampSec * NO_OF_MILLIS); + points[getYyyyMmDd(date)] = data.dataPoints[timestampSec]; + }); + data.dataPoints = points; + } - return data; - } + return data; + } - calc() { - let s = this.state; + calc() { + let s = this.state; - s.start = clone(this.data.start); - s.end = clone(this.data.end); + s.start = clone(this.data.start); + s.end = clone(this.data.end); - s.firstWeekStart = clone(s.start); - s.noOfWeeks = getWeeksBetween(s.start, s.end); - s.distribution = calcDistribution( - Object.values(this.data.dataPoints), HEATMAP_DISTRIBUTION_SIZE); + s.firstWeekStart = clone(s.start); + s.noOfWeeks = getWeeksBetween(s.start, s.end); + s.distribution = calcDistribution( + Object.values(this.data.dataPoints), + HEATMAP_DISTRIBUTION_SIZE + ); - s.domainConfigs = this.getDomains(); - } + s.domainConfigs = this.getDomains(); + } - setupComponents() { - let s = this.state; - let lessCol = this.discreteDomains ? 0 : 1; + setupComponents() { + let s = this.state; + let lessCol = this.discreteDomains ? 0 : 1; - let componentConfigs = s.domainConfigs.map((config, i) => [ - 'heatDomain', - { - index: config.index, - colWidth: COL_WIDTH, - rowHeight: ROW_HEIGHT, - squareSize: HEATMAP_SQUARE_SIZE, - radius: this.rawChartArgs.radius || 0, - xTranslate: s.domainConfigs - .filter((config, j) => j < i) - .map(config => config.cols.length - lessCol) - .reduce((a, b) => a + b, 0) - * COL_WIDTH - }, - function() { - return s.domainConfigs[i]; - }.bind(this) + let componentConfigs = s.domainConfigs.map((config, i) => [ + "heatDomain", + { + index: config.index, + colWidth: COL_WIDTH, + rowHeight: ROW_HEIGHT, + squareSize: HEATMAP_SQUARE_SIZE, + radius: this.rawChartArgs.radius || 0, + xTranslate: + s.domainConfigs + .filter((config, j) => j < i) + .map((config) => config.cols.length - lessCol) + .reduce((a, b) => a + b, 0) * COL_WIDTH, + }, + function () { + return s.domainConfigs[i]; + }.bind(this), + ]); - ]); + this.components = new Map( + componentConfigs.map((args, i) => { + let component = getComponent(...args); + return [args[0] + "-" + i, component]; + }) + ); - this.components = new Map(componentConfigs - .map((args, i) => { - let component = getComponent(...args); - return [args[0] + '-' + i, component]; - }) - ); + let y = 0; + DAY_NAMES_SHORT.forEach((dayName, i) => { + if ([1, 3, 5].includes(i)) { + let dayText = makeText("subdomain-name", -COL_WIDTH / 2, y, dayName, { + fontSize: HEATMAP_SQUARE_SIZE, + dy: 8, + textAnchor: "end", + }); + this.drawArea.appendChild(dayText); + } + y += ROW_HEIGHT; + }); + } - let y = 0; - DAY_NAMES_SHORT.forEach((dayName, i) => { - if([1, 3, 5].includes(i)) { - let dayText = makeText('subdomain-name', -COL_WIDTH/2, y, dayName, - { - fontSize: HEATMAP_SQUARE_SIZE, - dy: 8, - textAnchor: 'end' - } - ); - this.drawArea.appendChild(dayText); - } - y += ROW_HEIGHT; - }); - } + update(data) { + if (!data) { + console.error("No data to update."); + } - update(data) { - if(!data) { - console.error('No data to update.'); - } + this.data = this.prepareData(data); + this.draw(); + this.bindTooltip(); + } - this.data = this.prepareData(data); - this.draw(); - this.bindTooltip(); - } + bindTooltip() { + this.container.addEventListener("mousemove", (e) => { + this.components.forEach((comp) => { + let daySquares = comp.store; + let daySquare = e.target; + if (daySquares.includes(daySquare)) { + let count = daySquare.getAttribute("data-value"); + let dateParts = daySquare.getAttribute("data-date").split("-"); - bindTooltip() { - this.container.addEventListener('mousemove', (e) => { - this.components.forEach(comp => { - let daySquares = comp.store; - let daySquare = e.target; - if(daySquares.includes(daySquare)) { + let month = getMonthName(parseInt(dateParts[1]) - 1, true); - let count = daySquare.getAttribute('data-value'); - let dateParts = daySquare.getAttribute('data-date').split('-'); + let gOff = this.container.getBoundingClientRect(), + pOff = daySquare.getBoundingClientRect(); - let month = getMonthName(parseInt(dateParts[1])-1, true); + let width = parseInt(e.target.getAttribute("width")); + let x = pOff.left - gOff.left + width / 2; + let y = pOff.top - gOff.top; + let value = count + " " + this.countLabel; + let name = " on " + month + " " + dateParts[0] + ", " + dateParts[2]; - let gOff = this.container.getBoundingClientRect(), pOff = daySquare.getBoundingClientRect(); + this.tip.setValues( + x, + y, + { name: name, value: value, valueFirst: 1 }, + [] + ); + this.tip.showTip(); + } + }); + }); + } - let width = parseInt(e.target.getAttribute('width')); - let x = pOff.left - gOff.left + width/2; - let y = pOff.top - gOff.top; - let value = count + ' ' + this.countLabel; - let name = ' on ' + month + ' ' + dateParts[0] + ', ' + dateParts[2]; + renderLegend() { + this.legendArea.textContent = ""; + let x = 0; + let y = ROW_HEIGHT; + let radius = this.rawChartArgs.radius || 0; - this.tip.setValues(x, y, {name: name, value: value, valueFirst: 1}, []); - this.tip.showTip(); - } - }); - }); - } + let lessText = makeText("subdomain-name", x, y, "Less", { + fontSize: HEATMAP_SQUARE_SIZE + 1, + dy: 9, + }); + x = COL_WIDTH * 2 + COL_WIDTH / 2; + this.legendArea.appendChild(lessText); - renderLegend() { - this.legendArea.textContent = ''; - let x = 0; - let y = ROW_HEIGHT; - let radius = this.rawChartArgs.radius || 0; + this.colors.slice(0, HEATMAP_DISTRIBUTION_SIZE).map((color, i) => { + const square = heatSquare( + "heatmap-legend-unit", + x + (COL_WIDTH + 3) * i, + y, + HEATMAP_SQUARE_SIZE, + radius, + color + ); + this.legendArea.appendChild(square); + }); - let lessText = makeText('subdomain-name', x, y, 'Less', - { - fontSize: HEATMAP_SQUARE_SIZE + 1, - dy: 9 - } - ); - x = (COL_WIDTH * 2) + COL_WIDTH/2; - this.legendArea.appendChild(lessText); + let moreTextX = + x + HEATMAP_DISTRIBUTION_SIZE * (COL_WIDTH + 3) + COL_WIDTH / 4; + let moreText = makeText("subdomain-name", moreTextX, y, "More", { + fontSize: HEATMAP_SQUARE_SIZE + 1, + dy: 9, + }); + this.legendArea.appendChild(moreText); + } - this.colors.slice(0, HEATMAP_DISTRIBUTION_SIZE).map((color, i) => { - const square = heatSquare('heatmap-legend-unit', x + (COL_WIDTH + 3) * i, - y, HEATMAP_SQUARE_SIZE, radius, color); - this.legendArea.appendChild(square); - }); + getDomains() { + let s = this.state; + const [startMonth, startYear] = [s.start.getMonth(), s.start.getFullYear()]; + const [endMonth, endYear] = [s.end.getMonth(), s.end.getFullYear()]; - let moreTextX = x + HEATMAP_DISTRIBUTION_SIZE * (COL_WIDTH + 3) + COL_WIDTH/4; - let moreText = makeText('subdomain-name', moreTextX, y, 'More', - { - fontSize: HEATMAP_SQUARE_SIZE + 1, - dy: 9 - } - ); - this.legendArea.appendChild(moreText); - } + const noOfMonths = endMonth - startMonth + 1 + (endYear - startYear) * 12; - getDomains() { - let s = this.state; - const [startMonth, startYear] = [s.start.getMonth(), s.start.getFullYear()]; - const [endMonth, endYear] = [s.end.getMonth(), s.end.getFullYear()]; + let domainConfigs = []; - const noOfMonths = (endMonth - startMonth + 1) + (endYear - startYear) * 12; + let startOfMonth = clone(s.start); + for (var i = 0; i < noOfMonths; i++) { + let endDate = s.end; + if (!areInSameMonth(startOfMonth, s.end)) { + let [month, year] = [ + startOfMonth.getMonth(), + startOfMonth.getFullYear(), + ]; + endDate = getLastDateInMonth(month, year); + } + domainConfigs.push(this.getDomainConfig(startOfMonth, endDate)); - let domainConfigs = []; + addDays(endDate, 1); + startOfMonth = endDate; + } - let startOfMonth = clone(s.start); - for(var i = 0; i < noOfMonths; i++) { - let endDate = s.end; - if(!areInSameMonth(startOfMonth, s.end)) { - let [month, year] = [startOfMonth.getMonth(), startOfMonth.getFullYear()]; - endDate = getLastDateInMonth(month, year); - } - domainConfigs.push(this.getDomainConfig(startOfMonth, endDate)); + return domainConfigs; + } - addDays(endDate, 1); - startOfMonth = endDate; - } + getDomainConfig(startDate, endDate = "") { + let [month, year] = [startDate.getMonth(), startDate.getFullYear()]; + let startOfWeek = setDayToSunday(startDate); // TODO: Monday as well + endDate = endDate + ? clone(endDate) + : toMidnightUTC(getLastDateInMonth(month, year)); - return domainConfigs; - } + let domainConfig = { + index: month, + cols: [], + }; - getDomainConfig(startDate, endDate='') { - let [month, year] = [startDate.getMonth(), startDate.getFullYear()]; - let startOfWeek = setDayToSunday(startDate); // TODO: Monday as well - endDate = endDate ? clone(endDate) : toMidnightUTC(getLastDateInMonth(month, year)); + addDays(endDate, 1); + let noOfMonthWeeks = getWeeksBetween(startOfWeek, endDate); - let domainConfig = { - index: month, - cols: [] - }; + let cols = [], + col; + for (var i = 0; i < noOfMonthWeeks; i++) { + col = this.getCol(startOfWeek, month); + cols.push(col); - addDays(endDate, 1); - let noOfMonthWeeks = getWeeksBetween(startOfWeek, endDate); + startOfWeek = toMidnightUTC( + new Date(col[NO_OF_DAYS_IN_WEEK - 1].yyyyMmDd) + ); + addDays(startOfWeek, 1); + } - let cols = [], col; - for(var i = 0; i < noOfMonthWeeks; i++) { - col = this.getCol(startOfWeek, month); - cols.push(col); + if (col[NO_OF_DAYS_IN_WEEK - 1].dataValue !== undefined) { + addDays(startOfWeek, 1); + cols.push(this.getCol(startOfWeek, month, true)); + } - startOfWeek = toMidnightUTC(new Date(col[NO_OF_DAYS_IN_WEEK - 1].yyyyMmDd)); - addDays(startOfWeek, 1); - } + domainConfig.cols = cols; - if(col[NO_OF_DAYS_IN_WEEK - 1].dataValue !== undefined) { - addDays(startOfWeek, 1); - cols.push(this.getCol(startOfWeek, month, true)); - } + return domainConfig; + } - domainConfig.cols = cols; + getCol(startDate, month, empty = false) { + let s = this.state; - return domainConfig; - } + // startDate is the start of week + let currentDate = clone(startDate); + let col = []; - getCol(startDate, month, empty = false) { - let s = this.state; + for (var i = 0; i < NO_OF_DAYS_IN_WEEK; i++, addDays(currentDate, 1)) { + let config = {}; - // startDate is the start of week - let currentDate = clone(startDate); - let col = []; + // Non-generic adjustment for entire heatmap, needs state + let currentDateWithinData = + currentDate >= s.start && currentDate <= s.end; - for(var i = 0; i < NO_OF_DAYS_IN_WEEK; i++, addDays(currentDate, 1)) { - let config = {}; + if (empty || currentDate.getMonth() !== month || !currentDateWithinData) { + config.yyyyMmDd = getYyyyMmDd(currentDate); + } else { + config = this.getSubDomainConfig(currentDate); + } + col.push(config); + } - // Non-generic adjustment for entire heatmap, needs state - let currentDateWithinData = currentDate >= s.start && currentDate <= s.end; + return col; + } - if(empty || currentDate.getMonth() !== month || !currentDateWithinData) { - config.yyyyMmDd = getYyyyMmDd(currentDate); - } else { - config = this.getSubDomainConfig(currentDate); - } - col.push(config); - } - - return col; - } - - getSubDomainConfig(date) { - let yyyyMmDd = getYyyyMmDd(date); - let dataValue = this.data.dataPoints[yyyyMmDd]; - let config = { - yyyyMmDd: yyyyMmDd, - dataValue: dataValue || 0, - fill: this.colors[getMaxCheckpoint(dataValue, this.state.distribution)] - }; - return config; - } + getSubDomainConfig(date) { + let yyyyMmDd = getYyyyMmDd(date); + let dataValue = this.data.dataPoints[yyyyMmDd]; + let config = { + yyyyMmDd: yyyyMmDd, + dataValue: dataValue || 0, + fill: this.colors[getMaxCheckpoint(dataValue, this.state.distribution)], + }; + return config; + } } diff --git a/src/js/charts/MultiAxisChart.js b/src/js/charts/MultiAxisChart.js index 69cfb4f..75ceff9 100644 --- a/src/js/charts/MultiAxisChart.js +++ b/src/js/charts/MultiAxisChart.js @@ -1,173 +1,183 @@ -import AxisChart from './AxisChart'; -import { Y_AXIS_MARGIN } from '../utils/constants'; +import AxisChart from "./AxisChart"; +import { Y_AXIS_MARGIN } from "../utils/constants"; // import { ChartComponent } from '../objects/ChartComponents'; -import { floatTwo } from '../utils/helpers'; +import { floatTwo } from "../utils/helpers"; export default class MultiAxisChart extends AxisChart { - constructor(args) { - super(args); - // this.unitType = args.unitType || 'line'; - // this.setup(); - } + constructor(args) { + super(args); + // this.unitType = args.unitType || 'line'; + // this.setup(); + } - preSetup() { - this.type = 'multiaxis'; - } + preSetup() { + this.type = "multiaxis"; + } - setMeasures() { - super.setMeasures(); - let noOfLeftAxes = this.data.datasets.filter(d => d.axisPosition === 'left').length; - this.measures.margins.left = (noOfLeftAxes) * Y_AXIS_MARGIN || Y_AXIS_MARGIN; - this.measures.margins.right = (this.data.datasets.length - noOfLeftAxes) * Y_AXIS_MARGIN || Y_AXIS_MARGIN; - } + setMeasures() { + super.setMeasures(); + let noOfLeftAxes = this.data.datasets.filter( + (d) => d.axisPosition === "left" + ).length; + this.measures.margins.left = noOfLeftAxes * Y_AXIS_MARGIN || Y_AXIS_MARGIN; + this.measures.margins.right = + (this.data.datasets.length - noOfLeftAxes) * Y_AXIS_MARGIN || + Y_AXIS_MARGIN; + } - prepareYAxis() { } + prepareYAxis() {} - prepareData(data) { - super.prepareData(data); - let sets = this.state.datasets; - // let axesLeft = sets.filter(d => d.axisPosition === 'left'); - // let axesRight = sets.filter(d => d.axisPosition === 'right'); - // let axesNone = sets.filter(d => !d.axisPosition || - // !['left', 'right'].includes(d.axisPosition)); + prepareData(data) { + super.prepareData(data); + let sets = this.state.datasets; + // let axesLeft = sets.filter(d => d.axisPosition === 'left'); + // let axesRight = sets.filter(d => d.axisPosition === 'right'); + // let axesNone = sets.filter(d => !d.axisPosition || + // !['left', 'right'].includes(d.axisPosition)); - let leftCount = 0, rightCount = 0; + let leftCount = 0, + rightCount = 0; - sets.forEach((d, i) => { - d.yAxis = { - position: d.axisPosition, - index: d.axisPosition === 'left' ? leftCount++ : rightCount++ - }; - }); - } + sets.forEach((d, i) => { + d.yAxis = { + position: d.axisPosition, + index: d.axisPosition === "left" ? leftCount++ : rightCount++, + }; + }); + } - configure(args) { - super.configure(args); - this.config.xAxisMode = args.xAxisMode || 'tick'; - this.config.yAxisMode = args.yAxisMode || 'span'; - } + configure(args) { + super.configure(args); + this.config.xAxisMode = args.xAxisMode || "tick"; + this.config.yAxisMode = args.yAxisMode || "span"; + } - // setUnitWidthAndXOffset() { - // this.state.unitWidth = this.width/(this.state.datasetLength); - // this.state.xOffset = this.state.unitWidth/2; - // } + // setUnitWidthAndXOffset() { + // this.state.unitWidth = this.width/(this.state.datasetLength); + // this.state.xOffset = this.state.unitWidth/2; + // } - configUnits() { - this.unitArgs = { - type: 'bar', - args: { - spaceWidth: this.state.unitWidth/2, - } - }; - } + configUnits() { + this.unitArgs = { + type: "bar", + args: { + spaceWidth: this.state.unitWidth / 2, + }, + }; + } - setYAxis() { - this.state.datasets.map(d => { - this.calcYAxisParameters(d.yAxis, d.values, this.unitType === 'line'); - }); - } + setYAxis() { + this.state.datasets.map((d) => { + this.calcYAxisParameters(d.yAxis, d.values, this.unitType === "line"); + }); + } - calcYUnits() { - this.state.datasets.map(d => { - d.positions = d.values.map(val => floatTwo(d.yAxis.zeroLine - val * d.yAxis.scaleMultiplier)); - }); - } + calcYUnits() { + this.state.datasets.map((d) => { + d.positions = d.values.map((val) => + floatTwo(d.yAxis.zeroLine - val * d.yAxis.scaleMultiplier) + ); + }); + } - // TODO: function doesn't exist, handle with components - renderConstants() { - this.state.datasets.map(d => { - let guidePos = d.yAxis.position === 'left' - ? -1 * d.yAxis.index * Y_AXIS_MARGIN - : this.width + d.yAxis.index * Y_AXIS_MARGIN; - this.renderer.xLine(guidePos, '', { - pos:'top', - mode: 'span', - stroke: this.colors[i], - className: 'y-axis-guide' - }) - }); - } + // TODO: function doesn't exist, handle with components + renderConstants() { + this.state.datasets.map((d) => { + let guidePos = + d.yAxis.position === "left" + ? -1 * d.yAxis.index * Y_AXIS_MARGIN + : this.width + d.yAxis.index * Y_AXIS_MARGIN; + this.renderer.xLine(guidePos, "", { + pos: "top", + mode: "span", + stroke: this.colors[i], + className: "y-axis-guide", + }); + }); + } - getYAxesComponents() { - return this.data.datasets.map((e, i) => { - return new ChartComponent({ - layerClass: 'y axis y-axis-' + i, - make: () => { - let yAxis = this.state.datasets[i].yAxis; - this.renderer.setZeroline(yAxis.zeroline); - let options = { - pos: yAxis.position, - mode: 'tick', - offset: yAxis.index * Y_AXIS_MARGIN, - stroke: this.colors[i] - }; + getYAxesComponents() { + return this.data.datasets.map((e, i) => { + return new ChartComponent({ + layerClass: "y axis y-axis-" + i, + make: () => { + let yAxis = this.state.datasets[i].yAxis; + this.renderer.setZeroline(yAxis.zeroline); + let options = { + pos: yAxis.position, + mode: "tick", + offset: yAxis.index * Y_AXIS_MARGIN, + stroke: this.colors[i], + }; - return yAxis.positions.map((position, j) => - this.renderer.yLine(position, yAxis.labels[j], options) - ); - }, - animate: () => {} - }); - }); - } + return yAxis.positions.map((position, j) => + this.renderer.yLine(position, yAxis.labels[j], options) + ); + }, + animate: () => {}, + }); + }); + } - // TODO remove renderer zeroline from above and below - getChartComponents() { - return this.data.datasets.map((d, index) => { - return new ChartComponent({ - layerClass: 'dataset-units dataset-' + index, - make: () => { - let d = this.state.datasets[index]; - let unitType = this.unitArgs; + // TODO remove renderer zeroline from above and below + getChartComponents() { + return this.data.datasets.map((d, index) => { + return new ChartComponent({ + layerClass: "dataset-units dataset-" + index, + make: () => { + let d = this.state.datasets[index]; + let unitType = this.unitArgs; - // the only difference, should be tied to datasets or default - this.renderer.setZeroline(d.yAxis.zeroLine); + // the only difference, should be tied to datasets or default + this.renderer.setZeroline(d.yAxis.zeroLine); - return d.positions.map((y, j) => { - return this.renderer[unitType.type]( - this.state.xAxisPositions[j], - y, - unitType.args, - this.colors[index], - j, - index, - this.state.datasetLength - ); - }); - }, - animate: (svgUnits) => { - let d = this.state.datasets[index]; - let unitType = this.unitArgs.type; + return d.positions.map((y, j) => { + return this.renderer[unitType.type]( + this.state.xAxisPositions[j], + y, + unitType.args, + this.colors[index], + j, + index, + this.state.datasetLength + ); + }); + }, + animate: (svgUnits) => { + let d = this.state.datasets[index]; + let unitType = this.unitArgs.type; - // have been updated in axis render; - let newX = this.state.xAxisPositions; - let newY = this.state.datasets[index].positions; + // have been updated in axis render; + let newX = this.state.xAxisPositions; + let newY = this.state.datasets[index].positions; - let lastUnit = svgUnits[svgUnits.length - 1]; - let parentNode = lastUnit.parentNode; + let lastUnit = svgUnits[svgUnits.length - 1]; + let parentNode = lastUnit.parentNode; - if(this.oldState.xExtra > 0) { - for(var i = 0; i 0) { + for (var i = 0; i < this.oldState.xExtra; i++) { + let unit = lastUnit.cloneNode(true); + parentNode.appendChild(unit); + svgUnits.push(unit); + } + } - this.renderer.setZeroline(d.yAxis.zeroLine); + this.renderer.setZeroline(d.yAxis.zeroLine); - svgUnits.map((unit, i) => { - if(newX[i] === undefined || newY[i] === undefined) return; - this.elementsToAnimate.push(this.renderer['animate' + unitType]( - unit, // unit, with info to replace where it came from in the data - newX[i], - newY[i], - index, - this.state.noOfDatasets - )); - }); - } - }); - }); - } + svgUnits.map((unit, i) => { + if (newX[i] === undefined || newY[i] === undefined) return; + this.elementsToAnimate.push( + this.renderer["animate" + unitType]( + unit, // unit, with info to replace where it came from in the data + newX[i], + newY[i], + index, + this.state.noOfDatasets + ) + ); + }); + }, + }); + }); + } } diff --git a/src/js/charts/PercentageChart.js b/src/js/charts/PercentageChart.js index d673ba0..d78d89a 100644 --- a/src/js/charts/PercentageChart.js +++ b/src/js/charts/PercentageChart.js @@ -1,90 +1,96 @@ -import AggregationChart from './AggregationChart'; -import { getOffset } from '../utils/dom'; -import { getComponent } from '../objects/ChartComponents'; -import { PERCENTAGE_BAR_DEFAULT_HEIGHT } from '../utils/constants'; +import AggregationChart from "./AggregationChart"; +import { getOffset } from "../utils/dom"; +import { getComponent } from "../objects/ChartComponents"; +import { PERCENTAGE_BAR_DEFAULT_HEIGHT } from "../utils/constants"; export default class PercentageChart extends AggregationChart { - constructor(parent, args) { - super(parent, args); - this.type = 'percentage'; - this.setup(); - } + constructor(parent, args) { + super(parent, args); + this.type = "percentage"; + this.setup(); + } - setMeasures(options) { - let m = this.measures; - this.barOptions = options.barOptions || {}; + setMeasures(options) { + let m = this.measures; + this.barOptions = options.barOptions || {}; - let b = this.barOptions; - b.height = b.height || PERCENTAGE_BAR_DEFAULT_HEIGHT; + let b = this.barOptions; + b.height = b.height || PERCENTAGE_BAR_DEFAULT_HEIGHT; - m.paddings.right = 30; - m.legendHeight = 60; - m.baseHeight = (b.height + b.depth * 0.5) * 8; - } + m.paddings.right = 30; + m.legendHeight = 60; + m.baseHeight = (b.height + b.depth * 0.5) * 8; + } - setupComponents() { - let s = this.state; + setupComponents() { + let s = this.state; - let componentConfigs = [ - [ - 'percentageBars', - { - barHeight: this.barOptions.height, - }, - function() { - return { - xPositions: s.xPositions, - widths: s.widths, - colors: this.colors - }; - }.bind(this) - ] - ]; + let componentConfigs = [ + [ + "percentageBars", + { + barHeight: this.barOptions.height, + }, + function () { + return { + xPositions: s.xPositions, + widths: s.widths, + colors: this.colors, + }; + }.bind(this), + ], + ]; - this.components = new Map(componentConfigs - .map(args => { - let component = getComponent(...args); - return [args[0], component]; - })); - } + this.components = new Map( + componentConfigs.map((args) => { + let component = getComponent(...args); + return [args[0], component]; + }) + ); + } - calc() { - super.calc(); - let s = this.state; + calc() { + super.calc(); + let s = this.state; - s.xPositions = []; - s.widths = []; + s.xPositions = []; + s.widths = []; - let xPos = 0; - s.sliceTotals.map((value) => { - let width = this.width * value / s.grandTotal; - s.widths.push(width); - s.xPositions.push(xPos); - xPos += width; - }); - } + let xPos = 0; + s.sliceTotals.map((value) => { + let width = (this.width * value) / s.grandTotal; + s.widths.push(width); + s.xPositions.push(xPos); + xPos += width; + }); + } - makeDataByIndex() { } + makeDataByIndex() {} - bindTooltip() { - let s = this.state; - this.container.addEventListener('mousemove', (e) => { - let bars = this.components.get('percentageBars').store; - let bar = e.target; - if(bars.includes(bar)) { + bindTooltip() { + let s = this.state; + this.container.addEventListener("mousemove", (e) => { + let bars = this.components.get("percentageBars").store; + let bar = e.target; + if (bars.includes(bar)) { + let i = bars.indexOf(bar); + let gOff = getOffset(this.container), + pOff = getOffset(bar); - let i = bars.indexOf(bar); - let gOff = getOffset(this.container), pOff = getOffset(bar); + let x = pOff.left - gOff.left + parseInt(bar.getAttribute("width")) / 2; + let y = pOff.top - gOff.top; + let title = + (this.formattedLabels && this.formattedLabels.length > 0 + ? this.formattedLabels[i] + : this.state.labels[i]) + ": "; + let fraction = s.sliceTotals[i] / s.grandTotal; - let x = pOff.left - gOff.left + parseInt(bar.getAttribute('width'))/2; - let y = pOff.top - gOff.top; - let title = (this.formattedLabels && this.formattedLabels.length>0 - ? this.formattedLabels[i] : this.state.labels[i]) + ': '; - let fraction = s.sliceTotals[i]/s.grandTotal; - - this.tip.setValues(x, y, {name: title, value: (fraction*100).toFixed(1) + "%"}); - this.tip.showTip(); - } - }); - } + this.tip.setValues(x, y, { + name: title, + value: (fraction * 100).toFixed(1) + "%", + }); + this.tip.showTip(); + } + }); + } } diff --git a/src/js/charts/PieChart.js b/src/js/charts/PieChart.js index ca7fbeb..793b828 100644 --- a/src/js/charts/PieChart.js +++ b/src/js/charts/PieChart.js @@ -1,184 +1,207 @@ -import AggregationChart from './AggregationChart'; -import { getComponent } from '../objects/ChartComponents'; -import { getOffset, fire } from '../utils/dom'; -import { getPositionByAngle } from '../utils/helpers'; -import { makeArcPathStr, makeCircleStr } from '../utils/draw'; -import { lightenDarkenColor } from '../utils/colors'; -import { transform } from '../utils/animation'; -import { FULL_ANGLE } from '../utils/constants'; +import AggregationChart from "./AggregationChart"; +import { getComponent } from "../objects/ChartComponents"; +import { getOffset, fire } from "../utils/dom"; +import { getPositionByAngle } from "../utils/helpers"; +import { makeArcPathStr, makeCircleStr } from "../utils/draw"; +import { lightenDarkenColor } from "../utils/colors"; +import { transform } from "../utils/animation"; +import { FULL_ANGLE } from "../utils/constants"; export default class PieChart extends AggregationChart { - constructor(parent, args) { - super(parent, args); - this.type = 'pie'; - this.initTimeout = 0; - this.init = 1; + constructor(parent, args) { + super(parent, args); + this.type = "pie"; + this.initTimeout = 0; + this.init = 1; - this.setup(); - } + this.setup(); + } - configure(args) { - super.configure(args); - this.mouseMove = this.mouseMove.bind(this); - this.mouseLeave = this.mouseLeave.bind(this); + configure(args) { + super.configure(args); + this.mouseMove = this.mouseMove.bind(this); + this.mouseLeave = this.mouseLeave.bind(this); - this.hoverRadio = args.hoverRadio || 0.1; - this.config.startAngle = args.startAngle || 0; + this.hoverRadio = args.hoverRadio || 0.1; + this.config.startAngle = args.startAngle || 0; - this.clockWise = args.clockWise || false; - } + this.clockWise = args.clockWise || false; + } - calc() { - super.calc(); - let s = this.state; - this.radius = (this.height > this.width ? this.center.x : this.center.y); + calc() { + super.calc(); + let s = this.state; + this.radius = this.height > this.width ? this.center.x : this.center.y; - const { radius, clockWise } = this; + const { radius, clockWise } = this; - const prevSlicesProperties = s.slicesProperties || []; - s.sliceStrings = []; - s.slicesProperties = []; - let curAngle = 180 - this.config.startAngle; + const prevSlicesProperties = s.slicesProperties || []; + s.sliceStrings = []; + s.slicesProperties = []; + let curAngle = 180 - this.config.startAngle; - s.sliceTotals.map((total, i) => { + s.sliceTotals.map((total, i) => { + const startAngle = curAngle; + const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE; + const largeArc = originDiffAngle > 180 ? 1 : 0; + const diffAngle = clockWise ? -originDiffAngle : originDiffAngle; + const endAngle = (curAngle = curAngle + diffAngle); + const startPosition = getPositionByAngle(startAngle, radius); + const endPosition = getPositionByAngle(endAngle, radius); - const startAngle = curAngle; - const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE; - const largeArc = originDiffAngle > 180 ? 1 : 0; - const diffAngle = clockWise ? -originDiffAngle : originDiffAngle; - const endAngle = curAngle = curAngle + diffAngle; - const startPosition = getPositionByAngle(startAngle, radius); - const endPosition = getPositionByAngle(endAngle, radius); + const prevProperty = this.init && prevSlicesProperties[i]; - const prevProperty = this.init && prevSlicesProperties[i]; + let curStart, curEnd; + if (this.init) { + curStart = prevProperty ? prevProperty.startPosition : startPosition; + curEnd = prevProperty ? prevProperty.endPosition : startPosition; + } else { + curStart = startPosition; + curEnd = endPosition; + } + const curPath = + originDiffAngle === 360 + ? makeCircleStr( + curStart, + curEnd, + this.center, + this.radius, + clockWise, + largeArc + ) + : makeArcPathStr( + curStart, + curEnd, + this.center, + this.radius, + clockWise, + largeArc + ); - let curStart, curEnd; - if (this.init) { - curStart = prevProperty ? prevProperty.startPosition : startPosition; - curEnd = prevProperty ? prevProperty.endPosition : startPosition; - } else { - curStart = startPosition; - curEnd = endPosition; - } - const curPath = - originDiffAngle === 360 - ? makeCircleStr(curStart, curEnd, this.center, this.radius, clockWise, largeArc) - : makeArcPathStr(curStart, curEnd, this.center, this.radius, clockWise, largeArc); + s.sliceStrings.push(curPath); + s.slicesProperties.push({ + startPosition, + endPosition, + value: total, + total: s.grandTotal, + startAngle, + endAngle, + angle: diffAngle, + }); + }); + this.init = 0; + } - s.sliceStrings.push(curPath); - s.slicesProperties.push({ - startPosition, - endPosition, - value: total, - total: s.grandTotal, - startAngle, - endAngle, - angle: diffAngle - }); - }); - this.init = 0; - } + setupComponents() { + let s = this.state; - setupComponents() { - let s = this.state; + let componentConfigs = [ + [ + "pieSlices", + {}, + function () { + return { + sliceStrings: s.sliceStrings, + colors: this.colors, + }; + }.bind(this), + ], + ]; - let componentConfigs = [ - [ - 'pieSlices', - {}, - function () { - return { - sliceStrings: s.sliceStrings, - colors: this.colors - }; - }.bind(this) - ] - ]; + this.components = new Map( + componentConfigs.map((args) => { + let component = getComponent(...args); + return [args[0], component]; + }) + ); + } - this.components = new Map(componentConfigs - .map(args => { - let component = getComponent(...args); - return [args[0], component]; - })); + calTranslateByAngle(property) { + const { radius, hoverRadio } = this; + const position = getPositionByAngle( + property.startAngle + property.angle / 2, + radius + ); + return `translate3d(${position.x * hoverRadio}px,${ + position.y * hoverRadio + }px,0)`; + } - } + hoverSlice(path, i, flag, e) { + if (!path) return; + const color = this.colors[i]; + if (flag) { + transform(path, this.calTranslateByAngle(this.state.slicesProperties[i])); + path.style.fill = lightenDarkenColor(color, 50); + let g_off = getOffset(this.svg); + let x = e.pageX - g_off.left + 10; + let y = e.pageY - g_off.top - 10; + let title = + (this.formatted_labels && this.formatted_labels.length > 0 + ? this.formatted_labels[i] + : this.state.labels[i]) + ": "; + let percent = ( + (this.state.sliceTotals[i] * 100) / + this.state.grandTotal + ).toFixed(1); + this.tip.setValues(x, y, { name: title, value: percent + "%" }); + this.tip.showTip(); + } else { + transform(path, "translate3d(0,0,0)"); + this.tip.hideTip(); + path.style.fill = color; + } + } - calTranslateByAngle(property) { - const { radius, hoverRadio } = this; - const position = getPositionByAngle(property.startAngle + (property.angle / 2), radius); - return `translate3d(${(position.x) * hoverRadio}px,${(position.y) * hoverRadio}px,0)`; - } + bindTooltip() { + this.container.addEventListener("mousemove", this.mouseMove); + this.container.addEventListener("mouseleave", this.mouseLeave); + } + getDataPoint(index = this.state.currentIndex) { + let s = this.state; + let data_point = { + index: index, + label: s.labels[index], + values: s.sliceTotals[index], + }; + return data_point; + } + setCurrentDataPoint(index) { + let s = this.state; + index = parseInt(index); + if (index < 0) index = 0; + if (index >= s.labels.length) index = s.labels.length - 1; + if (index === s.currentIndex) return; + s.currentIndex = index; + fire(this.parent, "data-select", this.getDataPoint()); + } - hoverSlice(path, i, flag, e) { - if (!path) return; - const color = this.colors[i]; - if (flag) { - transform(path, this.calTranslateByAngle(this.state.slicesProperties[i])); - path.style.fill = lightenDarkenColor(color, 50); - let g_off = getOffset(this.svg); - let x = e.pageX - g_off.left + 10; - let y = e.pageY - g_off.top - 10; - let title = (this.formatted_labels && this.formatted_labels.length > 0 - ? this.formatted_labels[i] : this.state.labels[i]) + ': '; - let percent = (this.state.sliceTotals[i] * 100 / this.state.grandTotal).toFixed(1); - this.tip.setValues(x, y, { name: title, value: percent + "%" }); - this.tip.showTip(); - } else { - transform(path, 'translate3d(0,0,0)'); - this.tip.hideTip(); - path.style.fill = color; - } - } + bindUnits() { + const units = this.components.get("pieSlices").store; + if (!units) return; + units.forEach((unit, index) => { + unit.addEventListener("click", () => { + this.setCurrentDataPoint(index); + }); + }); + } + mouseMove(e) { + const target = e.target; + let slices = this.components.get("pieSlices").store; + let prevIndex = this.curActiveSliceIndex; + let prevAcitve = this.curActiveSlice; + if (slices.includes(target)) { + let i = slices.indexOf(target); + this.hoverSlice(prevAcitve, prevIndex, false); + this.curActiveSlice = target; + this.curActiveSliceIndex = i; + this.hoverSlice(target, i, true, e); + } else { + this.mouseLeave(); + } + } - bindTooltip() { - this.container.addEventListener('mousemove', this.mouseMove); - this.container.addEventListener('mouseleave', this.mouseLeave); - } - getDataPoint(index = this.state.currentIndex) { - let s = this.state; - let data_point = { - index: index, - label: s.labels[index], - values: s.sliceTotals[index] - }; - return data_point; - } - setCurrentDataPoint(index) { - let s = this.state; - index = parseInt(index); - if (index < 0) index = 0; - if (index >= s.labels.length) index = s.labels.length - 1; - if (index === s.currentIndex) return; - s.currentIndex = index; - fire(this.parent, "data-select", this.getDataPoint()); - } - - bindUnits() { - const units = this.components.get('pieSlices').store; - if (!units) return; - units.forEach((unit, index) => { - unit.addEventListener('click', () => { - this.setCurrentDataPoint(index); - }); - }); - } - mouseMove(e) { - const target = e.target; - let slices = this.components.get('pieSlices').store; - let prevIndex = this.curActiveSliceIndex; - let prevAcitve = this.curActiveSlice; - if (slices.includes(target)) { - let i = slices.indexOf(target); - this.hoverSlice(prevAcitve, prevIndex, false); - this.curActiveSlice = target; - this.curActiveSliceIndex = i; - this.hoverSlice(target, i, true, e); - } else { - this.mouseLeave(); - } - } - - mouseLeave() { - this.hoverSlice(this.curActiveSlice, this.curActiveSliceIndex, false); - } + mouseLeave() { + this.hoverSlice(this.curActiveSlice, this.curActiveSliceIndex, false); + } } diff --git a/src/js/index.js b/src/js/index.js index 5ce3c59..a2f5b2b 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -1,10 +1,10 @@ -import * as Charts from './chart'; +import * as Charts from "./chart"; -let frappe = { }; +let frappe = {}; -frappe.NAME = 'Frappe Charts'; -frappe.VERSION = '1.6.2'; +frappe.NAME = "Frappe Charts"; +frappe.VERSION = "1.6.2"; -frappe = Object.assign({ }, frappe, Charts); +frappe = Object.assign({}, frappe, Charts); -export default frappe; \ No newline at end of file +export default frappe; diff --git a/src/js/objects/ChartComponents.js b/src/js/objects/ChartComponents.js index f333b7b..4b01518 100644 --- a/src/js/objects/ChartComponents.js +++ b/src/js/objects/ChartComponents.js @@ -1,450 +1,528 @@ -import { makeSVGGroup } from '../utils/draw'; -import { makeText, makePath, xLine, yLine, yMarker, yRegion, datasetBar, datasetDot, percentageBar, getPaths, heatSquare } from '../utils/draw'; -import { equilizeNoOfElements } from '../utils/draw-utils'; -import { translateHoriLine, translateVertLine, animateRegion, animateBar, - animateDot, animatePath, animatePathStr } from '../utils/animate'; -import { getMonthName } from '../utils/date-utils'; +import { makeSVGGroup } from "../utils/draw"; +import { + makeText, + makePath, + xLine, + yLine, + yMarker, + yRegion, + datasetBar, + datasetDot, + percentageBar, + getPaths, + heatSquare, +} from "../utils/draw"; +import { equilizeNoOfElements } from "../utils/draw-utils"; +import { + translateHoriLine, + translateVertLine, + animateRegion, + animateBar, + animateDot, + animatePath, + animatePathStr, +} from "../utils/animate"; +import { getMonthName } from "../utils/date-utils"; class ChartComponent { - constructor({ - layerClass = '', - layerTransform = '', - constants, + constructor({ + layerClass = "", + layerTransform = "", + constants, - getData, - makeElements, - animateElements - }) { - this.layerTransform = layerTransform; - this.constants = constants; + getData, + makeElements, + animateElements, + }) { + this.layerTransform = layerTransform; + this.constants = constants; - this.makeElements = makeElements; - this.getData = getData; + this.makeElements = makeElements; + this.getData = getData; - this.animateElements = animateElements; + this.animateElements = animateElements; - this.store = []; - this.labels = []; + this.store = []; + this.labels = []; - this.layerClass = layerClass; - this.layerClass = typeof(this.layerClass) === 'function' - ? this.layerClass() : this.layerClass; + this.layerClass = layerClass; + this.layerClass = + typeof this.layerClass === "function" + ? this.layerClass() + : this.layerClass; - this.refresh(); - } + this.refresh(); + } - refresh(data) { - this.data = data || this.getData(); - } + refresh(data) { + this.data = data || this.getData(); + } - setup(parent) { - this.layer = makeSVGGroup(this.layerClass, this.layerTransform, parent); - } + setup(parent) { + this.layer = makeSVGGroup(this.layerClass, this.layerTransform, parent); + } - make() { - this.render(this.data); - this.oldData = this.data; - } + make() { + this.render(this.data); + this.oldData = this.data; + } - render(data) { - this.store = this.makeElements(data); + render(data) { + this.store = this.makeElements(data); - this.layer.textContent = ''; - this.store.forEach(element => { - this.layer.appendChild(element); - }); - this.labels.forEach(element => { - this.layer.appendChild(element); - }); - } + this.layer.textContent = ""; + this.store.forEach((element) => { + this.layer.appendChild(element); + }); + this.labels.forEach((element) => { + this.layer.appendChild(element); + }); + } - update(animate = true) { - this.refresh(); - let animateElements = []; - if(animate) { - animateElements = this.animateElements(this.data) || []; - } - return animateElements; - } + update(animate = true) { + this.refresh(); + let animateElements = []; + if (animate) { + animateElements = this.animateElements(this.data) || []; + } + return animateElements; + } } let componentConfigs = { - donutSlices: { - layerClass: 'donut-slices', - makeElements(data) { - return data.sliceStrings.map((s, i) => { - let slice = makePath(s, 'donut-path', data.colors[i], 'none', data.strokeWidth); - slice.style.transition = 'transform .3s;'; - return slice; - }); - }, + donutSlices: { + layerClass: "donut-slices", + makeElements(data) { + return data.sliceStrings.map((s, i) => { + let slice = makePath( + s, + "donut-path", + data.colors[i], + "none", + data.strokeWidth + ); + slice.style.transition = "transform .3s;"; + return slice; + }); + }, - animateElements(newData) { - return this.store.map((slice, i) => animatePathStr(slice, newData.sliceStrings[i])); - }, - }, - pieSlices: { - layerClass: 'pie-slices', - makeElements(data) { - return data.sliceStrings.map((s, i) =>{ - let slice = makePath(s, 'pie-path', 'none', data.colors[i]); - slice.style.transition = 'transform .3s;'; - return slice; - }); - }, + animateElements(newData) { + return this.store.map((slice, i) => + animatePathStr(slice, newData.sliceStrings[i]) + ); + }, + }, + pieSlices: { + layerClass: "pie-slices", + makeElements(data) { + return data.sliceStrings.map((s, i) => { + let slice = makePath(s, "pie-path", "none", data.colors[i]); + slice.style.transition = "transform .3s;"; + return slice; + }); + }, - animateElements(newData) { - return this.store.map((slice, i) => - animatePathStr(slice, newData.sliceStrings[i]) - ); - } - }, - percentageBars: { - layerClass: 'percentage-bars', - makeElements(data) { - const numberOfPoints = data.xPositions.length; - return data.xPositions.map((x, i) =>{ - let y = 0; + animateElements(newData) { + return this.store.map((slice, i) => + animatePathStr(slice, newData.sliceStrings[i]) + ); + }, + }, + percentageBars: { + layerClass: "percentage-bars", + makeElements(data) { + const numberOfPoints = data.xPositions.length; + return data.xPositions.map((x, i) => { + let y = 0; - let isLast = i == numberOfPoints - 1; - let isFirst = i == 0; + let isLast = i == numberOfPoints - 1; + let isFirst = i == 0; - let bar = percentageBar(x, y, data.widths[i], this.constants.barHeight, isFirst, isLast, data.colors[i]); - return bar; - }); - }, + let bar = percentageBar( + x, + y, + data.widths[i], + this.constants.barHeight, + isFirst, + isLast, + data.colors[i] + ); + return bar; + }); + }, - animateElements(newData) { - if(newData) return []; - } - }, - yAxis: { - layerClass: 'y axis', - makeElements(data) { - return data.positions.map((position, i) => - yLine(position, data.labels[i], this.constants.width, - {mode: this.constants.mode, pos: this.constants.pos, shortenNumbers: this.constants.shortenNumbers}) - ); - }, + animateElements(newData) { + if (newData) return []; + }, + }, + yAxis: { + layerClass: "y axis", + makeElements(data) { + return data.positions.map((position, i) => + yLine(position, data.labels[i], this.constants.width, { + mode: this.constants.mode, + pos: this.constants.pos, + shortenNumbers: this.constants.shortenNumbers, + }) + ); + }, - animateElements(newData) { - let newPos = newData.positions; - let newLabels = newData.labels; - let oldPos = this.oldData.positions; - let oldLabels = this.oldData.labels; + animateElements(newData) { + let newPos = newData.positions; + let newLabels = newData.labels; + let oldPos = this.oldData.positions; + let oldLabels = this.oldData.labels; - [oldPos, newPos] = equilizeNoOfElements(oldPos, newPos); - [oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels); + [oldPos, newPos] = equilizeNoOfElements(oldPos, newPos); + [oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels); - this.render({ - positions: oldPos, - labels: newLabels - }); + this.render({ + positions: oldPos, + labels: newLabels, + }); - return this.store.map((line, i) => { - return translateHoriLine( - line, newPos[i], oldPos[i] - ); - }); - } - }, + return this.store.map((line, i) => { + return translateHoriLine(line, newPos[i], oldPos[i]); + }); + }, + }, - xAxis: { - layerClass: 'x axis', - makeElements(data) { - return data.positions.map((position, i) => - xLine(position, data.calcLabels[i], this.constants.height, - {mode: this.constants.mode, pos: this.constants.pos}) - ); - }, + xAxis: { + layerClass: "x axis", + makeElements(data) { + return data.positions.map((position, i) => + xLine(position, data.calcLabels[i], this.constants.height, { + mode: this.constants.mode, + pos: this.constants.pos, + }) + ); + }, - animateElements(newData) { - let newPos = newData.positions; - let newLabels = newData.calcLabels; - let oldPos = this.oldData.positions; - let oldLabels = this.oldData.calcLabels; + animateElements(newData) { + let newPos = newData.positions; + let newLabels = newData.calcLabels; + let oldPos = this.oldData.positions; + let oldLabels = this.oldData.calcLabels; - [oldPos, newPos] = equilizeNoOfElements(oldPos, newPos); - [oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels); + [oldPos, newPos] = equilizeNoOfElements(oldPos, newPos); + [oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels); - this.render({ - positions: oldPos, - calcLabels: newLabels - }); + this.render({ + positions: oldPos, + calcLabels: newLabels, + }); - return this.store.map((line, i) => { - return translateVertLine( - line, newPos[i], oldPos[i] - ); - }); - } - }, + return this.store.map((line, i) => { + return translateVertLine(line, newPos[i], oldPos[i]); + }); + }, + }, - yMarkers: { - layerClass: 'y-markers', - makeElements(data) { - return data.map(m => - yMarker(m.position, m.label, this.constants.width, - {labelPos: m.options.labelPos, mode: 'span', lineType: 'dashed'}) - ); - }, - animateElements(newData) { - [this.oldData, newData] = equilizeNoOfElements(this.oldData, newData); + yMarkers: { + layerClass: "y-markers", + makeElements(data) { + return data.map((m) => + yMarker(m.position, m.label, this.constants.width, { + labelPos: m.options.labelPos, + mode: "span", + lineType: "dashed", + }) + ); + }, + animateElements(newData) { + [this.oldData, newData] = equilizeNoOfElements(this.oldData, newData); - let newPos = newData.map(d => d.position); - let newLabels = newData.map(d => d.label); - let newOptions = newData.map(d => d.options); + let newPos = newData.map((d) => d.position); + let newLabels = newData.map((d) => d.label); + let newOptions = newData.map((d) => d.options); - let oldPos = this.oldData.map(d => d.position); + let oldPos = this.oldData.map((d) => d.position); - this.render(oldPos.map((pos, i) => { - return { - position: oldPos[i], - label: newLabels[i], - options: newOptions[i] - }; - })); + this.render( + oldPos.map((pos, i) => { + return { + position: oldPos[i], + label: newLabels[i], + options: newOptions[i], + }; + }) + ); - return this.store.map((line, i) => { - return translateHoriLine( - line, newPos[i], oldPos[i] - ); - }); - } - }, + return this.store.map((line, i) => { + return translateHoriLine(line, newPos[i], oldPos[i]); + }); + }, + }, - yRegions: { - layerClass: 'y-regions', - makeElements(data) { - return data.map(r => - yRegion(r.startPos, r.endPos, this.constants.width, - r.label, {labelPos: r.options.labelPos}) - ); - }, - animateElements(newData) { - [this.oldData, newData] = equilizeNoOfElements(this.oldData, newData); + yRegions: { + layerClass: "y-regions", + makeElements(data) { + return data.map((r) => + yRegion(r.startPos, r.endPos, this.constants.width, r.label, { + labelPos: r.options.labelPos, + }) + ); + }, + animateElements(newData) { + [this.oldData, newData] = equilizeNoOfElements(this.oldData, newData); - let newPos = newData.map(d => d.endPos); - let newLabels = newData.map(d => d.label); - let newStarts = newData.map(d => d.startPos); - let newOptions = newData.map(d => d.options); + let newPos = newData.map((d) => d.endPos); + let newLabels = newData.map((d) => d.label); + let newStarts = newData.map((d) => d.startPos); + let newOptions = newData.map((d) => d.options); - let oldPos = this.oldData.map(d => d.endPos); - let oldStarts = this.oldData.map(d => d.startPos); + let oldPos = this.oldData.map((d) => d.endPos); + let oldStarts = this.oldData.map((d) => d.startPos); - this.render(oldPos.map((pos, i) => { - return { - startPos: oldStarts[i], - endPos: oldPos[i], - label: newLabels[i], - options: newOptions[i] - }; - })); + this.render( + oldPos.map((pos, i) => { + return { + startPos: oldStarts[i], + endPos: oldPos[i], + label: newLabels[i], + options: newOptions[i], + }; + }) + ); - let animateElements = []; + let animateElements = []; - this.store.map((rectGroup, i) => { - animateElements = animateElements.concat(animateRegion( - rectGroup, newStarts[i], newPos[i], oldPos[i] - )); - }); + this.store.map((rectGroup, i) => { + animateElements = animateElements.concat( + animateRegion(rectGroup, newStarts[i], newPos[i], oldPos[i]) + ); + }); - return animateElements; - } - }, + return animateElements; + }, + }, - heatDomain: { - layerClass: function() { return 'heat-domain domain-' + this.constants.index; }, - makeElements(data) { - let {index, colWidth, rowHeight, squareSize, radius, xTranslate} = this.constants; - let monthNameHeight = -12; - let x = xTranslate, y = 0; + heatDomain: { + layerClass: function () { + return "heat-domain domain-" + this.constants.index; + }, + makeElements(data) { + let { index, colWidth, rowHeight, squareSize, radius, xTranslate } = + this.constants; + let monthNameHeight = -12; + let x = xTranslate, + y = 0; - this.serializedSubDomains = []; + this.serializedSubDomains = []; - data.cols.map((week, weekNo) => { - if(weekNo === 1) { - this.labels.push( - makeText('domain-name', x, monthNameHeight, getMonthName(index, true).toUpperCase(), - { - fontSize: 9 - } - ) - ); - } - week.map((day, i) => { - if(day.fill) { - let data = { - 'data-date': day.yyyyMmDd, - 'data-value': day.dataValue, - 'data-day': i - }; - let square = heatSquare('day', x, y, squareSize, radius, day.fill, data); - this.serializedSubDomains.push(square); - } - y += rowHeight; - }); - y = 0; - x += colWidth; - }); + data.cols.map((week, weekNo) => { + if (weekNo === 1) { + this.labels.push( + makeText( + "domain-name", + x, + monthNameHeight, + getMonthName(index, true).toUpperCase(), + { + fontSize: 9, + } + ) + ); + } + week.map((day, i) => { + if (day.fill) { + let data = { + "data-date": day.yyyyMmDd, + "data-value": day.dataValue, + "data-day": i, + }; + let square = heatSquare( + "day", + x, + y, + squareSize, + radius, + day.fill, + data + ); + this.serializedSubDomains.push(square); + } + y += rowHeight; + }); + y = 0; + x += colWidth; + }); - return this.serializedSubDomains; - }, + return this.serializedSubDomains; + }, - animateElements(newData) { - if(newData) return []; - } - }, + animateElements(newData) { + if (newData) return []; + }, + }, - barGraph: { - layerClass: function() { return 'dataset-units dataset-bars dataset-' + this.constants.index; }, - makeElements(data) { - let c = this.constants; - this.unitType = 'bar'; - this.units = data.yPositions.map((y, j) => { - return datasetBar( - data.xPositions[j], - y, - data.barWidth, - c.color, - data.labels[j], - j, - data.offsets[j], - { - zeroLine: data.zeroLine, - barsWidth: data.barsWidth, - minHeight: c.minHeight - } - ); - }); - return this.units; - }, - animateElements(newData) { - let newXPos = newData.xPositions; - let newYPos = newData.yPositions; - let newOffsets = newData.offsets; - let newLabels = newData.labels; + barGraph: { + layerClass: function () { + return "dataset-units dataset-bars dataset-" + this.constants.index; + }, + makeElements(data) { + let c = this.constants; + this.unitType = "bar"; + this.units = data.yPositions.map((y, j) => { + return datasetBar( + data.xPositions[j], + y, + data.barWidth, + c.color, + data.labels[j], + j, + data.offsets[j], + { + zeroLine: data.zeroLine, + barsWidth: data.barsWidth, + minHeight: c.minHeight, + } + ); + }); + return this.units; + }, + animateElements(newData) { + let newXPos = newData.xPositions; + let newYPos = newData.yPositions; + let newOffsets = newData.offsets; + let newLabels = newData.labels; - let oldXPos = this.oldData.xPositions; - let oldYPos = this.oldData.yPositions; - let oldOffsets = this.oldData.offsets; - let oldLabels = this.oldData.labels; + let oldXPos = this.oldData.xPositions; + let oldYPos = this.oldData.yPositions; + let oldOffsets = this.oldData.offsets; + let oldLabels = this.oldData.labels; - [oldXPos, newXPos] = equilizeNoOfElements(oldXPos, newXPos); - [oldYPos, newYPos] = equilizeNoOfElements(oldYPos, newYPos); - [oldOffsets, newOffsets] = equilizeNoOfElements(oldOffsets, newOffsets); - [oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels); + [oldXPos, newXPos] = equilizeNoOfElements(oldXPos, newXPos); + [oldYPos, newYPos] = equilizeNoOfElements(oldYPos, newYPos); + [oldOffsets, newOffsets] = equilizeNoOfElements(oldOffsets, newOffsets); + [oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels); - this.render({ - xPositions: oldXPos, - yPositions: oldYPos, - offsets: oldOffsets, - labels: newLabels, + this.render({ + xPositions: oldXPos, + yPositions: oldYPos, + offsets: oldOffsets, + labels: newLabels, - zeroLine: this.oldData.zeroLine, - barsWidth: this.oldData.barsWidth, - barWidth: this.oldData.barWidth, - }); + zeroLine: this.oldData.zeroLine, + barsWidth: this.oldData.barsWidth, + barWidth: this.oldData.barWidth, + }); - let animateElements = []; + let animateElements = []; - this.store.map((bar, i) => { - animateElements = animateElements.concat(animateBar( - bar, newXPos[i], newYPos[i], newData.barWidth, newOffsets[i], - {zeroLine: newData.zeroLine} - )); - }); + this.store.map((bar, i) => { + animateElements = animateElements.concat( + animateBar( + bar, + newXPos[i], + newYPos[i], + newData.barWidth, + newOffsets[i], + { zeroLine: newData.zeroLine } + ) + ); + }); - return animateElements; - } - }, + return animateElements; + }, + }, - lineGraph: { - layerClass: function() { return 'dataset-units dataset-line dataset-' + this.constants.index; }, - makeElements(data) { - let c = this.constants; - this.unitType = 'dot'; - this.paths = {}; - if(!c.hideLine) { - this.paths = getPaths( - data.xPositions, - data.yPositions, - c.color, - { - heatline: c.heatline, - regionFill: c.regionFill, - spline: c.spline - }, - { - svgDefs: c.svgDefs, - zeroLine: data.zeroLine - } - ); - } + lineGraph: { + layerClass: function () { + return "dataset-units dataset-line dataset-" + this.constants.index; + }, + makeElements(data) { + let c = this.constants; + this.unitType = "dot"; + this.paths = {}; + if (!c.hideLine) { + this.paths = getPaths( + data.xPositions, + data.yPositions, + c.color, + { + heatline: c.heatline, + regionFill: c.regionFill, + spline: c.spline, + }, + { + svgDefs: c.svgDefs, + zeroLine: data.zeroLine, + } + ); + } - this.units = []; - if(!c.hideDots) { - this.units = data.yPositions.map((y, j) => { - return datasetDot( - data.xPositions[j], - y, - data.radius, - c.color, - (c.valuesOverPoints ? data.values[j] : ''), - j - ); - }); - } + this.units = []; + if (!c.hideDots) { + this.units = data.yPositions.map((y, j) => { + return datasetDot( + data.xPositions[j], + y, + data.radius, + c.color, + c.valuesOverPoints ? data.values[j] : "", + j + ); + }); + } - return Object.values(this.paths).concat(this.units); - }, - animateElements(newData) { - let newXPos = newData.xPositions; - let newYPos = newData.yPositions; - let newValues = newData.values; + return Object.values(this.paths).concat(this.units); + }, + animateElements(newData) { + let newXPos = newData.xPositions; + let newYPos = newData.yPositions; + let newValues = newData.values; - let oldXPos = this.oldData.xPositions; - let oldYPos = this.oldData.yPositions; - let oldValues = this.oldData.values; + let oldXPos = this.oldData.xPositions; + let oldYPos = this.oldData.yPositions; + let oldValues = this.oldData.values; - [oldXPos, newXPos] = equilizeNoOfElements(oldXPos, newXPos); - [oldYPos, newYPos] = equilizeNoOfElements(oldYPos, newYPos); - [oldValues, newValues] = equilizeNoOfElements(oldValues, newValues); + [oldXPos, newXPos] = equilizeNoOfElements(oldXPos, newXPos); + [oldYPos, newYPos] = equilizeNoOfElements(oldYPos, newYPos); + [oldValues, newValues] = equilizeNoOfElements(oldValues, newValues); - this.render({ - xPositions: oldXPos, - yPositions: oldYPos, - values: newValues, + this.render({ + xPositions: oldXPos, + yPositions: oldYPos, + values: newValues, - zeroLine: this.oldData.zeroLine, - radius: this.oldData.radius, - }); + zeroLine: this.oldData.zeroLine, + radius: this.oldData.radius, + }); - let animateElements = []; + let animateElements = []; - if(Object.keys(this.paths).length) { - animateElements = animateElements.concat(animatePath( - this.paths, newXPos, newYPos, newData.zeroLine, this.constants.spline)); - } + if (Object.keys(this.paths).length) { + animateElements = animateElements.concat( + animatePath( + this.paths, + newXPos, + newYPos, + newData.zeroLine, + this.constants.spline + ) + ); + } - if(this.units.length) { - this.units.map((dot, i) => { - animateElements = animateElements.concat(animateDot( - dot, newXPos[i], newYPos[i])); - }); - } + if (this.units.length) { + this.units.map((dot, i) => { + animateElements = animateElements.concat( + animateDot(dot, newXPos[i], newYPos[i]) + ); + }); + } - return animateElements; - } - } + return animateElements; + }, + }, }; export function getComponent(name, constants, getData) { - let keys = Object.keys(componentConfigs).filter(k => name.includes(k)); - let config = componentConfigs[keys[0]]; - Object.assign(config, { - constants: constants, - getData: getData - }); - return new ChartComponent(config); + let keys = Object.keys(componentConfigs).filter((k) => name.includes(k)); + let config = componentConfigs[keys[0]]; + Object.assign(config, { + constants: constants, + getData: getData, + }); + return new ChartComponent(config); } diff --git a/src/js/objects/SvgTip.js b/src/js/objects/SvgTip.js index 5dec933..8e9a0d7 100644 --- a/src/js/objects/SvgTip.js +++ b/src/js/objects/SvgTip.js @@ -1,134 +1,132 @@ -import { $ } from '../utils/dom'; -import { TOOLTIP_POINTER_TRIANGLE_HEIGHT } from '../utils/constants'; +import { $ } from "../utils/dom"; +import { TOOLTIP_POINTER_TRIANGLE_HEIGHT } from "../utils/constants"; export default class SvgTip { - constructor({ - parent = null, - colors = [] - }) { - this.parent = parent; - this.colors = colors; - this.titleName = ''; - this.titleValue = ''; - this.listValues = []; - this.titleValueFirst = 0; + constructor({ parent = null, colors = [] }) { + this.parent = parent; + this.colors = colors; + this.titleName = ""; + this.titleValue = ""; + this.listValues = []; + this.titleValueFirst = 0; - this.x = 0; - this.y = 0; + this.x = 0; + this.y = 0; - this.top = 0; - this.left = 0; + this.top = 0; + this.left = 0; - this.setup(); - } + this.setup(); + } - setup() { - this.makeTooltip(); - } + setup() { + this.makeTooltip(); + } - refresh() { - this.fill(); - this.calcPosition(); - } + refresh() { + this.fill(); + this.calcPosition(); + } - makeTooltip() { - this.container = $.create('div', { - inside: this.parent, - className: 'graph-svg-tip comparison', - innerHTML: ` + makeTooltip() { + this.container = $.create("div", { + inside: this.parent, + className: "graph-svg-tip comparison", + innerHTML: ` -
` - }); - this.hideTip(); +
`, + }); + this.hideTip(); - this.title = this.container.querySelector('.title'); - this.list = this.container.querySelector('.data-point-list'); - this.dataPointList = this.container.querySelector('.data-point-list'); + this.title = this.container.querySelector(".title"); + this.list = this.container.querySelector(".data-point-list"); + this.dataPointList = this.container.querySelector(".data-point-list"); - this.parent.addEventListener('mouseleave', () => { - this.hideTip(); - }); - } + this.parent.addEventListener("mouseleave", () => { + this.hideTip(); + }); + } - fill() { - let title; - if(this.index) { - this.container.setAttribute('data-point-index', this.index); - } - if(this.titleValueFirst) { - title = `${this.titleValue}${this.titleName}`; - } else { - title = `${this.titleName}${this.titleValue}`; - } + fill() { + let title; + if (this.index) { + this.container.setAttribute("data-point-index", this.index); + } + if (this.titleValueFirst) { + title = `${this.titleValue}${this.titleName}`; + } else { + title = `${this.titleName}${this.titleValue}`; + } - if (this.listValues.length > 4) { - this.list.classList.add('tooltip-grid'); - } else { - this.list.classList.remove('tooltip-grid'); - } + if (this.listValues.length > 4) { + this.list.classList.add("tooltip-grid"); + } else { + this.list.classList.remove("tooltip-grid"); + } - this.title.innerHTML = title; - this.dataPointList.innerHTML = ''; + this.title.innerHTML = title; + this.dataPointList.innerHTML = ""; - this.listValues.map((set, i) => { - const color = this.colors[i] || 'black'; - let value = set.formatted === 0 || set.formatted ? set.formatted : set.value; - let li = $.create('li', { - innerHTML: `
+ this.listValues.map((set, i) => { + const color = this.colors[i] || "black"; + let value = + set.formatted === 0 || set.formatted ? set.formatted : set.value; + let li = $.create("li", { + innerHTML: `
-
${ value === 0 || value ? value : '' }
-
${set.title ? set.title : '' }
-
` - }); +
${value === 0 || value ? value : ""}
+
${set.title ? set.title : ""}
+ `, + }); - this.dataPointList.appendChild(li); - }); - } + this.dataPointList.appendChild(li); + }); + } - calcPosition() { - let width = this.container.offsetWidth; + calcPosition() { + let width = this.container.offsetWidth; - this.top = this.y - this.container.offsetHeight - - TOOLTIP_POINTER_TRIANGLE_HEIGHT; - this.left = this.x - width/2; - let maxLeft = this.parent.offsetWidth - width; + this.top = + this.y - this.container.offsetHeight - TOOLTIP_POINTER_TRIANGLE_HEIGHT; + this.left = this.x - width / 2; + let maxLeft = this.parent.offsetWidth - width; - let pointer = this.container.querySelector('.svg-pointer'); + let pointer = this.container.querySelector(".svg-pointer"); - if(this.left < 0) { - pointer.style.left = `calc(50% - ${-1 * this.left}px)`; - this.left = 0; - } else if(this.left > maxLeft) { - let delta = this.left - maxLeft; - let pointerOffset = `calc(50% + ${delta}px)`; - pointer.style.left = pointerOffset; + if (this.left < 0) { + pointer.style.left = `calc(50% - ${-1 * this.left}px)`; + this.left = 0; + } else if (this.left > maxLeft) { + let delta = this.left - maxLeft; + let pointerOffset = `calc(50% + ${delta}px)`; + pointer.style.left = pointerOffset; - this.left = maxLeft; - } else { - pointer.style.left = `50%`; - } - } + this.left = maxLeft; + } else { + pointer.style.left = `50%`; + } + } - setValues(x, y, title = {}, listValues = [], index = -1) { - this.titleName = title.name; - this.titleValue = title.value; - this.listValues = listValues; - this.x = x; - this.y = y; - this.titleValueFirst = title.valueFirst || 0; - this.index = index; - this.refresh(); - } + setValues(x, y, title = {}, listValues = [], index = -1) { + this.titleName = title.name; + this.titleValue = title.value; + this.listValues = listValues; + this.x = x; + this.y = y; + this.titleValueFirst = title.valueFirst || 0; + this.index = index; + this.refresh(); + } - hideTip() { - this.container.style.top = '0px'; - this.container.style.left = '0px'; - this.container.style.opacity = '0'; - } + hideTip() { + this.container.style.top = "0px"; + this.container.style.left = "0px"; + this.container.style.opacity = "0"; + } - showTip() { - this.container.style.top = this.top + 'px'; - this.container.style.left = this.left + 'px'; - this.container.style.opacity = '1'; - } + showTip() { + this.container.style.top = this.top + "px"; + this.container.style.left = this.left + "px"; + this.container.style.opacity = "1"; + } } diff --git a/src/js/utils/animate.js b/src/js/utils/animate.js index 2cc6c8e..960a98c 100644 --- a/src/js/utils/animate.js +++ b/src/js/utils/animate.js @@ -1,105 +1,121 @@ -import { getBarHeightAndYAttr, getSplineCurvePointsStr } from './draw-utils'; +import { getBarHeightAndYAttr, getSplineCurvePointsStr } from "./draw-utils"; export const UNIT_ANIM_DUR = 350; export const PATH_ANIM_DUR = 350; export const MARKER_LINE_ANIM_DUR = UNIT_ANIM_DUR; export const REPLACE_ALL_NEW_DUR = 250; -export const STD_EASING = 'easein'; +export const STD_EASING = "easein"; export function translate(unit, oldCoord, newCoord, duration) { - let old = typeof oldCoord === 'string' ? oldCoord : oldCoord.join(', '); - return [ - unit, - {transform: newCoord.join(', ')}, - duration, - STD_EASING, - "translate", - {transform: old} - ]; + let old = typeof oldCoord === "string" ? oldCoord : oldCoord.join(", "); + return [ + unit, + { transform: newCoord.join(", ") }, + duration, + STD_EASING, + "translate", + { transform: old }, + ]; } export function translateVertLine(xLine, newX, oldX) { - return translate(xLine, [oldX, 0], [newX, 0], MARKER_LINE_ANIM_DUR); + return translate(xLine, [oldX, 0], [newX, 0], MARKER_LINE_ANIM_DUR); } export function translateHoriLine(yLine, newY, oldY) { - return translate(yLine, [0, oldY], [0, newY], MARKER_LINE_ANIM_DUR); + return translate(yLine, [0, oldY], [0, newY], MARKER_LINE_ANIM_DUR); } export function animateRegion(rectGroup, newY1, newY2, oldY2) { - let newHeight = newY1 - newY2; - let rect = rectGroup.childNodes[0]; - let width = rect.getAttribute("width"); - let rectAnim = [ - rect, - { height: newHeight, 'stroke-dasharray': `${width}, ${newHeight}` }, - MARKER_LINE_ANIM_DUR, - STD_EASING - ]; + let newHeight = newY1 - newY2; + let rect = rectGroup.childNodes[0]; + let width = rect.getAttribute("width"); + let rectAnim = [ + rect, + { height: newHeight, "stroke-dasharray": `${width}, ${newHeight}` }, + MARKER_LINE_ANIM_DUR, + STD_EASING, + ]; - let groupAnim = translate(rectGroup, [0, oldY2], [0, newY2], MARKER_LINE_ANIM_DUR); - return [rectAnim, groupAnim]; + let groupAnim = translate( + rectGroup, + [0, oldY2], + [0, newY2], + MARKER_LINE_ANIM_DUR + ); + return [rectAnim, groupAnim]; } -export function animateBar(bar, x, yTop, width, offset=0, meta={}) { - let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine); - y -= offset; - if(bar.nodeName !== 'rect') { - let rect = bar.childNodes[0]; - let rectAnim = [ - rect, - {width: width, height: height}, - UNIT_ANIM_DUR, - STD_EASING - ]; +export function animateBar(bar, x, yTop, width, offset = 0, meta = {}) { + let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine); + y -= offset; + if (bar.nodeName !== "rect") { + let rect = bar.childNodes[0]; + let rectAnim = [ + rect, + { width: width, height: height }, + UNIT_ANIM_DUR, + STD_EASING, + ]; - let oldCoordStr = bar.getAttribute("transform").split("(")[1].slice(0, -1); - let groupAnim = translate(bar, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR); - return [rectAnim, groupAnim]; - } else { - return [[bar, {width: width, height: height, x: x, y: y}, UNIT_ANIM_DUR, STD_EASING]]; - } - // bar.animate({height: args.newHeight, y: yTop}, UNIT_ANIM_DUR, mina.easein); + let oldCoordStr = bar.getAttribute("transform").split("(")[1].slice(0, -1); + let groupAnim = translate(bar, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR); + return [rectAnim, groupAnim]; + } else { + return [ + [ + bar, + { width: width, height: height, x: x, y: y }, + UNIT_ANIM_DUR, + STD_EASING, + ], + ]; + } + // bar.animate({height: args.newHeight, y: yTop}, UNIT_ANIM_DUR, mina.easein); } export function animateDot(dot, x, y) { - if(dot.nodeName !== 'circle') { - let oldCoordStr = dot.getAttribute("transform").split("(")[1].slice(0, -1); - let groupAnim = translate(dot, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR); - return [groupAnim]; - } else { - return [[dot, {cx: x, cy: y}, UNIT_ANIM_DUR, STD_EASING]]; - } - // dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein); + if (dot.nodeName !== "circle") { + let oldCoordStr = dot.getAttribute("transform").split("(")[1].slice(0, -1); + let groupAnim = translate(dot, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR); + return [groupAnim]; + } else { + return [[dot, { cx: x, cy: y }, UNIT_ANIM_DUR, STD_EASING]]; + } + // dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein); } export function animatePath(paths, newXList, newYList, zeroLine, spline) { - let pathComponents = []; - let pointsStr = newYList.map((y, i) => (newXList[i] + ',' + y)).join("L"); + let pathComponents = []; + let pointsStr = newYList.map((y, i) => newXList[i] + "," + y).join("L"); - if (spline) - pointsStr = getSplineCurvePointsStr(newXList, newYList); + if (spline) pointsStr = getSplineCurvePointsStr(newXList, newYList); - const animPath = [paths.path, {d:"M" + pointsStr}, PATH_ANIM_DUR, STD_EASING]; - pathComponents.push(animPath); + const animPath = [ + paths.path, + { d: "M" + pointsStr }, + PATH_ANIM_DUR, + STD_EASING, + ]; + pathComponents.push(animPath); - if(paths.region) { - let regStartPt = `${newXList[0]},${zeroLine}L`; - let regEndPt = `L${newXList.slice(-1)[0]}, ${zeroLine}`; + if (paths.region) { + let regStartPt = `${newXList[0]},${zeroLine}L`; + let regEndPt = `L${newXList.slice(-1)[0]}, ${zeroLine}`; - const animRegion = [ - paths.region, - {d:"M" + regStartPt + pointsStr + regEndPt}, - PATH_ANIM_DUR, - STD_EASING - ]; - pathComponents.push(animRegion); - } + const animRegion = [ + paths.region, + { d: "M" + regStartPt + pointsStr + regEndPt }, + PATH_ANIM_DUR, + STD_EASING, + ]; + pathComponents.push(animRegion); + } - return pathComponents; + return pathComponents; } export function animatePathStr(oldPath, pathStr) { - return [oldPath, {d: pathStr}, UNIT_ANIM_DUR, STD_EASING]; + return [oldPath, { d: pathStr }, UNIT_ANIM_DUR, STD_EASING]; } diff --git a/src/js/utils/animation.js b/src/js/utils/animation.js index 19af5fe..d624599 100644 --- a/src/js/utils/animation.js +++ b/src/js/utils/animation.js @@ -1,120 +1,133 @@ // Leveraging SMIL Animations -import { REPLACE_ALL_NEW_DUR } from './animate'; +import { REPLACE_ALL_NEW_DUR } from "./animate"; const EASING = { - ease: "0.25 0.1 0.25 1", - linear: "0 0 1 1", - // easein: "0.42 0 1 1", - easein: "0.1 0.8 0.2 1", - easeout: "0 0 0.58 1", - easeinout: "0.42 0 0.58 1" + ease: "0.25 0.1 0.25 1", + linear: "0 0 1 1", + // easein: "0.42 0 1 1", + easein: "0.1 0.8 0.2 1", + easeout: "0 0 0.58 1", + easeinout: "0.42 0 0.58 1", }; -function animateSVGElement(element, props, dur, easingType="linear", type=undefined, oldValues={}) { +function animateSVGElement( + element, + props, + dur, + easingType = "linear", + type = undefined, + oldValues = {} +) { + let animElement = element.cloneNode(true); + let newElement = element.cloneNode(true); - let animElement = element.cloneNode(true); - let newElement = element.cloneNode(true); + for (var attributeName in props) { + let animateElement; + if (attributeName === "transform") { + animateElement = document.createElementNS( + "http://www.w3.org/2000/svg", + "animateTransform" + ); + } else { + animateElement = document.createElementNS( + "http://www.w3.org/2000/svg", + "animate" + ); + } + let currentValue = + oldValues[attributeName] || element.getAttribute(attributeName); + let value = props[attributeName]; - for(var attributeName in props) { - let animateElement; - if(attributeName === 'transform') { - animateElement = document.createElementNS("http://www.w3.org/2000/svg", "animateTransform"); - } else { - animateElement = document.createElementNS("http://www.w3.org/2000/svg", "animate"); - } - let currentValue = oldValues[attributeName] || element.getAttribute(attributeName); - let value = props[attributeName]; + let animAttr = { + attributeName: attributeName, + from: currentValue, + to: value, + begin: "0s", + dur: dur / 1000 + "s", + values: currentValue + ";" + value, + keySplines: EASING[easingType], + keyTimes: "0;1", + calcMode: "spline", + fill: "freeze", + }; - let animAttr = { - attributeName: attributeName, - from: currentValue, - to: value, - begin: "0s", - dur: dur/1000 + "s", - values: currentValue + ";" + value, - keySplines: EASING[easingType], - keyTimes: "0;1", - calcMode: "spline", - fill: 'freeze' - }; + if (type) { + animAttr["type"] = type; + } - if(type) { - animAttr["type"] = type; - } + for (var i in animAttr) { + animateElement.setAttribute(i, animAttr[i]); + } - for (var i in animAttr) { - animateElement.setAttribute(i, animAttr[i]); - } + animElement.appendChild(animateElement); - animElement.appendChild(animateElement); + if (type) { + newElement.setAttribute(attributeName, `translate(${value})`); + } else { + newElement.setAttribute(attributeName, value); + } + } - if(type) { - newElement.setAttribute(attributeName, `translate(${value})`); - } else { - newElement.setAttribute(attributeName, value); - } - } - - return [animElement, newElement]; + return [animElement, newElement]; } -export function transform(element, style) { // eslint-disable-line no-unused-vars - element.style.transform = style; - element.style.webkitTransform = style; - element.style.msTransform = style; - element.style.mozTransform = style; - element.style.oTransform = style; +export function transform(element, style) { + // eslint-disable-line no-unused-vars + element.style.transform = style; + element.style.webkitTransform = style; + element.style.msTransform = style; + element.style.mozTransform = style; + element.style.oTransform = style; } function animateSVG(svgContainer, elements) { - let newElements = []; - let animElements = []; + let newElements = []; + let animElements = []; - elements.map(element => { - let unit = element[0]; - let parent = unit.parentNode; + elements.map((element) => { + let unit = element[0]; + let parent = unit.parentNode; - let animElement, newElement; + let animElement, newElement; - element[0] = unit; - [animElement, newElement] = animateSVGElement(...element); + element[0] = unit; + [animElement, newElement] = animateSVGElement(...element); - newElements.push(newElement); - animElements.push([animElement, parent]); - - if (parent) { - parent.replaceChild(animElement, unit); - } - }); + newElements.push(newElement); + animElements.push([animElement, parent]); - let animSvg = svgContainer.cloneNode(true); + if (parent) { + parent.replaceChild(animElement, unit); + } + }); - animElements.map((animElement, i) => { - if (animElement[1]) { - animElement[1].replaceChild(newElements[i], animElement[0]); - elements[i][0] = newElements[i]; - } - }); + let animSvg = svgContainer.cloneNode(true); - return animSvg; + animElements.map((animElement, i) => { + if (animElement[1]) { + animElement[1].replaceChild(newElements[i], animElement[0]); + elements[i][0] = newElements[i]; + } + }); + + return animSvg; } export function runSMILAnimation(parent, svgElement, elementsToAnimate) { - if(elementsToAnimate.length === 0) return; + if (elementsToAnimate.length === 0) return; - let animSvgElement = animateSVG(svgElement, elementsToAnimate); - if(svgElement.parentNode == parent) { - parent.removeChild(svgElement); - parent.appendChild(animSvgElement); + let animSvgElement = animateSVG(svgElement, elementsToAnimate); + if (svgElement.parentNode == parent) { + parent.removeChild(svgElement); + parent.appendChild(animSvgElement); + } - } - - // Replace the new svgElement (data has already been replaced) - setTimeout(() => { - if(animSvgElement.parentNode == parent) { - parent.removeChild(animSvgElement); - parent.appendChild(svgElement); - } - }, REPLACE_ALL_NEW_DUR); + // Replace the new svgElement (data has already been replaced) + setTimeout(() => { + if (animSvgElement.parentNode == parent) { + parent.removeChild(animSvgElement); + parent.appendChild(svgElement); + } + }, REPLACE_ALL_NEW_DUR); } diff --git a/src/js/utils/axis-chart-utils.js b/src/js/utils/axis-chart-utils.js index 7b5e4cc..08ce68c 100644 --- a/src/js/utils/axis-chart-utils.js +++ b/src/js/utils/axis-chart-utils.js @@ -1,130 +1,135 @@ -import { fillArray } from '../utils/helpers'; -import { DEFAULT_AXIS_CHART_TYPE, AXIS_DATASET_CHART_TYPES, DEFAULT_CHAR_WIDTH, - SERIES_LABEL_SPACE_RATIO } from '../utils/constants'; +import { fillArray } from "../utils/helpers"; +import { + DEFAULT_AXIS_CHART_TYPE, + AXIS_DATASET_CHART_TYPES, + DEFAULT_CHAR_WIDTH, + SERIES_LABEL_SPACE_RATIO, +} from "../utils/constants"; export function dataPrep(data, type) { - data.labels = data.labels || []; + data.labels = data.labels || []; - let datasetLength = data.labels.length; + let datasetLength = data.labels.length; - // Datasets - let datasets = data.datasets; - let zeroArray = new Array(datasetLength).fill(0); - if(!datasets) { - // default - datasets = [{ - values: zeroArray - }]; - } + // Datasets + let datasets = data.datasets; + let zeroArray = new Array(datasetLength).fill(0); + if (!datasets) { + // default + datasets = [ + { + values: zeroArray, + }, + ]; + } - datasets.map(d=> { - // Set values - if(!d.values) { - d.values = zeroArray; - } else { - // Check for non values - let vals = d.values; - vals = vals.map(val => (!isNaN(val) ? val : 0)); + datasets.map((d) => { + // Set values + if (!d.values) { + d.values = zeroArray; + } else { + // Check for non values + let vals = d.values; + vals = vals.map((val) => (!isNaN(val) ? val : 0)); - // Trim or extend - if(vals.length > datasetLength) { - vals = vals.slice(0, datasetLength); - } else { - vals = fillArray(vals, datasetLength - vals.length, 0); - } - d.values = vals; - } + // Trim or extend + if (vals.length > datasetLength) { + vals = vals.slice(0, datasetLength); + } else { + vals = fillArray(vals, datasetLength - vals.length, 0); + } + d.values = vals; + } - // Set type - if(!d.chartType ) { - if(!AXIS_DATASET_CHART_TYPES.includes(type)) type = DEFAULT_AXIS_CHART_TYPE; - d.chartType = type; - } + // Set type + if (!d.chartType) { + if (!AXIS_DATASET_CHART_TYPES.includes(type)) + type = DEFAULT_AXIS_CHART_TYPE; + d.chartType = type; + } + }); - }); + // Markers - // Markers + // Regions + // data.yRegions = data.yRegions || []; + if (data.yRegions) { + data.yRegions.map((d) => { + if (d.end < d.start) { + [d.start, d.end] = [d.end, d.start]; + } + }); + } - // Regions - // data.yRegions = data.yRegions || []; - if(data.yRegions) { - data.yRegions.map(d => { - if(d.end < d.start) { - [d.start, d.end] = [d.end, d.start]; - } - }); - } - - return data; + return data; } export function zeroDataPrep(realData) { - let datasetLength = realData.labels.length; - let zeroArray = new Array(datasetLength).fill(0); + let datasetLength = realData.labels.length; + let zeroArray = new Array(datasetLength).fill(0); - let zeroData = { - labels: realData.labels.slice(0, -1), - datasets: realData.datasets.map(d => { - return { - name: '', - values: zeroArray.slice(0, -1), - chartType: d.chartType - }; - }), - }; + let zeroData = { + labels: realData.labels.slice(0, -1), + datasets: realData.datasets.map((d) => { + return { + name: "", + values: zeroArray.slice(0, -1), + chartType: d.chartType, + }; + }), + }; - if(realData.yMarkers) { - zeroData.yMarkers = [ - { - value: 0, - label: '' - } - ]; - } + if (realData.yMarkers) { + zeroData.yMarkers = [ + { + value: 0, + label: "", + }, + ]; + } - if(realData.yRegions) { - zeroData.yRegions = [ - { - start: 0, - end: 0, - label: '' - } - ]; - } + if (realData.yRegions) { + zeroData.yRegions = [ + { + start: 0, + end: 0, + label: "", + }, + ]; + } - return zeroData; + return zeroData; } -export function getShortenedLabels(chartWidth, labels=[], isSeries=true) { - let allowedSpace = (chartWidth / labels.length) * SERIES_LABEL_SPACE_RATIO; - if(allowedSpace <= 0) allowedSpace = 1; - let allowedLetters = allowedSpace / DEFAULT_CHAR_WIDTH; +export function getShortenedLabels(chartWidth, labels = [], isSeries = true) { + let allowedSpace = (chartWidth / labels.length) * SERIES_LABEL_SPACE_RATIO; + if (allowedSpace <= 0) allowedSpace = 1; + let allowedLetters = allowedSpace / DEFAULT_CHAR_WIDTH; - let seriesMultiple; - if(isSeries) { - // Find the maximum label length for spacing calculations - let maxLabelLength = Math.max(...labels.map(label => label.length)); - seriesMultiple = Math.ceil(maxLabelLength/allowedLetters); - } + let seriesMultiple; + if (isSeries) { + // Find the maximum label length for spacing calculations + let maxLabelLength = Math.max(...labels.map((label) => label.length)); + seriesMultiple = Math.ceil(maxLabelLength / allowedLetters); + } - let calcLabels = labels.map((label, i) => { - label += ""; - if(label.length > allowedLetters) { + let calcLabels = labels.map((label, i) => { + label += ""; + if (label.length > allowedLetters) { + if (!isSeries) { + if (allowedLetters - 3 > 0) { + label = label.slice(0, allowedLetters - 3) + " ..."; + } else { + label = label.slice(0, allowedLetters) + ".."; + } + } else { + if (i % seriesMultiple !== 0 && i !== labels.length - 1) { + label = ""; + } + } + } + return label; + }); - if(!isSeries) { - if(allowedLetters-3 > 0) { - label = label.slice(0, allowedLetters-3) + " ..."; - } else { - label = label.slice(0, allowedLetters) + '..'; - } - } else { - if(i % seriesMultiple !== 0 && i !== (labels.length - 1)) { - label = ""; - } - } - } - return label; - }); - - return calcLabels; + return calcLabels; } diff --git a/src/js/utils/colors.js b/src/js/utils/colors.js index 3496c56..96b6d37 100644 --- a/src/js/utils/colors.js +++ b/src/js/utils/colors.js @@ -1,59 +1,61 @@ const PRESET_COLOR_MAP = { - 'pink': '#F683AE', - 'blue': '#318AD8', - 'green': '#48BB74', - 'grey': '#A6B1B9', - 'red': '#F56B6B', - 'yellow': '#FACF7A', - 'purple': '#44427B', - 'teal': '#5FD8C4', - 'cyan': '#15CCEF', - 'orange': '#F8814F', - 'light-pink': '#FED7E5', - 'light-blue': '#BFDDF7', - 'light-green': '#48BB74', - 'light-grey': '#F4F5F6', - 'light-red': '#F6DFDF', - 'light-yellow': '#FEE9BF', - 'light-purple': '#E8E8F7', - 'light-teal': '#D3FDF6', - 'light-cyan': '#DDF8FD', - 'light-orange': '#FECDB8' + pink: "#F683AE", + blue: "#318AD8", + green: "#48BB74", + grey: "#A6B1B9", + red: "#F56B6B", + yellow: "#FACF7A", + purple: "#44427B", + teal: "#5FD8C4", + cyan: "#15CCEF", + orange: "#F8814F", + "light-pink": "#FED7E5", + "light-blue": "#BFDDF7", + "light-green": "#48BB74", + "light-grey": "#F4F5F6", + "light-red": "#F6DFDF", + "light-yellow": "#FEE9BF", + "light-purple": "#E8E8F7", + "light-teal": "#D3FDF6", + "light-cyan": "#DDF8FD", + "light-orange": "#FECDB8", }; -function limitColor(r){ - if (r > 255) return 255; - else if (r < 0) return 0; - return r; +function limitColor(r) { + if (r > 255) return 255; + else if (r < 0) return 0; + return r; } export function lightenDarkenColor(color, amt) { - let col = getColor(color); - let usePound = false; - if (col[0] == "#") { - col = col.slice(1); - usePound = true; - } - let num = parseInt(col,16); - let r = limitColor((num >> 16) + amt); - let b = limitColor(((num >> 8) & 0x00FF) + amt); - let g = limitColor((num & 0x0000FF) + amt); - return (usePound?"#":"") + (g | (b << 8) | (r << 16)).toString(16); + let col = getColor(color); + let usePound = false; + if (col[0] == "#") { + col = col.slice(1); + usePound = true; + } + let num = parseInt(col, 16); + let r = limitColor((num >> 16) + amt); + let b = limitColor(((num >> 8) & 0x00ff) + amt); + let g = limitColor((num & 0x0000ff) + amt); + return (usePound ? "#" : "") + (g | (b << 8) | (r << 16)).toString(16); } export function isValidColor(string) { - // https://stackoverflow.com/a/32685393 - let HEX_RE = /(^\s*)(#)((?:[A-Fa-f0-9]{3}){1,2})$/i; - let RGB_RE = /(^\s*)(rgb|hsl)(a?)[(]\s*([\d.]+\s*%?)\s*,\s*([\d.]+\s*%?)\s*,\s*([\d.]+\s*%?)\s*(?:,\s*([\d.]+)\s*)?[)]$/i; - return HEX_RE.test(string) || RGB_RE.test(string); + // https://stackoverflow.com/a/32685393 + let HEX_RE = /(^\s*)(#)((?:[A-Fa-f0-9]{3}){1,2})$/i; + let RGB_RE = + /(^\s*)(rgb|hsl)(a?)[(]\s*([\d.]+\s*%?)\s*,\s*([\d.]+\s*%?)\s*,\s*([\d.]+\s*%?)\s*(?:,\s*([\d.]+)\s*)?[)]$/i; + return HEX_RE.test(string) || RGB_RE.test(string); } export const getColor = (color) => { - // When RGB color, convert to hexadecimal (alpha value is omitted) - if((/rgb[a]{0,1}\([\d, ]+\)/gim).test(color)) { - return (/\D+(\d*)\D+(\d*)\D+(\d*)/gim).exec(color) - .map((x, i) => (i !== 0 ? Number(x).toString(16) : '#')) - .reduce((c, ch) => `${c}${ch}`); - } - return PRESET_COLOR_MAP[color] || color; + // When RGB color, convert to hexadecimal (alpha value is omitted) + if (/rgb[a]{0,1}\([\d, ]+\)/gim.test(color)) { + return /\D+(\d*)\D+(\d*)\D+(\d*)/gim + .exec(color) + .map((x, i) => (i !== 0 ? Number(x).toString(16) : "#")) + .reduce((c, ch) => `${c}${ch}`); + } + return PRESET_COLOR_MAP[color] || color; }; diff --git a/src/js/utils/constants.js b/src/js/utils/constants.js index d03184d..c4b066c 100644 --- a/src/js/utils/constants.js +++ b/src/js/utils/constants.js @@ -1,75 +1,86 @@ -export const ALL_CHART_TYPES = ['line', 'scatter', 'bar', 'percentage', 'heatmap', 'pie']; +export const ALL_CHART_TYPES = [ + "line", + "scatter", + "bar", + "percentage", + "heatmap", + "pie", +]; export const COMPATIBLE_CHARTS = { - bar: ['line', 'scatter', 'percentage', 'pie'], - line: ['scatter', 'bar', 'percentage', 'pie'], - pie: ['line', 'scatter', 'percentage', 'bar'], - percentage: ['bar', 'line', 'scatter', 'pie'], - heatmap: [] + bar: ["line", "scatter", "percentage", "pie"], + line: ["scatter", "bar", "percentage", "pie"], + pie: ["line", "scatter", "percentage", "bar"], + percentage: ["bar", "line", "scatter", "pie"], + heatmap: [], }; export const DATA_COLOR_DIVISIONS = { - bar: 'datasets', - line: 'datasets', - pie: 'labels', - percentage: 'labels', - heatmap: HEATMAP_DISTRIBUTION_SIZE + bar: "datasets", + line: "datasets", + pie: "labels", + percentage: "labels", + heatmap: HEATMAP_DISTRIBUTION_SIZE, }; export const BASE_MEASURES = { - margins: { - top: 10, - bottom: 10, - left: 20, - right: 20 - }, - paddings: { - top: 20, - bottom: 40, - left: 30, - right: 10 - }, + margins: { + top: 10, + bottom: 10, + left: 20, + right: 20, + }, + paddings: { + top: 20, + bottom: 40, + left: 30, + right: 10, + }, - baseHeight: 240, - titleHeight: 20, - legendHeight: 30, + baseHeight: 240, + titleHeight: 20, + legendHeight: 30, - titleFontSize: 12, + titleFontSize: 12, }; export function getTopOffset(m) { - return m.titleHeight + m.margins.top + m.paddings.top; + return m.titleHeight + m.margins.top + m.paddings.top; } export function getLeftOffset(m) { - return m.margins.left + m.paddings.left; + return m.margins.left + m.paddings.left; } export function getExtraHeight(m) { - let totalExtraHeight = m.margins.top + m.margins.bottom - + m.paddings.top + m.paddings.bottom - + m.titleHeight + m.legendHeight; - return totalExtraHeight; + let totalExtraHeight = + m.margins.top + + m.margins.bottom + + m.paddings.top + + m.paddings.bottom + + m.titleHeight + + m.legendHeight; + return totalExtraHeight; } export function getExtraWidth(m) { - let totalExtraWidth = m.margins.left + m.margins.right - + m.paddings.left + m.paddings.right; + let totalExtraWidth = + m.margins.left + m.margins.right + m.paddings.left + m.paddings.right; - return totalExtraWidth; + return totalExtraWidth; } export const INIT_CHART_UPDATE_TIMEOUT = 700; export const CHART_POST_ANIMATE_TIMEOUT = 400; -export const DEFAULT_AXIS_CHART_TYPE = 'line'; -export const AXIS_DATASET_CHART_TYPES = ['line', 'bar']; +export const DEFAULT_AXIS_CHART_TYPE = "line"; +export const AXIS_DATASET_CHART_TYPES = ["line", "bar"]; export const AXIS_LEGEND_BAR_SIZE = 100; export const SERIES_LABEL_SPACE_RATIO = 0.6; export const BAR_CHART_SPACE_RATIO = 0.5; -export const MIN_BAR_PERCENT_HEIGHT = 0.00; +export const MIN_BAR_PERCENT_HEIGHT = 0.0; export const LINE_CHART_DOT_SIZE = 4; export const DOT_OVERLAY_SIZE_INCR = 4; @@ -86,18 +97,47 @@ export const HEATMAP_GUTTER_SIZE = 2; export const DEFAULT_CHAR_WIDTH = 7; export const TOOLTIP_POINTER_TRIANGLE_HEIGHT = 7.48; -const DEFAULT_CHART_COLORS = ['pink', 'blue', 'green', 'grey', 'red', 'yellow', 'purple', 'teal', 'cyan', 'orange']; -const HEATMAP_COLORS_GREEN = ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127']; -export const HEATMAP_COLORS_BLUE = ['#ebedf0', '#c0ddf9', '#73b3f3', '#3886e1', '#17459e']; -export const HEATMAP_COLORS_YELLOW = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; +const DEFAULT_CHART_COLORS = [ + "pink", + "blue", + "green", + "grey", + "red", + "yellow", + "purple", + "teal", + "cyan", + "orange", +]; +const HEATMAP_COLORS_GREEN = [ + "#ebedf0", + "#c6e48b", + "#7bc96f", + "#239a3b", + "#196127", +]; +export const HEATMAP_COLORS_BLUE = [ + "#ebedf0", + "#c0ddf9", + "#73b3f3", + "#3886e1", + "#17459e", +]; +export const HEATMAP_COLORS_YELLOW = [ + "#ebedf0", + "#fdf436", + "#ffc700", + "#ff9100", + "#06001c", +]; export const DEFAULT_COLORS = { - bar: DEFAULT_CHART_COLORS, - line: DEFAULT_CHART_COLORS, - pie: DEFAULT_CHART_COLORS, - percentage: DEFAULT_CHART_COLORS, - heatmap: HEATMAP_COLORS_GREEN, - donut: DEFAULT_CHART_COLORS + bar: DEFAULT_CHART_COLORS, + line: DEFAULT_CHART_COLORS, + pie: DEFAULT_CHART_COLORS, + percentage: DEFAULT_CHART_COLORS, + heatmap: HEATMAP_COLORS_GREEN, + donut: DEFAULT_CHART_COLORS, }; // Universal constants diff --git a/src/js/utils/date-utils.js b/src/js/utils/date-utils.js index 6ffaf74..83d7a06 100644 --- a/src/js/utils/date-utils.js +++ b/src/js/utils/date-utils.js @@ -6,91 +6,132 @@ export const DAYS_IN_YEAR = 375; export const NO_OF_MILLIS = 1000; export const SEC_IN_DAY = 86400; -export const MONTH_NAMES = ["January", "February", "March", "April", "May", - "June", "July", "August", "September", "October", "November", "December"]; -export const MONTH_NAMES_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +export const MONTH_NAMES = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; +export const MONTH_NAMES_SHORT = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +]; -export const DAY_NAMES_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; -export const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", - "Thursday", "Friday", "Saturday"]; +export const DAY_NAMES_SHORT = [ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", +]; +export const DAY_NAMES = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", +]; // https://stackoverflow.com/a/11252167/6495043 function treatAsUtc(date) { - let result = new Date(date); - result.setMinutes(result.getMinutes() - result.getTimezoneOffset()); - return result; + let result = new Date(date); + result.setMinutes(result.getMinutes() - result.getTimezoneOffset()); + return result; } export function toMidnightUTC(date) { - let result = new Date(date); - result.setUTCHours(0, result.getTimezoneOffset(), 0, 0); - return result; + let result = new Date(date); + result.setUTCHours(0, result.getTimezoneOffset(), 0, 0); + return result; } export function getYyyyMmDd(date) { - let dd = date.getDate(); - let mm = date.getMonth() + 1; // getMonth() is zero-based - return [ - date.getFullYear(), - (mm>9 ? '' : '0') + mm, - (dd>9 ? '' : '0') + dd - ].join('-'); + let dd = date.getDate(); + let mm = date.getMonth() + 1; // getMonth() is zero-based + return [ + date.getFullYear(), + (mm > 9 ? "" : "0") + mm, + (dd > 9 ? "" : "0") + dd, + ].join("-"); } export function clone(date) { - return new Date(date.getTime()); + return new Date(date.getTime()); } export function timestampSec(date) { - return date.getTime()/NO_OF_MILLIS; + return date.getTime() / NO_OF_MILLIS; } export function timestampToMidnight(timestamp, roundAhead = false) { - let midnightTs = Math.floor(timestamp - (timestamp % SEC_IN_DAY)); - if(roundAhead) { - return midnightTs + SEC_IN_DAY; - } - return midnightTs; + let midnightTs = Math.floor(timestamp - (timestamp % SEC_IN_DAY)); + if (roundAhead) { + return midnightTs + SEC_IN_DAY; + } + return midnightTs; } // export function getMonthsBetween(startDate, endDate) {} export function getWeeksBetween(startDate, endDate) { - let weekStartDate = setDayToSunday(startDate); - return Math.ceil(getDaysBetween(weekStartDate, endDate) / NO_OF_DAYS_IN_WEEK); + let weekStartDate = setDayToSunday(startDate); + return Math.ceil(getDaysBetween(weekStartDate, endDate) / NO_OF_DAYS_IN_WEEK); } export function getDaysBetween(startDate, endDate) { - let millisecondsPerDay = SEC_IN_DAY * NO_OF_MILLIS; - return (treatAsUtc(endDate) - treatAsUtc(startDate)) / millisecondsPerDay; + let millisecondsPerDay = SEC_IN_DAY * NO_OF_MILLIS; + return (treatAsUtc(endDate) - treatAsUtc(startDate)) / millisecondsPerDay; } export function areInSameMonth(startDate, endDate) { - return startDate.getMonth() === endDate.getMonth() - && startDate.getFullYear() === endDate.getFullYear(); + return ( + startDate.getMonth() === endDate.getMonth() && + startDate.getFullYear() === endDate.getFullYear() + ); } -export function getMonthName(i, short=false) { - let monthName = MONTH_NAMES[i]; - return short ? monthName.slice(0, 3) : monthName; +export function getMonthName(i, short = false) { + let monthName = MONTH_NAMES[i]; + return short ? monthName.slice(0, 3) : monthName; } -export function getLastDateInMonth (month, year) { - return new Date(year, month + 1, 0); // 0: last day in previous month +export function getLastDateInMonth(month, year) { + return new Date(year, month + 1, 0); // 0: last day in previous month } // mutates export function setDayToSunday(date) { - let newDate = clone(date); - const day = newDate.getDay(); - if(day !== 0) { - addDays(newDate, (-1) * day); - } - return newDate; + let newDate = clone(date); + const day = newDate.getDay(); + if (day !== 0) { + addDays(newDate, -1 * day); + } + return newDate; } // mutates export function addDays(date, numberOfDays) { - date.setDate(date.getDate() + numberOfDays); + date.setDate(date.getDate() + numberOfDays); } diff --git a/src/js/utils/dom.js b/src/js/utils/dom.js index 91cd2a7..9805baf 100644 --- a/src/js/utils/dom.js +++ b/src/js/utils/dom.js @@ -1,137 +1,149 @@ export function $(expr, con) { - return typeof expr === "string"? (con || document).querySelector(expr) : expr || null; + return typeof expr === "string" + ? (con || document).querySelector(expr) + : expr || null; } -export function findNodeIndex(node) -{ - var i = 0; - while (node.previousSibling) { - node = node.previousSibling; - i++; - } - return i; +export function findNodeIndex(node) { + var i = 0; + while (node.previousSibling) { + node = node.previousSibling; + i++; + } + return i; } $.create = (tag, o) => { - var element = document.createElement(tag); + var element = document.createElement(tag); - for (var i in o) { - var val = o[i]; + for (var i in o) { + var val = o[i]; - if (i === "inside") { - $(val).appendChild(element); - } - else if (i === "around") { - var ref = $(val); - ref.parentNode.insertBefore(element, ref); - element.appendChild(ref); + if (i === "inside") { + $(val).appendChild(element); + } else if (i === "around") { + var ref = $(val); + ref.parentNode.insertBefore(element, ref); + element.appendChild(ref); + } else if (i === "styles") { + if (typeof val === "object") { + Object.keys(val).map((prop) => { + element.style[prop] = val[prop]; + }); + } + } else if (i in element) { + element[i] = val; + } else { + element.setAttribute(i, val); + } + } - } else if (i === "styles") { - if(typeof val === "object") { - Object.keys(val).map(prop => { - element.style[prop] = val[prop]; - }); - } - } else if (i in element ) { - element[i] = val; - } - else { - element.setAttribute(i, val); - } - } - - return element; + return element; }; export function getOffset(element) { - let rect = element.getBoundingClientRect(); - return { - // https://stackoverflow.com/a/7436602/6495043 - // rect.top varies with scroll, so we add whatever has been - // scrolled to it to get absolute distance from actual page top - top: rect.top + (document.documentElement.scrollTop || document.body.scrollTop), - left: rect.left + (document.documentElement.scrollLeft || document.body.scrollLeft) - }; + let rect = element.getBoundingClientRect(); + return { + // https://stackoverflow.com/a/7436602/6495043 + // rect.top varies with scroll, so we add whatever has been + // scrolled to it to get absolute distance from actual page top + top: + rect.top + + (document.documentElement.scrollTop || document.body.scrollTop), + left: + rect.left + + (document.documentElement.scrollLeft || document.body.scrollLeft), + }; } // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent // an element's offsetParent property will return null whenever it, or any of its parents, // is hidden via the display style property. export function isHidden(el) { - return (el.offsetParent === null); + return el.offsetParent === null; } export function isElementInViewport(el) { - // Although straightforward: https://stackoverflow.com/a/7557433/6495043 - var rect = el.getBoundingClientRect(); + // Although straightforward: https://stackoverflow.com/a/7557433/6495043 + var rect = el.getBoundingClientRect(); - return ( - rect.top >= 0 && - rect.left >= 0 && - rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */ - rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */ - ); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= + (window.innerHeight || + document.documentElement.clientHeight) /*or $(window).height() */ && + rect.right <= + (window.innerWidth || + document.documentElement.clientWidth) /*or $(window).width() */ + ); } export function getElementContentWidth(element) { - var styles = window.getComputedStyle(element); - var padding = parseFloat(styles.paddingLeft) + - parseFloat(styles.paddingRight); + var styles = window.getComputedStyle(element); + var padding = + parseFloat(styles.paddingLeft) + parseFloat(styles.paddingRight); - return element.clientWidth - padding; + return element.clientWidth - padding; } -export function bind(element, o){ - if (element) { - for (var event in o) { - var callback = o[event]; +export function bind(element, o) { + if (element) { + for (var event in o) { + var callback = o[event]; - event.split(/\s+/).forEach(function (event) { - element.addEventListener(event, callback); - }); - } - } + event.split(/\s+/).forEach(function (event) { + element.addEventListener(event, callback); + }); + } + } } -export function unbind(element, o){ - if (element) { - for (var event in o) { - var callback = o[event]; +export function unbind(element, o) { + if (element) { + for (var event in o) { + var callback = o[event]; - event.split(/\s+/).forEach(function(event) { - element.removeEventListener(event, callback); - }); - } - } + event.split(/\s+/).forEach(function (event) { + element.removeEventListener(event, callback); + }); + } + } } export function fire(target, type, properties) { - var evt = document.createEvent("HTMLEvents"); + var evt = document.createEvent("HTMLEvents"); - evt.initEvent(type, true, true ); + evt.initEvent(type, true, true); - for (var j in properties) { - evt[j] = properties[j]; - } + for (var j in properties) { + evt[j] = properties[j]; + } - return target.dispatchEvent(evt); + return target.dispatchEvent(evt); } // https://css-tricks.com/snippets/javascript/loop-queryselectorall-matches/ export function forEachNode(nodeList, callback, scope) { - if(!nodeList) return; - for (var i = 0; i < nodeList.length; i++) { - callback.call(scope, nodeList[i], i); - } + if (!nodeList) return; + for (var i = 0; i < nodeList.length; i++) { + callback.call(scope, nodeList[i], i); + } } -export function activate($parent, $child, commonClass, activeClass='active', index = -1) { - let $children = $parent.querySelectorAll(`.${commonClass}.${activeClass}`); +export function activate( + $parent, + $child, + commonClass, + activeClass = "active", + index = -1 +) { + let $children = $parent.querySelectorAll(`.${commonClass}.${activeClass}`); - forEachNode($children, (node, i) => { - if(index >= 0 && i <= index) return; - node.classList.remove(activeClass); - }); + forEachNode($children, (node, i) => { + if (index >= 0 && i <= index) return; + node.classList.remove(activeClass); + }); - $child.classList.add(activeClass); + $child.classList.add(activeClass); } diff --git a/src/js/utils/draw-utils.js b/src/js/utils/draw-utils.js index c4a81aa..83b5038 100644 --- a/src/js/utils/draw-utils.js +++ b/src/js/utils/draw-utils.js @@ -1,99 +1,103 @@ -import { fillArray } from './helpers'; +import { fillArray } from "./helpers"; export function getBarHeightAndYAttr(yTop, zeroLine) { - let height, y; - if (yTop <= zeroLine) { - height = zeroLine - yTop; - y = yTop; - } else { - height = yTop - zeroLine; - y = zeroLine; - } + let height, y; + if (yTop <= zeroLine) { + height = zeroLine - yTop; + y = yTop; + } else { + height = yTop - zeroLine; + y = zeroLine; + } - return [height, y]; + return [height, y]; } -export function equilizeNoOfElements(array1, array2, - extraCount = array2.length - array1.length) { - - // Doesn't work if either has zero elements. - if(extraCount > 0) { - array1 = fillArray(array1, extraCount); - } else { - array2 = fillArray(array2, extraCount); - } - return [array1, array2]; +export function equilizeNoOfElements( + array1, + array2, + extraCount = array2.length - array1.length +) { + // Doesn't work if either has zero elements. + if (extraCount > 0) { + array1 = fillArray(array1, extraCount); + } else { + array2 = fillArray(array2, extraCount); + } + return [array1, array2]; } export function truncateString(txt, len) { - if (!txt) { - return; - } - if (txt.length > len) { - return txt.slice(0, len-3) + '...'; - } else { - return txt; - } + if (!txt) { + return; + } + if (txt.length > len) { + return txt.slice(0, len - 3) + "..."; + } else { + return txt; + } } export function shortenLargeNumber(label) { - let number; - if (typeof label === 'number') number = label; - else if (typeof label === 'string') { - number = Number(label); - if (Number.isNaN(number)) return label; - } + let number; + if (typeof label === "number") number = label; + else if (typeof label === "string") { + number = Number(label); + if (Number.isNaN(number)) return label; + } - // Using absolute since log wont work for negative numbers - let p = Math.floor(Math.log10(Math.abs(number))); - if (p <= 2) return number; // Return as is for a 3 digit number of less - let l = Math.floor(p / 3); - let shortened = (Math.pow(10, p - l * 3) * +(number / Math.pow(10, p)).toFixed(1)); + // Using absolute since log wont work for negative numbers + let p = Math.floor(Math.log10(Math.abs(number))); + if (p <= 2) return number; // Return as is for a 3 digit number of less + let l = Math.floor(p / 3); + let shortened = + Math.pow(10, p - l * 3) * +(number / Math.pow(10, p)).toFixed(1); - // Correct for floating point error upto 2 decimal places - return Math.round(shortened*100)/100 + ' ' + ['', 'K', 'M', 'B', 'T'][l]; + // Correct for floating point error upto 2 decimal places + return Math.round(shortened * 100) / 100 + " " + ["", "K", "M", "B", "T"][l]; } // cubic bezier curve calculation (from example by François Romain) export function getSplineCurvePointsStr(xList, yList) { + let points = []; + for (let i = 0; i < xList.length; i++) { + points.push([xList[i], yList[i]]); + } - let points=[]; - for(let i=0;i { + let lengthX = pointB[0] - pointA[0]; + let lengthY = pointB[1] - pointA[1]; + return { + length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)), + angle: Math.atan2(lengthY, lengthX), + }; + }; - let smoothing = 0.2; - let line = (pointA, pointB) => { - let lengthX = pointB[0] - pointA[0]; - let lengthY = pointB[1] - pointA[1]; - return { - length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)), - angle: Math.atan2(lengthY, lengthX) - }; - }; - - let controlPoint = (current, previous, next, reverse) => { - let p = previous || current; - let n = next || current; - let o = line(p, n); - let angle = o.angle + (reverse ? Math.PI : 0); - let length = o.length * smoothing; - let x = current[0] + Math.cos(angle) * length; - let y = current[1] + Math.sin(angle) * length; - return [x, y]; - }; - - let bezierCommand = (point, i, a) => { - let cps = controlPoint(a[i - 1], a[i - 2], point); - let cpe = controlPoint(point, a[i - 1], a[i + 1], true); - return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}`; - }; - - let pointStr = (points, command) => { - return points.reduce((acc, point, i, a) => i === 0 - ? `${point[0]},${point[1]}` - : `${acc} ${command(point, i, a)}`, ''); - }; - - return pointStr(points, bezierCommand); + let controlPoint = (current, previous, next, reverse) => { + let p = previous || current; + let n = next || current; + let o = line(p, n); + let angle = o.angle + (reverse ? Math.PI : 0); + let length = o.length * smoothing; + let x = current[0] + Math.cos(angle) * length; + let y = current[1] + Math.sin(angle) * length; + return [x, y]; + }; + + let bezierCommand = (point, i, a) => { + let cps = controlPoint(a[i - 1], a[i - 2], point); + let cpe = controlPoint(point, a[i - 1], a[i + 1], true); + return `C ${cps[0]},${cps[1]} ${cpe[0]},${cpe[1]} ${point[0]},${point[1]}`; + }; + + let pointStr = (points, command) => { + return points.reduce( + (acc, point, i, a) => + i === 0 ? `${point[0]},${point[1]}` : `${acc} ${command(point, i, a)}`, + "" + ); + }; + + return pointStr(points, bezierCommand); } diff --git a/src/js/utils/draw.js b/src/js/utils/draw.js index 8c4fe75..7b0421f 100644 --- a/src/js/utils/draw.js +++ b/src/js/utils/draw.js @@ -1,128 +1,165 @@ -import { getBarHeightAndYAttr, truncateString, shortenLargeNumber, getSplineCurvePointsStr } from './draw-utils'; -import { getStringWidth, isValidNumber } from './helpers'; -import { DOT_OVERLAY_SIZE_INCR } from './constants'; +import { + getBarHeightAndYAttr, + truncateString, + shortenLargeNumber, + getSplineCurvePointsStr, +} from "./draw-utils"; +import { getStringWidth, isValidNumber } from "./helpers"; +import { DOT_OVERLAY_SIZE_INCR } from "./constants"; export const AXIS_TICK_LENGTH = 6; const LABEL_MARGIN = 4; const LABEL_MAX_CHARS = 15; export const FONT_SIZE = 10; -const BASE_LINE_COLOR = '#E2E6E9'; -const FONT_FILL = '#313B44'; +const BASE_LINE_COLOR = "#E2E6E9"; +const FONT_FILL = "#313B44"; function $(expr, con) { - return typeof expr === "string"? (con || document).querySelector(expr) : expr || null; + return typeof expr === "string" + ? (con || document).querySelector(expr) + : expr || null; } export function createSVG(tag, o) { - var element = document.createElementNS("http://www.w3.org/2000/svg", tag); + var element = document.createElementNS("http://www.w3.org/2000/svg", tag); - for (var i in o) { - var val = o[i]; + for (var i in o) { + var val = o[i]; - if (i === "inside") { - $(val).appendChild(element); - } - else if (i === "around") { - var ref = $(val); - ref.parentNode.insertBefore(element, ref); - element.appendChild(ref); + if (i === "inside") { + $(val).appendChild(element); + } else if (i === "around") { + var ref = $(val); + ref.parentNode.insertBefore(element, ref); + element.appendChild(ref); + } else if (i === "styles") { + if (typeof val === "object") { + Object.keys(val).map((prop) => { + element.style[prop] = val[prop]; + }); + } + } else { + if (i === "className") { + i = "class"; + } + if (i === "innerHTML") { + element["textContent"] = val; + } else { + element.setAttribute(i, val); + } + } + } - } else if (i === "styles") { - if(typeof val === "object") { - Object.keys(val).map(prop => { - element.style[prop] = val[prop]; - }); - } - } else { - if(i === "className") { i = "class"; } - if(i === "innerHTML") { - element['textContent'] = val; - } else { - element.setAttribute(i, val); - } - } - } - - return element; + return element; } function renderVerticalGradient(svgDefElem, gradientId) { - return createSVG('linearGradient', { - inside: svgDefElem, - id: gradientId, - x1: 0, - x2: 0, - y1: 0, - y2: 1 - }); + return createSVG("linearGradient", { + inside: svgDefElem, + id: gradientId, + x1: 0, + x2: 0, + y1: 0, + y2: 1, + }); } function setGradientStop(gradElem, offset, color, opacity) { - return createSVG('stop', { - 'inside': gradElem, - 'style': `stop-color: ${color}`, - 'offset': offset, - 'stop-opacity': opacity - }); + return createSVG("stop", { + inside: gradElem, + style: `stop-color: ${color}`, + offset: offset, + "stop-opacity": opacity, + }); } export function makeSVGContainer(parent, className, width, height) { - return createSVG('svg', { - className: className, - inside: parent, - width: width, - height: height - }); + return createSVG("svg", { + className: className, + inside: parent, + width: width, + height: height, + }); } export function makeSVGDefs(svgContainer) { - return createSVG('defs', { - inside: svgContainer, - }); + return createSVG("defs", { + inside: svgContainer, + }); } -export function makeSVGGroup(className, transform='', parent=undefined) { - let args = { - className: className, - transform: transform - }; - if(parent) args.inside = parent; - return createSVG('g', args); +export function makeSVGGroup(className, transform = "", parent = undefined) { + let args = { + className: className, + transform: transform, + }; + if (parent) args.inside = parent; + return createSVG("g", args); } -export function wrapInSVGGroup(elements, className='') { - let g = createSVG('g', { - className: className - }); - elements.forEach(e => g.appendChild(e)); - return g; +export function wrapInSVGGroup(elements, className = "") { + let g = createSVG("g", { + className: className, + }); + elements.forEach((e) => g.appendChild(e)); + return g; } -export function makePath(pathStr, className='', stroke='none', fill='none', strokeWidth=2) { - return createSVG('path', { - className: className, - d: pathStr, - styles: { - stroke: stroke, - fill: fill, - 'stroke-width': strokeWidth - } - }); +export function makePath( + pathStr, + className = "", + stroke = "none", + fill = "none", + strokeWidth = 2 +) { + return createSVG("path", { + className: className, + d: pathStr, + styles: { + stroke: stroke, + fill: fill, + "stroke-width": strokeWidth, + }, + }); } -export function makeArcPathStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){ - let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y]; - let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y]; - return `M${center.x} ${center.y} +export function makeArcPathStr( + startPosition, + endPosition, + center, + radius, + clockWise = 1, + largeArc = 0 +) { + let [arcStartX, arcStartY] = [ + center.x + startPosition.x, + center.y + startPosition.y, + ]; + let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y]; + return `M${center.x} ${center.y} L${arcStartX} ${arcStartY} A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0} ${arcEndX} ${arcEndY} z`; } -export function makeCircleStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){ - let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y]; - let [arcEndX, midArc, arcEndY] = [center.x + endPosition.x, center.y * 2, center.y + endPosition.y]; - return `M${center.x} ${center.y} +export function makeCircleStr( + startPosition, + endPosition, + center, + radius, + clockWise = 1, + largeArc = 0 +) { + let [arcStartX, arcStartY] = [ + center.x + startPosition.x, + center.y + startPosition.y, + ]; + let [arcEndX, midArc, arcEndY] = [ + center.x + endPosition.x, + center.y * 2, + center.y + endPosition.y, + ]; + return `M${center.x} ${center.y} L${arcStartX} ${arcStartY} A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0} ${arcEndX} ${midArc} z @@ -131,20 +168,44 @@ export function makeCircleStr(startPosition, endPosition, center, radius, clockW ${arcEndX} ${arcEndY} z`; } -export function makeArcStrokePathStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){ - let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y]; - let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y]; +export function makeArcStrokePathStr( + startPosition, + endPosition, + center, + radius, + clockWise = 1, + largeArc = 0 +) { + let [arcStartX, arcStartY] = [ + center.x + startPosition.x, + center.y + startPosition.y, + ]; + let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y]; - return `M${arcStartX} ${arcStartY} + return `M${arcStartX} ${arcStartY} A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0} ${arcEndX} ${arcEndY}`; } -export function makeStrokeCircleStr(startPosition, endPosition, center, radius, clockWise=1, largeArc=0){ - let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y]; - let [arcEndX, midArc, arcEndY] = [center.x + endPosition.x, radius * 2 + arcStartY, center.y + startPosition.y]; +export function makeStrokeCircleStr( + startPosition, + endPosition, + center, + radius, + clockWise = 1, + largeArc = 0 +) { + let [arcStartX, arcStartY] = [ + center.x + startPosition.x, + center.y + startPosition.y, + ]; + let [arcEndX, midArc, arcEndY] = [ + center.x + endPosition.x, + radius * 2 + arcStartY, + center.y + startPosition.y, + ]; - return `M${arcStartX} ${arcStartY} + return `M${arcStartX} ${arcStartY} A ${radius} ${radius} 0 ${largeArc} ${clockWise ? 1 : 0} ${arcEndX} ${midArc} M${arcStartX} ${midArc} @@ -153,625 +214,680 @@ export function makeStrokeCircleStr(startPosition, endPosition, center, radius, } export function makeGradient(svgDefElem, color, lighter = false) { - let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default'); - let gradientDef = renderVerticalGradient(svgDefElem, gradientId); - let opacities = [1, 0.6, 0.2]; - if(lighter) { - opacities = [0.4, 0.2, 0]; - } + let gradientId = + "path-fill-gradient" + + "-" + + color + + "-" + + (lighter ? "lighter" : "default"); + let gradientDef = renderVerticalGradient(svgDefElem, gradientId); + let opacities = [1, 0.6, 0.2]; + if (lighter) { + opacities = [0.4, 0.2, 0]; + } - setGradientStop(gradientDef, "0%", color, opacities[0]); - setGradientStop(gradientDef, "50%", color, opacities[1]); - setGradientStop(gradientDef, "100%", color, opacities[2]); + setGradientStop(gradientDef, "0%", color, opacities[0]); + setGradientStop(gradientDef, "50%", color, opacities[1]); + setGradientStop(gradientDef, "100%", color, opacities[2]); - return gradientId; + return gradientId; } export function rightRoundedBar(x, width, height) { - // https://medium.com/@dennismphil/one-side-rounded-rectangle-using-svg-fb31cf318d90 - let radius = height/2; - let xOffset = width - radius; + // https://medium.com/@dennismphil/one-side-rounded-rectangle-using-svg-fb31cf318d90 + let radius = height / 2; + let xOffset = width - radius; - return `M${x},0 h${xOffset} q${radius},0 ${radius},${radius} q0,${radius} -${radius},${radius} h-${xOffset} v${height}z`; + return `M${x},0 h${xOffset} q${radius},0 ${radius},${radius} q0,${radius} -${radius},${radius} h-${xOffset} v${height}z`; } export function leftRoundedBar(x, width, height) { - let radius = height/2; - let xOffset = width - radius; + let radius = height / 2; + let xOffset = width - radius; - return `M${x + radius},0 h${xOffset} v${height} h-${xOffset} q-${radius}, 0 -${radius},-${radius} q0,-${radius} ${radius},-${radius}z`; + return `M${ + x + radius + },0 h${xOffset} v${height} h-${xOffset} q-${radius}, 0 -${radius},-${radius} q0,-${radius} ${radius},-${radius}z`; } -export function percentageBar(x, y, width, height, isFirst, isLast, fill='none') { - if (isLast) { - let pathStr = rightRoundedBar(x, width, height); - return makePath(pathStr, 'percentage-bar', null, fill); - } +export function percentageBar( + x, + y, + width, + height, + isFirst, + isLast, + fill = "none" +) { + if (isLast) { + let pathStr = rightRoundedBar(x, width, height); + return makePath(pathStr, "percentage-bar", null, fill); + } - if (isFirst) { - let pathStr = leftRoundedBar(x, width, height); - return makePath(pathStr, 'percentage-bar', null, fill); - } + if (isFirst) { + let pathStr = leftRoundedBar(x, width, height); + return makePath(pathStr, "percentage-bar", null, fill); + } - let args = { - className: 'percentage-bar', - x: x, - y: y, - width: width, - height: height, - fill: fill - }; + let args = { + className: "percentage-bar", + x: x, + y: y, + width: width, + height: height, + fill: fill, + }; - return createSVG("rect", args); + return createSVG("rect", args); } -export function heatSquare(className, x, y, size, radius, fill='none', data={}) { - let args = { - className: className, - x: x, - y: y, - width: size, - height: size, - rx: radius, - fill: fill - }; +export function heatSquare( + className, + x, + y, + size, + radius, + fill = "none", + data = {} +) { + let args = { + className: className, + x: x, + y: y, + width: size, + height: size, + rx: radius, + fill: fill, + }; - Object.keys(data).map(key => { - args[key] = data[key]; - }); + Object.keys(data).map((key) => { + args[key] = data[key]; + }); - return createSVG("rect", args); + return createSVG("rect", args); } -export function legendBar(x, y, size, fill='none', label, truncate=false) { - label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label; +export function legendBar(x, y, size, fill = "none", label, truncate = false) { + label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label; - let args = { - className: 'legend-bar', - x: 0, - y: 0, - width: size, - height: '2px', - fill: fill - }; - let text = createSVG('text', { - className: 'legend-dataset-text', - x: 0, - y: 0, - dy: (FONT_SIZE * 2) + 'px', - 'font-size': (FONT_SIZE * 1.2) + 'px', - 'text-anchor': 'start', - fill: FONT_FILL, - innerHTML: label - }); + let args = { + className: "legend-bar", + x: 0, + y: 0, + width: size, + height: "2px", + fill: fill, + }; + let text = createSVG("text", { + className: "legend-dataset-text", + x: 0, + y: 0, + dy: FONT_SIZE * 2 + "px", + "font-size": FONT_SIZE * 1.2 + "px", + "text-anchor": "start", + fill: FONT_FILL, + innerHTML: label, + }); - let group = createSVG('g', { - transform: `translate(${x}, ${y})` - }); - group.appendChild(createSVG("rect", args)); - group.appendChild(text); + let group = createSVG("g", { + transform: `translate(${x}, ${y})`, + }); + group.appendChild(createSVG("rect", args)); + group.appendChild(text); - return group; + return group; } -export function legendDot(x, y, size, radius, fill='none', label, value, truncate=false) { - label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label; +export function legendDot( + x, + y, + size, + radius, + fill = "none", + label, + value, + truncate = false +) { + label = truncate ? truncateString(label, LABEL_MAX_CHARS) : label; - let args = { - className: 'legend-dot', - x: 0, - y: 4 - size, - height: size, - width: size, - rx: radius, - fill: fill - }; + let args = { + className: "legend-dot", + x: 0, + y: 4 - size, + height: size, + width: size, + rx: radius, + fill: fill, + }; - let textLabel = createSVG('text', { - className: 'legend-dataset-label', - x: size, - y: 0, - dx: (FONT_SIZE) + 'px', - dy: (FONT_SIZE/3) + 'px', - 'font-size': (FONT_SIZE * 1.6) + 'px', - 'text-anchor': 'start', - fill: FONT_FILL, - innerHTML: label - }); + let textLabel = createSVG("text", { + className: "legend-dataset-label", + x: size, + y: 0, + dx: FONT_SIZE + "px", + dy: FONT_SIZE / 3 + "px", + "font-size": FONT_SIZE * 1.6 + "px", + "text-anchor": "start", + fill: FONT_FILL, + innerHTML: label, + }); - let textValue = createSVG('text', { - className: 'legend-dataset-value', - x: size, - y: FONT_SIZE + 10, - dx: (FONT_SIZE) + 'px', - dy: (FONT_SIZE/3) + 'px', - 'font-size': (FONT_SIZE * 1.2) + 'px', - 'text-anchor': 'start', - fill: FONT_FILL, - innerHTML: value - }); + let textValue = createSVG("text", { + className: "legend-dataset-value", + x: size, + y: FONT_SIZE + 10, + dx: FONT_SIZE + "px", + dy: FONT_SIZE / 3 + "px", + "font-size": FONT_SIZE * 1.2 + "px", + "text-anchor": "start", + fill: FONT_FILL, + innerHTML: value, + }); - let group = createSVG('g', { - transform: `translate(${x}, ${y})` - }); - group.appendChild(createSVG("rect", args)); - group.appendChild(textLabel); - group.appendChild(textValue); + let group = createSVG("g", { + transform: `translate(${x}, ${y})`, + }); + group.appendChild(createSVG("rect", args)); + group.appendChild(textLabel); + group.appendChild(textValue); - return group; + return group; } export function makeText(className, x, y, content, options = {}) { - let fontSize = options.fontSize || FONT_SIZE; - let dy = options.dy !== undefined ? options.dy : (fontSize / 2); - let fill = options.fill || FONT_FILL; - let textAnchor = options.textAnchor || 'start'; - return createSVG('text', { - className: className, - x: x, - y: y, - dy: dy + 'px', - 'font-size': fontSize + 'px', - fill: fill, - 'text-anchor': textAnchor, - innerHTML: content - }); + let fontSize = options.fontSize || FONT_SIZE; + let dy = options.dy !== undefined ? options.dy : fontSize / 2; + let fill = options.fill || FONT_FILL; + let textAnchor = options.textAnchor || "start"; + return createSVG("text", { + className: className, + x: x, + y: y, + dy: dy + "px", + "font-size": fontSize + "px", + fill: fill, + "text-anchor": textAnchor, + innerHTML: content, + }); } -function makeVertLine(x, label, y1, y2, options={}) { - let l = createSVG('line', { - className: 'line-vertical ' + options.className, - x1: 0, - x2: 0, - y1: y1, - y2: y2, - styles: { - stroke: options.stroke - } - }); +function makeVertLine(x, label, y1, y2, options = {}) { + let l = createSVG("line", { + className: "line-vertical " + options.className, + x1: 0, + x2: 0, + y1: y1, + y2: y2, + styles: { + stroke: options.stroke, + }, + }); - let text = createSVG('text', { - x: 0, - y: y1 > y2 ? y1 + LABEL_MARGIN : y1 - LABEL_MARGIN - FONT_SIZE, - dy: FONT_SIZE + 'px', - 'font-size': FONT_SIZE + 'px', - 'text-anchor': 'middle', - innerHTML: label + "" - }); + let text = createSVG("text", { + x: 0, + y: y1 > y2 ? y1 + LABEL_MARGIN : y1 - LABEL_MARGIN - FONT_SIZE, + dy: FONT_SIZE + "px", + "font-size": FONT_SIZE + "px", + "text-anchor": "middle", + innerHTML: label + "", + }); - let line = createSVG('g', { - transform: `translate(${ x }, 0)` - }); + let line = createSVG("g", { + transform: `translate(${x}, 0)`, + }); - line.appendChild(l); - line.appendChild(text); + line.appendChild(l); + line.appendChild(text); - return line; + return line; } -function makeHoriLine(y, label, x1, x2, options={}) { - if(!options.lineType) options.lineType = ''; - if (options.shortenNumbers) label = shortenLargeNumber(label); +function makeHoriLine(y, label, x1, x2, options = {}) { + if (!options.lineType) options.lineType = ""; + if (options.shortenNumbers) label = shortenLargeNumber(label); - let className = 'line-horizontal ' + options.className + - (options.lineType === "dashed" ? "dashed": ""); + let className = + "line-horizontal " + + options.className + + (options.lineType === "dashed" ? "dashed" : ""); - let l = createSVG('line', { - className: className, - x1: x1, - x2: x2, - y1: 0, - y2: 0, - styles: { - stroke: options.stroke - } - }); + let l = createSVG("line", { + className: className, + x1: x1, + x2: x2, + y1: 0, + y2: 0, + styles: { + stroke: options.stroke, + }, + }); - let text = createSVG('text', { - x: x1 < x2 ? x1 - LABEL_MARGIN : x1 + LABEL_MARGIN, - y: 0, - dy: (FONT_SIZE / 2 - 2) + 'px', - 'font-size': FONT_SIZE + 'px', - 'text-anchor': x1 < x2 ? 'end' : 'start', - innerHTML: label+"" - }); + let text = createSVG("text", { + x: x1 < x2 ? x1 - LABEL_MARGIN : x1 + LABEL_MARGIN, + y: 0, + dy: FONT_SIZE / 2 - 2 + "px", + "font-size": FONT_SIZE + "px", + "text-anchor": x1 < x2 ? "end" : "start", + innerHTML: label + "", + }); - let line = createSVG('g', { - transform: `translate(0, ${y})`, - 'stroke-opacity': 1 - }); + let line = createSVG("g", { + transform: `translate(0, ${y})`, + "stroke-opacity": 1, + }); - if(text === 0 || text === '0') { - line.style.stroke = "rgba(27, 31, 35, 0.6)"; - } + if (text === 0 || text === "0") { + line.style.stroke = "rgba(27, 31, 35, 0.6)"; + } - line.appendChild(l); - line.appendChild(text); + line.appendChild(l); + line.appendChild(text); - return line; + return line; } -export function yLine(y, label, width, options={}) { - if (!isValidNumber(y)) y = 0; +export function yLine(y, label, width, options = {}) { + if (!isValidNumber(y)) y = 0; - if(!options.pos) options.pos = 'left'; - if(!options.offset) options.offset = 0; - if(!options.mode) options.mode = 'span'; - if(!options.stroke) options.stroke = BASE_LINE_COLOR; - if(!options.className) options.className = ''; + if (!options.pos) options.pos = "left"; + if (!options.offset) options.offset = 0; + if (!options.mode) options.mode = "span"; + if (!options.stroke) options.stroke = BASE_LINE_COLOR; + if (!options.className) options.className = ""; - let x1 = -1 * AXIS_TICK_LENGTH; - let x2 = options.mode === 'span' ? width + AXIS_TICK_LENGTH : 0; + let x1 = -1 * AXIS_TICK_LENGTH; + let x2 = options.mode === "span" ? width + AXIS_TICK_LENGTH : 0; - if(options.mode === 'tick' && options.pos === 'right') { - x1 = width + AXIS_TICK_LENGTH; - x2 = width; - } + if (options.mode === "tick" && options.pos === "right") { + x1 = width + AXIS_TICK_LENGTH; + x2 = width; + } - // let offset = options.pos === 'left' ? -1 * options.offset : options.offset; + // let offset = options.pos === 'left' ? -1 * options.offset : options.offset; - x1 += options.offset; - x2 += options.offset; + x1 += options.offset; + x2 += options.offset; - return makeHoriLine(y, label, x1, x2, { - className: options.className, - lineType: options.lineType, - shortenNumbers: options.shortenNumbers - }); + return makeHoriLine(y, label, x1, x2, { + className: options.className, + lineType: options.lineType, + shortenNumbers: options.shortenNumbers, + }); } -export function xLine(x, label, height, options={}) { - if (!isValidNumber(x)) x = 0; +export function xLine(x, label, height, options = {}) { + if (!isValidNumber(x)) x = 0; - if(!options.pos) options.pos = 'bottom'; - if(!options.offset) options.offset = 0; - if(!options.mode) options.mode = 'span'; - if(!options.className) options.className = ''; + if (!options.pos) options.pos = "bottom"; + if (!options.offset) options.offset = 0; + if (!options.mode) options.mode = "span"; + if (!options.className) options.className = ""; - // Draw X axis line in span/tick mode with optional label - // y2(span) - // | - // | - // x line | - // | - // | - // ---------------------+-- y2(tick) - // | - // y1 + // Draw X axis line in span/tick mode with optional label + // y2(span) + // | + // | + // x line | + // | + // | + // ---------------------+-- y2(tick) + // | + // y1 - let y1 = height + AXIS_TICK_LENGTH; - let y2 = options.mode === 'span' ? -1 * AXIS_TICK_LENGTH : height; + let y1 = height + AXIS_TICK_LENGTH; + let y2 = options.mode === "span" ? -1 * AXIS_TICK_LENGTH : height; - if(options.mode === 'tick' && options.pos === 'top') { - // top axis ticks - y1 = -1 * AXIS_TICK_LENGTH; - y2 = 0; - } + if (options.mode === "tick" && options.pos === "top") { + // top axis ticks + y1 = -1 * AXIS_TICK_LENGTH; + y2 = 0; + } - return makeVertLine(x, label, y1, y2, { - className: options.className, - lineType: options.lineType - }); + return makeVertLine(x, label, y1, y2, { + className: options.className, + lineType: options.lineType, + }); } -export function yMarker(y, label, width, options={}) { - if(!options.labelPos) options.labelPos = 'right'; - let x = options.labelPos === 'left' ? LABEL_MARGIN - : width - getStringWidth(label, 5) - LABEL_MARGIN; +export function yMarker(y, label, width, options = {}) { + if (!options.labelPos) options.labelPos = "right"; + let x = + options.labelPos === "left" + ? LABEL_MARGIN + : width - getStringWidth(label, 5) - LABEL_MARGIN; - let labelSvg = createSVG('text', { - className: 'chart-label', - x: x, - y: 0, - dy: (FONT_SIZE / -2) + 'px', - 'font-size': FONT_SIZE + 'px', - 'text-anchor': 'start', - innerHTML: label+"" - }); + let labelSvg = createSVG("text", { + className: "chart-label", + x: x, + y: 0, + dy: FONT_SIZE / -2 + "px", + "font-size": FONT_SIZE + "px", + "text-anchor": "start", + innerHTML: label + "", + }); - let line = makeHoriLine(y, '', 0, width, { - stroke: options.stroke || BASE_LINE_COLOR, - className: options.className || '', - lineType: options.lineType - }); + let line = makeHoriLine(y, "", 0, width, { + stroke: options.stroke || BASE_LINE_COLOR, + className: options.className || "", + lineType: options.lineType, + }); - line.appendChild(labelSvg); + line.appendChild(labelSvg); - return line; + return line; } -export function yRegion(y1, y2, width, label, options={}) { - // return a group - let height = y1 - y2; +export function yRegion(y1, y2, width, label, options = {}) { + // return a group + let height = y1 - y2; - let rect = createSVG('rect', { - className: `bar mini`, // remove class - styles: { - fill: `rgba(228, 234, 239, 0.49)`, - stroke: BASE_LINE_COLOR, - 'stroke-dasharray': `${width}, ${height}` - }, - // 'data-point-index': index, - x: 0, - y: 0, - width: width, - height: height - }); + let rect = createSVG("rect", { + className: `bar mini`, // remove class + styles: { + fill: `rgba(228, 234, 239, 0.49)`, + stroke: BASE_LINE_COLOR, + "stroke-dasharray": `${width}, ${height}`, + }, + // 'data-point-index': index, + x: 0, + y: 0, + width: width, + height: height, + }); - if(!options.labelPos) options.labelPos = 'right'; - let x = options.labelPos === 'left' ? LABEL_MARGIN - : width - getStringWidth(label+"", 4.5) - LABEL_MARGIN; + if (!options.labelPos) options.labelPos = "right"; + let x = + options.labelPos === "left" + ? LABEL_MARGIN + : width - getStringWidth(label + "", 4.5) - LABEL_MARGIN; - let labelSvg = createSVG('text', { - className: 'chart-label', - x: x, - y: 0, - dy: (FONT_SIZE / -2) + 'px', - 'font-size': FONT_SIZE + 'px', - 'text-anchor': 'start', - innerHTML: label+"" - }); + let labelSvg = createSVG("text", { + className: "chart-label", + x: x, + y: 0, + dy: FONT_SIZE / -2 + "px", + "font-size": FONT_SIZE + "px", + "text-anchor": "start", + innerHTML: label + "", + }); - let region = createSVG('g', { - transform: `translate(0, ${y2})` - }); + let region = createSVG("g", { + transform: `translate(0, ${y2})`, + }); - region.appendChild(rect); - region.appendChild(labelSvg); + region.appendChild(rect); + region.appendChild(labelSvg); - return region; + return region; } -export function datasetBar(x, yTop, width, color, label='', index=0, offset=0, meta={}) { - let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine); - y -= offset; +export function datasetBar( + x, + yTop, + width, + color, + label = "", + index = 0, + offset = 0, + meta = {} +) { + let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine); + y -= offset; - if(height === 0) { - height = meta.minHeight; - y -= meta.minHeight; - } + if (height === 0) { + height = meta.minHeight; + y -= meta.minHeight; + } - // Preprocess numbers to avoid svg building errors - if (!isValidNumber(x)) x = 0; - if (!isValidNumber(y)) y = 0; - if (!isValidNumber(height, true)) height = 0; - if (!isValidNumber(width, true)) width = 0; + // Preprocess numbers to avoid svg building errors + if (!isValidNumber(x)) x = 0; + if (!isValidNumber(y)) y = 0; + if (!isValidNumber(height, true)) height = 0; + if (!isValidNumber(width, true)) width = 0; - // x y h w + // x y h w - // M{x},{y+r} - // q0,-{r} {r},-{r} - // q{r},0 {r},{r} - // v{h-r} - // h-{w}z + // M{x},{y+r} + // q0,-{r} {r},-{r} + // q{r},0 {r},{r} + // v{h-r} + // h-{w}z - // let radius = width/2; - // let pathStr = `M${x},${y+radius} q0,-${radius} ${radius},-${radius} q${radius},0 ${radius},${radius} v${height-radius} h-${width}z` + // let radius = width/2; + // let pathStr = `M${x},${y+radius} q0,-${radius} ${radius},-${radius} q${radius},0 ${radius},${radius} v${height-radius} h-${width}z` - // let rect = createSVG('path', { - // className: 'bar mini', - // d: pathStr, - // styles: { fill: color }, - // x: x, - // y: y, - // 'data-point-index': index, - // }); + // let rect = createSVG('path', { + // className: 'bar mini', + // d: pathStr, + // styles: { fill: color }, + // x: x, + // y: y, + // 'data-point-index': index, + // }); - let rect = createSVG('rect', { - className: `bar mini`, - style: `fill: ${color}`, - 'data-point-index': index, - x: x, - y: y, - width: width, - height: height - }); + let rect = createSVG("rect", { + className: `bar mini`, + style: `fill: ${color}`, + "data-point-index": index, + x: x, + y: y, + width: width, + height: height, + }); - label += ""; + label += ""; - if(!label && !label.length) { - return rect; - } else { - rect.setAttribute('y', 0); - rect.setAttribute('x', 0); - let text = createSVG('text', { - className: 'data-point-value', - x: width/2, - y: 0, - dy: (FONT_SIZE / 2 * -1) + 'px', - 'font-size': FONT_SIZE + 'px', - 'text-anchor': 'middle', - innerHTML: label - }); + if (!label && !label.length) { + return rect; + } else { + rect.setAttribute("y", 0); + rect.setAttribute("x", 0); + let text = createSVG("text", { + className: "data-point-value", + x: width / 2, + y: 0, + dy: (FONT_SIZE / 2) * -1 + "px", + "font-size": FONT_SIZE + "px", + "text-anchor": "middle", + innerHTML: label, + }); - let group = createSVG('g', { - 'data-point-index': index, - transform: `translate(${x}, ${y})` - }); - group.appendChild(rect); - group.appendChild(text); + let group = createSVG("g", { + "data-point-index": index, + transform: `translate(${x}, ${y})`, + }); + group.appendChild(rect); + group.appendChild(text); - return group; - } + return group; + } } -export function datasetDot(x, y, radius, color, label='', index=0) { - let dot = createSVG('circle', { - style: `fill: ${color}`, - 'data-point-index': index, - cx: x, - cy: y, - r: radius - }); +export function datasetDot(x, y, radius, color, label = "", index = 0) { + let dot = createSVG("circle", { + style: `fill: ${color}`, + "data-point-index": index, + cx: x, + cy: y, + r: radius, + }); - label += ""; + label += ""; - if(!label && !label.length) { - return dot; - } else { - dot.setAttribute('cy', 0); - dot.setAttribute('cx', 0); + if (!label && !label.length) { + return dot; + } else { + dot.setAttribute("cy", 0); + dot.setAttribute("cx", 0); - let text = createSVG('text', { - className: 'data-point-value', - x: 0, - y: 0, - dy: (FONT_SIZE / 2 * -1 - radius) + 'px', - 'font-size': FONT_SIZE + 'px', - 'text-anchor': 'middle', - innerHTML: label - }); + let text = createSVG("text", { + className: "data-point-value", + x: 0, + y: 0, + dy: (FONT_SIZE / 2) * -1 - radius + "px", + "font-size": FONT_SIZE + "px", + "text-anchor": "middle", + innerHTML: label, + }); - let group = createSVG('g', { - 'data-point-index': index, - transform: `translate(${x}, ${y})` - }); - group.appendChild(dot); - group.appendChild(text); + let group = createSVG("g", { + "data-point-index": index, + transform: `translate(${x}, ${y})`, + }); + group.appendChild(dot); + group.appendChild(text); - return group; - } + return group; + } } -export function getPaths(xList, yList, color, options={}, meta={}) { - let pointsList = yList.map((y, i) => (xList[i] + ',' + y)); - let pointsStr = pointsList.join("L"); +export function getPaths(xList, yList, color, options = {}, meta = {}) { + let pointsList = yList.map((y, i) => xList[i] + "," + y); + let pointsStr = pointsList.join("L"); - // Spline - if (options.spline) - pointsStr = getSplineCurvePointsStr(xList, yList); + // Spline + if (options.spline) pointsStr = getSplineCurvePointsStr(xList, yList); - let path = makePath("M"+pointsStr, 'line-graph-path', color); + let path = makePath("M" + pointsStr, "line-graph-path", color); - // HeatLine - if(options.heatline) { - let gradient_id = makeGradient(meta.svgDefs, color); - path.style.stroke = `url(#${gradient_id})`; - } + // HeatLine + if (options.heatline) { + let gradient_id = makeGradient(meta.svgDefs, color); + path.style.stroke = `url(#${gradient_id})`; + } - let paths = { - path: path - }; + let paths = { + path: path, + }; - // Region - if(options.regionFill) { - let gradient_id_region = makeGradient(meta.svgDefs, color, true); + // Region + if (options.regionFill) { + let gradient_id_region = makeGradient(meta.svgDefs, color, true); - let pathStr = "M" + `${xList[0]},${meta.zeroLine}L` + pointsStr + `L${xList.slice(-1)[0]},${meta.zeroLine}`; - paths.region = makePath(pathStr, `region-fill`, 'none', `url(#${gradient_id_region})`); - } + let pathStr = + "M" + + `${xList[0]},${meta.zeroLine}L` + + pointsStr + + `L${xList.slice(-1)[0]},${meta.zeroLine}`; + paths.region = makePath( + pathStr, + `region-fill`, + "none", + `url(#${gradient_id_region})` + ); + } - return paths; + return paths; } export let makeOverlay = { - 'bar': (unit) => { - let transformValue; - if(unit.nodeName !== 'rect') { - transformValue = unit.getAttribute('transform'); - unit = unit.childNodes[0]; - } - let overlay = unit.cloneNode(); - overlay.style.fill = '#000000'; - overlay.style.opacity = '0.4'; + bar: (unit) => { + let transformValue; + if (unit.nodeName !== "rect") { + transformValue = unit.getAttribute("transform"); + unit = unit.childNodes[0]; + } + let overlay = unit.cloneNode(); + overlay.style.fill = "#000000"; + overlay.style.opacity = "0.4"; - if(transformValue) { - overlay.setAttribute('transform', transformValue); - } - return overlay; - }, + if (transformValue) { + overlay.setAttribute("transform", transformValue); + } + return overlay; + }, - 'dot': (unit) => { - let transformValue; - if(unit.nodeName !== 'circle') { - transformValue = unit.getAttribute('transform'); - unit = unit.childNodes[0]; - } - let overlay = unit.cloneNode(); - let radius = unit.getAttribute('r'); - let fill = unit.getAttribute('fill'); - overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR); - overlay.setAttribute('fill', fill); - overlay.style.opacity = '0.6'; + dot: (unit) => { + let transformValue; + if (unit.nodeName !== "circle") { + transformValue = unit.getAttribute("transform"); + unit = unit.childNodes[0]; + } + let overlay = unit.cloneNode(); + let radius = unit.getAttribute("r"); + let fill = unit.getAttribute("fill"); + overlay.setAttribute("r", parseInt(radius) + DOT_OVERLAY_SIZE_INCR); + overlay.setAttribute("fill", fill); + overlay.style.opacity = "0.6"; - if(transformValue) { - overlay.setAttribute('transform', transformValue); - } - return overlay; - }, + if (transformValue) { + overlay.setAttribute("transform", transformValue); + } + return overlay; + }, - 'heat_square': (unit) => { - let transformValue; - if(unit.nodeName !== 'circle') { - transformValue = unit.getAttribute('transform'); - unit = unit.childNodes[0]; - } - let overlay = unit.cloneNode(); - let radius = unit.getAttribute('r'); - let fill = unit.getAttribute('fill'); - overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR); - overlay.setAttribute('fill', fill); - overlay.style.opacity = '0.6'; + heat_square: (unit) => { + let transformValue; + if (unit.nodeName !== "circle") { + transformValue = unit.getAttribute("transform"); + unit = unit.childNodes[0]; + } + let overlay = unit.cloneNode(); + let radius = unit.getAttribute("r"); + let fill = unit.getAttribute("fill"); + overlay.setAttribute("r", parseInt(radius) + DOT_OVERLAY_SIZE_INCR); + overlay.setAttribute("fill", fill); + overlay.style.opacity = "0.6"; - if(transformValue) { - overlay.setAttribute('transform', transformValue); - } - return overlay; - } + if (transformValue) { + overlay.setAttribute("transform", transformValue); + } + return overlay; + }, }; export let updateOverlay = { - 'bar': (unit, overlay) => { - let transformValue; - if(unit.nodeName !== 'rect') { - transformValue = unit.getAttribute('transform'); - unit = unit.childNodes[0]; - } - let attributes = ['x', 'y', 'width', 'height']; - Object.values(unit.attributes) - .filter(attr => attributes.includes(attr.name) && attr.specified) - .map(attr => { - overlay.setAttribute(attr.name, attr.nodeValue); - }); + bar: (unit, overlay) => { + let transformValue; + if (unit.nodeName !== "rect") { + transformValue = unit.getAttribute("transform"); + unit = unit.childNodes[0]; + } + let attributes = ["x", "y", "width", "height"]; + Object.values(unit.attributes) + .filter((attr) => attributes.includes(attr.name) && attr.specified) + .map((attr) => { + overlay.setAttribute(attr.name, attr.nodeValue); + }); - if(transformValue) { - overlay.setAttribute('transform', transformValue); - } - }, + if (transformValue) { + overlay.setAttribute("transform", transformValue); + } + }, - 'dot': (unit, overlay) => { - let transformValue; - if(unit.nodeName !== 'circle') { - transformValue = unit.getAttribute('transform'); - unit = unit.childNodes[0]; - } - let attributes = ['cx', 'cy']; - Object.values(unit.attributes) - .filter(attr => attributes.includes(attr.name) && attr.specified) - .map(attr => { - overlay.setAttribute(attr.name, attr.nodeValue); - }); + dot: (unit, overlay) => { + let transformValue; + if (unit.nodeName !== "circle") { + transformValue = unit.getAttribute("transform"); + unit = unit.childNodes[0]; + } + let attributes = ["cx", "cy"]; + Object.values(unit.attributes) + .filter((attr) => attributes.includes(attr.name) && attr.specified) + .map((attr) => { + overlay.setAttribute(attr.name, attr.nodeValue); + }); - if(transformValue) { - overlay.setAttribute('transform', transformValue); - } - }, + if (transformValue) { + overlay.setAttribute("transform", transformValue); + } + }, - 'heat_square': (unit, overlay) => { - let transformValue; - if(unit.nodeName !== 'circle') { - transformValue = unit.getAttribute('transform'); - unit = unit.childNodes[0]; - } - let attributes = ['cx', 'cy']; - Object.values(unit.attributes) - .filter(attr => attributes.includes(attr.name) && attr.specified) - .map(attr => { - overlay.setAttribute(attr.name, attr.nodeValue); - }); + heat_square: (unit, overlay) => { + let transformValue; + if (unit.nodeName !== "circle") { + transformValue = unit.getAttribute("transform"); + unit = unit.childNodes[0]; + } + let attributes = ["cx", "cy"]; + Object.values(unit.attributes) + .filter((attr) => attributes.includes(attr.name) && attr.specified) + .map((attr) => { + overlay.setAttribute(attr.name, attr.nodeValue); + }); - if(transformValue) { - overlay.setAttribute('transform', transformValue); - } - }, + if (transformValue) { + overlay.setAttribute("transform", transformValue); + } + }, }; diff --git a/src/js/utils/export.js b/src/js/utils/export.js index 842dc3d..8b36c51 100644 --- a/src/js/utils/export.js +++ b/src/js/utils/export.js @@ -1,33 +1,33 @@ -import { $ } from '../utils/dom'; -import { CSSTEXT } from '../../css/chartsCss'; +import { $ } from "../utils/dom"; +import { CSSTEXT } from "../../css/chartsCss"; export function downloadFile(filename, data) { - var a = document.createElement('a'); - a.style = "display: none"; - var blob = new Blob(data, {type: "image/svg+xml; charset=utf-8"}); - var url = window.URL.createObjectURL(blob); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - setTimeout(function(){ - document.body.removeChild(a); - window.URL.revokeObjectURL(url); - }, 300); + var a = document.createElement("a"); + a.style = "display: none"; + var blob = new Blob(data, { type: "image/svg+xml; charset=utf-8" }); + var url = window.URL.createObjectURL(blob); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + setTimeout(function () { + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }, 300); } export function prepareForExport(svg) { - let clone = svg.cloneNode(true); - clone.classList.add('chart-container'); - clone.setAttribute('xmlns', "http://www.w3.org/2000/svg"); - clone.setAttribute('xmlns:xlink', "http://www.w3.org/1999/xlink"); - let styleEl = $.create('style', { - 'innerHTML': CSSTEXT - }); - clone.insertBefore(styleEl, clone.firstChild); + let clone = svg.cloneNode(true); + clone.classList.add("chart-container"); + clone.setAttribute("xmlns", "http://www.w3.org/2000/svg"); + clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); + let styleEl = $.create("style", { + innerHTML: CSSTEXT, + }); + clone.insertBefore(styleEl, clone.firstChild); - let container = $.create('div'); - container.appendChild(clone); + let container = $.create("div"); + container.appendChild(clone); - return container.innerHTML; + return container.innerHTML; } diff --git a/src/js/utils/helpers.js b/src/js/utils/helpers.js index 33b16cf..0ce5d71 100644 --- a/src/js/utils/helpers.js +++ b/src/js/utils/helpers.js @@ -1,11 +1,11 @@ -import { ANGLE_RATIO } from './constants'; +import { ANGLE_RATIO } from "./constants"; /** * Returns the value of a number upto 2 decimal places. * @param {Number} d Any number */ export function floatTwo(d) { - return parseFloat(d.toFixed(2)); + return parseFloat(d.toFixed(2)); } /** @@ -14,12 +14,12 @@ export function floatTwo(d) { * @param {Array} arr2 Second array */ export function arraysEqual(arr1, arr2) { - if (arr1.length !== arr2.length) return false; - let areEqual = true; - arr1.map((d, i) => { - if (arr2[i] !== d) areEqual = false; - }); - return areEqual; + if (arr1.length !== arr2.length) return false; + let areEqual = true; + arr1.map((d, i) => { + if (arr2[i] !== d) areEqual = false; + }); + return areEqual; } /** @@ -27,16 +27,16 @@ export function arraysEqual(arr1, arr2) { * @param {Array} array An array containing the items. */ export function shuffle(array) { - // Awesomeness: https://bost.ocks.org/mike/shuffle/ - // https://stackoverflow.com/a/2450976/6495043 - // https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array?noredirect=1&lq=1 + // Awesomeness: https://bost.ocks.org/mike/shuffle/ + // https://stackoverflow.com/a/2450976/6495043 + // https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array?noredirect=1&lq=1 - for (let i = array.length - 1; i > 0; i--) { - let j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j], array[i]]; - } + for (let i = array.length - 1; i > 0; i--) { + let j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } - return array; + return array; } /** @@ -47,12 +47,12 @@ export function shuffle(array) { * @param {Boolean} start fill at start? */ export function fillArray(array, count, element, start = false) { - if (!element) { - element = start ? array[0] : array[array.length - 1]; - } - let fillerArray = new Array(Math.abs(count)).fill(element); - array = start ? fillerArray.concat(array) : array.concat(fillerArray); - return array; + if (!element) { + element = start ? array[0] : array[array.length - 1]; + } + let fillerArray = new Array(Math.abs(count)).fill(element); + array = start ? fillerArray.concat(array) : array.concat(fillerArray); + return array; } /** @@ -61,36 +61,36 @@ export function fillArray(array, count, element, start = false) { * @param {Number} charWidth Width of single char in pixels */ export function getStringWidth(string, charWidth) { - return (string + "").length * charWidth; + return (string + "").length * charWidth; } export function bindChange(obj, getFn, setFn) { - return new Proxy(obj, { - set: function (target, prop, value) { - setFn(); - return Reflect.set(target, prop, value); - }, - get: function (target, prop) { - getFn(); - return Reflect.get(target, prop); - } - }); + return new Proxy(obj, { + set: function (target, prop, value) { + setFn(); + return Reflect.set(target, prop, value); + }, + get: function (target, prop) { + getFn(); + return Reflect.get(target, prop); + }, + }); } // https://stackoverflow.com/a/29325222 export function getRandomBias(min, max, bias, influence) { - const range = max - min; - const biasValue = range * bias + min; - var rnd = Math.random() * range + min, // random in range - mix = Math.random() * influence; // random mixer - return rnd * (1 - mix) + biasValue * mix; // mix full range and bias + const range = max - min; + const biasValue = range * bias + min; + var rnd = Math.random() * range + min, // random in range + mix = Math.random() * influence; // random mixer + return rnd * (1 - mix) + biasValue * mix; // mix full range and bias } export function getPositionByAngle(angle, radius) { - return { - x: Math.sin(angle * ANGLE_RATIO) * radius, - y: Math.cos(angle * ANGLE_RATIO) * radius, - }; + return { + x: Math.sin(angle * ANGLE_RATIO) * radius, + y: Math.cos(angle * ANGLE_RATIO) * radius, + }; } /** @@ -99,11 +99,11 @@ export function getPositionByAngle(angle, radius) { * @param {Boolean} nonNegative flag to treat negative number as invalid */ export function isValidNumber(candidate, nonNegative = false) { - if (Number.isNaN(candidate)) return false; - else if (candidate === undefined) return false; - else if (!Number.isFinite(candidate)) return false; - else if (nonNegative && candidate < 0) return false; - else return true; + if (Number.isNaN(candidate)) return false; + else if (candidate === undefined) return false; + else if (!Number.isFinite(candidate)) return false; + else if (nonNegative && candidate < 0) return false; + else return true; } /** @@ -111,9 +111,9 @@ export function isValidNumber(candidate, nonNegative = false) { * @param {Number} d Any Number */ export function round(d) { - // https://floating-point-gui.de/ - // https://www.jacklmoore.com/notes/rounding-in-javascript/ - return Number(Math.round(d + 'e4') + 'e-4'); + // https://floating-point-gui.de/ + // https://www.jacklmoore.com/notes/rounding-in-javascript/ + return Number(Math.round(d + "e4") + "e-4"); } /** @@ -121,23 +121,23 @@ export function round(d) { * @param {Object} candidate Any Object */ export function deepClone(candidate) { - let cloned, value, key; + let cloned, value, key; - if (candidate instanceof Date) { - return new Date(candidate.getTime()); - } + if (candidate instanceof Date) { + return new Date(candidate.getTime()); + } - if (typeof candidate !== "object" || candidate === null) { - return candidate; - } + if (typeof candidate !== "object" || candidate === null) { + return candidate; + } - cloned = Array.isArray(candidate) ? [] : {}; + cloned = Array.isArray(candidate) ? [] : {}; - for (key in candidate) { - value = candidate[key]; + for (key in candidate) { + value = candidate[key]; - cloned[key] = deepClone(value); - } + cloned[key] = deepClone(value); + } - return cloned; -} \ No newline at end of file + return cloned; +} diff --git a/src/js/utils/intervals.js b/src/js/utils/intervals.js index 35e0f3d..21cd604 100644 --- a/src/js/utils/intervals.js +++ b/src/js/utils/intervals.js @@ -1,247 +1,247 @@ -import { floatTwo } from './helpers'; +import { floatTwo } from "./helpers"; function normalize(x) { - // Calculates mantissa and exponent of a number - // Returns normalized number and exponent - // https://stackoverflow.com/q/9383593/6495043 + // Calculates mantissa and exponent of a number + // Returns normalized number and exponent + // https://stackoverflow.com/q/9383593/6495043 - if(x===0) { - return [0, 0]; - } - if(isNaN(x)) { - return {mantissa: -6755399441055744, exponent: 972}; - } - var sig = x > 0 ? 1 : -1; - if(!isFinite(x)) { - return {mantissa: sig * 4503599627370496, exponent: 972}; - } + if (x === 0) { + return [0, 0]; + } + if (isNaN(x)) { + return { mantissa: -6755399441055744, exponent: 972 }; + } + var sig = x > 0 ? 1 : -1; + if (!isFinite(x)) { + return { mantissa: sig * 4503599627370496, exponent: 972 }; + } - x = Math.abs(x); - var exp = Math.floor(Math.log10(x)); - var man = x/Math.pow(10, exp); + x = Math.abs(x); + var exp = Math.floor(Math.log10(x)); + var man = x / Math.pow(10, exp); - return [sig * man, exp]; + return [sig * man, exp]; } -function getChartRangeIntervals(max, min=0) { - let upperBound = Math.ceil(max); - let lowerBound = Math.floor(min); - let range = upperBound - lowerBound; +function getChartRangeIntervals(max, min = 0) { + let upperBound = Math.ceil(max); + let lowerBound = Math.floor(min); + let range = upperBound - lowerBound; - let noOfParts = range; - let partSize = 1; + let noOfParts = range; + let partSize = 1; - // To avoid too many partitions - if(range > 5) { - if(range % 2 !== 0) { - upperBound++; - // Recalc range - range = upperBound - lowerBound; - } - noOfParts = range/2; - partSize = 2; - } + // To avoid too many partitions + if (range > 5) { + if (range % 2 !== 0) { + upperBound++; + // Recalc range + range = upperBound - lowerBound; + } + noOfParts = range / 2; + partSize = 2; + } - // Special case: 1 and 2 - if(range <= 2) { - noOfParts = 4; - partSize = range/noOfParts; - } + // Special case: 1 and 2 + if (range <= 2) { + noOfParts = 4; + partSize = range / noOfParts; + } - // Special case: 0 - if(range === 0) { - noOfParts = 5; - partSize = 1; - } + // Special case: 0 + if (range === 0) { + noOfParts = 5; + partSize = 1; + } - let intervals = []; - for(var i = 0; i <= noOfParts; i++){ - intervals.push(lowerBound + partSize * i); - } - return intervals; + let intervals = []; + for (var i = 0; i <= noOfParts; i++) { + intervals.push(lowerBound + partSize * i); + } + return intervals; } -function getChartIntervals(maxValue, minValue=0) { - let [normalMaxValue, exponent] = normalize(maxValue); - let normalMinValue = minValue ? minValue/Math.pow(10, exponent): 0; +function getChartIntervals(maxValue, minValue = 0) { + let [normalMaxValue, exponent] = normalize(maxValue); + let normalMinValue = minValue ? minValue / Math.pow(10, exponent) : 0; - // Allow only 7 significant digits - normalMaxValue = normalMaxValue.toFixed(6); + // Allow only 7 significant digits + normalMaxValue = normalMaxValue.toFixed(6); - let intervals = getChartRangeIntervals(normalMaxValue, normalMinValue); - intervals = intervals.map(value => { - // For negative exponents we want to divide by 10^-exponent to avoid - // floating point arithmetic bugs. For instance, in javascript - // 6 * 10^-1 == 0.6000000000000001, we instead want 6 / 10^1 == 0.6 - if (exponent < 0) { - return value / Math.pow(10, -exponent); - } - return value * Math.pow(10, exponent); - }); - return intervals; + let intervals = getChartRangeIntervals(normalMaxValue, normalMinValue); + intervals = intervals.map((value) => { + // For negative exponents we want to divide by 10^-exponent to avoid + // floating point arithmetic bugs. For instance, in javascript + // 6 * 10^-1 == 0.6000000000000001, we instead want 6 / 10^1 == 0.6 + if (exponent < 0) { + return value / Math.pow(10, -exponent); + } + return value * Math.pow(10, exponent); + }); + return intervals; } -export function calcChartIntervals(values, withMinimum=false) { - //*** Where the magic happens *** +export function calcChartIntervals(values, withMinimum = false) { + //*** Where the magic happens *** - // Calculates best-fit y intervals from given values - // and returns the interval array + // Calculates best-fit y intervals from given values + // and returns the interval array - let maxValue = Math.max(...values); - let minValue = Math.min(...values); + let maxValue = Math.max(...values); + let minValue = Math.min(...values); - // Exponent to be used for pretty print - let exponent = 0, intervals = []; // eslint-disable-line no-unused-vars + // Exponent to be used for pretty print + let exponent = 0, + intervals = []; // eslint-disable-line no-unused-vars - function getPositiveFirstIntervals(maxValue, absMinValue) { - let intervals = getChartIntervals(maxValue); + function getPositiveFirstIntervals(maxValue, absMinValue) { + let intervals = getChartIntervals(maxValue); - let intervalSize = intervals[1] - intervals[0]; + let intervalSize = intervals[1] - intervals[0]; - // Then unshift the negative values - let value = 0; - for(var i = 1; value < absMinValue; i++) { - value += intervalSize; - intervals.unshift((-1) * value); - } - return intervals; - } + // Then unshift the negative values + let value = 0; + for (var i = 1; value < absMinValue; i++) { + value += intervalSize; + intervals.unshift(-1 * value); + } + return intervals; + } - // CASE I: Both non-negative + // CASE I: Both non-negative - if(maxValue >= 0 && minValue >= 0) { - exponent = normalize(maxValue)[1]; - if(!withMinimum) { - intervals = getChartIntervals(maxValue); - } else { - intervals = getChartIntervals(maxValue, minValue); - } - } + if (maxValue >= 0 && minValue >= 0) { + exponent = normalize(maxValue)[1]; + if (!withMinimum) { + intervals = getChartIntervals(maxValue); + } else { + intervals = getChartIntervals(maxValue, minValue); + } + } - // CASE II: Only minValue negative + // CASE II: Only minValue negative + else if (maxValue > 0 && minValue < 0) { + // `withMinimum` irrelevant in this case, + // We'll be handling both sides of zero separately + // (both starting from zero) + // Because ceil() and floor() behave differently + // in those two regions - else if(maxValue > 0 && minValue < 0) { - // `withMinimum` irrelevant in this case, - // We'll be handling both sides of zero separately - // (both starting from zero) - // Because ceil() and floor() behave differently - // in those two regions + let absMinValue = Math.abs(minValue); - let absMinValue = Math.abs(minValue); + if (maxValue >= absMinValue) { + exponent = normalize(maxValue)[1]; + intervals = getPositiveFirstIntervals(maxValue, absMinValue); + } else { + // Mirror: maxValue => absMinValue, then change sign + exponent = normalize(absMinValue)[1]; + let posIntervals = getPositiveFirstIntervals(absMinValue, maxValue); + intervals = posIntervals.reverse().map((d) => d * -1); + } + } - if(maxValue >= absMinValue) { - exponent = normalize(maxValue)[1]; - intervals = getPositiveFirstIntervals(maxValue, absMinValue); - } else { - // Mirror: maxValue => absMinValue, then change sign - exponent = normalize(absMinValue)[1]; - let posIntervals = getPositiveFirstIntervals(absMinValue, maxValue); - intervals = posIntervals.reverse().map(d => d * (-1)); - } + // CASE III: Both non-positive + else if (maxValue <= 0 && minValue <= 0) { + // Mirrored Case I: + // Work with positives, then reverse the sign and array - } + let pseudoMaxValue = Math.abs(minValue); + let pseudoMinValue = Math.abs(maxValue); - // CASE III: Both non-positive + exponent = normalize(pseudoMaxValue)[1]; + if (!withMinimum) { + intervals = getChartIntervals(pseudoMaxValue); + } else { + intervals = getChartIntervals(pseudoMaxValue, pseudoMinValue); + } - else if(maxValue <= 0 && minValue <= 0) { - // Mirrored Case I: - // Work with positives, then reverse the sign and array + intervals = intervals.reverse().map((d) => d * -1); + } - let pseudoMaxValue = Math.abs(minValue); - let pseudoMinValue = Math.abs(maxValue); - - exponent = normalize(pseudoMaxValue)[1]; - if(!withMinimum) { - intervals = getChartIntervals(pseudoMaxValue); - } else { - intervals = getChartIntervals(pseudoMaxValue, pseudoMinValue); - } - - intervals = intervals.reverse().map(d => d * (-1)); - } - - return intervals.sort((a, b) => (a - b)); + return intervals.sort((a, b) => a - b); } export function getZeroIndex(yPts) { - let zeroIndex; - let interval = getIntervalSize(yPts); - if(yPts.indexOf(0) >= 0) { - // the range has a given zero - // zero-line on the chart - zeroIndex = yPts.indexOf(0); - } else if(yPts[0] > 0) { - // Minimum value is positive - // zero-line is off the chart: below - let min = yPts[0]; - zeroIndex = (-1) * min / interval; - } else { - // Maximum value is negative - // zero-line is off the chart: above - let max = yPts[yPts.length - 1]; - zeroIndex = (-1) * max / interval + (yPts.length - 1); - } - return zeroIndex; + let zeroIndex; + let interval = getIntervalSize(yPts); + if (yPts.indexOf(0) >= 0) { + // the range has a given zero + // zero-line on the chart + zeroIndex = yPts.indexOf(0); + } else if (yPts[0] > 0) { + // Minimum value is positive + // zero-line is off the chart: below + let min = yPts[0]; + zeroIndex = (-1 * min) / interval; + } else { + // Maximum value is negative + // zero-line is off the chart: above + let max = yPts[yPts.length - 1]; + zeroIndex = (-1 * max) / interval + (yPts.length - 1); + } + return zeroIndex; } export function getRealIntervals(max, noOfIntervals, min = 0, asc = 1) { - let range = max - min; - let part = range * 1.0 / noOfIntervals; - let intervals = []; + let range = max - min; + let part = (range * 1.0) / noOfIntervals; + let intervals = []; - for(var i = 0; i <= noOfIntervals; i++) { - intervals.push(min + part * i); - } + for (var i = 0; i <= noOfIntervals; i++) { + intervals.push(min + part * i); + } - return asc ? intervals : intervals.reverse(); + return asc ? intervals : intervals.reverse(); } export function getIntervalSize(orderedArray) { - return orderedArray[1] - orderedArray[0]; + return orderedArray[1] - orderedArray[0]; } export function getValueRange(orderedArray) { - return orderedArray[orderedArray.length-1] - orderedArray[0]; + return orderedArray[orderedArray.length - 1] - orderedArray[0]; } export function scale(val, yAxis) { - return floatTwo(yAxis.zeroLine - val * yAxis.scaleMultiplier); + return floatTwo(yAxis.zeroLine - val * yAxis.scaleMultiplier); } export function isInRange(val, min, max) { - return val > min && val < max; + return val > min && val < max; } export function isInRange2D(coord, minCoord, maxCoord) { - return isInRange(coord[0], minCoord[0], maxCoord[0]) - && isInRange(coord[1], minCoord[1], maxCoord[1]); + return ( + isInRange(coord[0], minCoord[0], maxCoord[0]) && + isInRange(coord[1], minCoord[1], maxCoord[1]) + ); } export function getClosestInArray(goal, arr, index = false) { - let closest = arr.reduce(function(prev, curr) { - return (Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev); - }, []); + let closest = arr.reduce(function (prev, curr) { + return Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev; + }, []); - return index ? arr.indexOf(closest) : closest; + return index ? arr.indexOf(closest) : closest; } export function calcDistribution(values, distributionSize) { - // Assume non-negative values, - // implying distribution minimum at zero + // Assume non-negative values, + // implying distribution minimum at zero - let dataMaxValue = Math.max(...values); + let dataMaxValue = Math.max(...values); - let distributionStep = 1 / (distributionSize - 1); - let distribution = []; + let distributionStep = 1 / (distributionSize - 1); + let distribution = []; - for(var i = 0; i < distributionSize; i++) { - let checkpoint = dataMaxValue * (distributionStep * i); - distribution.push(checkpoint); - } + for (var i = 0; i < distributionSize; i++) { + let checkpoint = dataMaxValue * (distributionStep * i); + distribution.push(checkpoint); + } - return distribution; + return distribution; } export function getMaxCheckpoint(value, distribution) { - return distribution.filter(d => d < value).length; + return distribution.filter((d) => d < value).length; } diff --git a/src/js/utils/test/colors.test.js b/src/js/utils/test/colors.test.js index 3b8d276..9365194 100644 --- a/src/js/utils/test/colors.test.js +++ b/src/js/utils/test/colors.test.js @@ -1,14 +1,14 @@ -const assert = require('assert'); -const colors = require('../colors'); +const assert = require("assert"); +const colors = require("../colors"); -describe('utils.colors', () => { - it('should return #aaabac for RGB()', () => { - assert.equal(colors.getColor('rgb(170, 171, 172)'), '#aaabac'); +describe("utils.colors", () => { + it("should return #aaabac for RGB()", () => { + assert.equal(colors.getColor("rgb(170, 171, 172)"), "#aaabac"); }); - it('should return #ff5858 for the named color red', () => { - assert.equal(colors.getColor('red'), '#ff5858d'); + it("should return #ff5858 for the named color red", () => { + assert.equal(colors.getColor("red"), "#ff5858d"); }); - it('should return #1a5c29 for the hex color #1a5c29', () => { - assert.equal(colors.getColor('#1a5c29'), '#1a5c29'); + it("should return #1a5c29 for the hex color #1a5c29", () => { + assert.equal(colors.getColor("#1a5c29"), "#1a5c29"); }); -}); \ No newline at end of file +}); diff --git a/src/js/utils/test/helpers.test.js b/src/js/utils/test/helpers.test.js index 0d8e7e3..69c0ae3 100644 --- a/src/js/utils/test/helpers.test.js +++ b/src/js/utils/test/helpers.test.js @@ -1,10 +1,10 @@ -const assert = require('assert'); -const helpers = require('../helpers'); +const assert = require("assert"); +const helpers = require("../helpers"); -describe('utils.helpers', () => { - it('should return a value fixed upto 2 decimals', () => { +describe("utils.helpers", () => { + it("should return a value fixed upto 2 decimals", () => { assert.equal(helpers.floatTwo(1.234), 1.23); assert.equal(helpers.floatTwo(1.456), 1.46); - assert.equal(helpers.floatTwo(1), 1.00); + assert.equal(helpers.floatTwo(1), 1.0); }); -}); \ No newline at end of file +});