diff --git a/src/js/charts/BaseChart.js b/src/js/charts/BaseChart.js index 5a63c7d..08b9f57 100644 --- a/src/js/charts/BaseChart.js +++ b/src/js/charts/BaseChart.js @@ -1,26 +1,26 @@ 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 { LEGEND_ITEM_WIDTH } from "../utils/constants"; 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"; @@ -28,348 +28,353 @@ 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, options.config); + 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 : 0, + 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, true); - }, this.initTimeout); - } + if (init) { + this.data = this.realData; + setTimeout(() => { + this.update(this.data, true); + }, this.initTimeout); + } - if (this.config.showLegend) { - this.renderLegend(); - } + if (this.config.showLegend) { + 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, drawing = false) { - if (!data) console.error("No data to update."); - if (!drawing) data = deepClone(data); - this.data = this.prepareData(data); - this.calc(); // builds state - this.render(this.components, this.config.animate); - } + update(data, drawing = false) { + if (!data) console.error("No data to update."); + if (!drawing) data = deepClone(data); + this.data = this.prepareData(data); + this.calc(); // builds state + this.render(this.components, this.config.animate); + } - 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(dataset) { - this.legendArea.textContent = ""; - let count = 0; - let y = 0; + renderLegend(dataset) { + this.legendArea.textContent = ""; + let count = 0; + let y = 0; - dataset.map((data, index) => { - let divisor = Math.floor(this.width / LEGEND_ITEM_WIDTH); - if (count > divisor) { - count = 0; - y += this.config.legendRowHeight; - } - let x = LEGEND_ITEM_WIDTH * count; - let dot = this.makeLegend(data, index, x, y); - this.legendArea.appendChild(dot); - count++; - }); - } + dataset.map((data, index) => { + let divisor = Math.floor(this.width / LEGEND_ITEM_WIDTH); + if (count > divisor) { + count = 0; + y += this.config.legendRowHeight; + } + let x = LEGEND_ITEM_WIDTH * count; + let dot = this.makeLegend(data, index, x, y); + this.legendArea.appendChild(dot); + count++; + }); + } - makeLegend() {} + makeLegend() {} - 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/utils/helpers.js b/src/js/utils/helpers.js index 0ce5d71..6b7e4f3 100644 --- a/src/js/utils/helpers.js +++ b/src/js/utils/helpers.js @@ -5,7 +5,7 @@ import { ANGLE_RATIO } from "./constants"; * @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 == undefined) { + 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; + return cloned; }