{"version":3,"file":"main.513450b9.js","sources":["../src/scripts/utils.js","../src/scripts/plotter.js","../src/scripts/animation.js","../src/scripts/constants.js","../src/scripts/plot-base.js","../src/scripts/xaxis.js","../src/scripts/yaxis.js","../src/scripts/legend.js","../src/scripts/plot.js","../src/scripts/timeline.js","../src/scripts/filter.js","../src/scripts/chart.js","../src/scripts/main.js"],"sourcesContent":["const months = [\n 'Jan',\n 'Feb',\n 'Mar',\n 'Apr',\n 'May',\n 'Jun',\n 'Jul',\n 'Aug',\n 'Sep',\n 'Oct',\n 'Nov',\n 'Dec',\n];\nconst days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];\n\nexport const min = array => Math.min(...array);\n\nexport const max = array => Math.max(...array);\n\nexport const viewBoxToArray = viewBox =>\n viewBox.split(' ').map(value => +value);\n\nexport const arrayToViewBox = array => array.join(' ');\n\nexport const leftIndexFromRatio = (array, ratio) =>\n Math.floor((array.length - 1) * ratio);\n\nexport const rightIndexFromRatio = (array, ratio) =>\n Math.ceil((array.length - 1) * ratio);\n\nexport const createElement = (\n tag,\n { classes = '', style = {}, ...options } = {}\n) => {\n const element = document.createElement(tag);\n\n element.className = classes;\n Object.assign(element.style, style);\n\n objectForEach(options || {}, (key, value) =>\n element.setAttribute(key, value)\n );\n\n return element;\n};\n\nexport const createSvgElement = (tag, { classes = '', ...options }) => {\n const element = document.createElementNS('http://www.w3.org/2000/svg', tag);\n\n element.setAttribute('class', classes);\n\n objectForEach(options || {}, (key, value) =>\n element.setAttribute(key, value)\n );\n\n return element;\n};\n\nexport const pairsToObject = pairs =>\n pairs.reduce((acc, [key, value]) => ((acc[key] = value), acc), {});\n\nexport const objectFilter = (obj, func) =>\n pairsToObject(Object.entries(obj).filter(([key, value]) => func(key, value)));\n\nexport const objectMap = (obj, func) =>\n pairsToObject(\n Object.entries(obj).map(([key, value]) => [key, func(key, value)])\n );\n\nexport const objectForEach = (obj, func) =>\n Object.entries(obj).forEach(([key, value]) => func(key, value));\n\nexport const formatDate = timestamp => {\n const date = new Date(timestamp);\n\n return `${days[date.getUTCDay()]}, ${\n months[date.getUTCMonth()]\n } ${date.getUTCDate()}`;\n};\n\nexport const formatAxisDate = timestamp => {\n const date = new Date(timestamp);\n\n return `${months[date.getUTCMonth()]} ${date.getUTCDate()}`;\n};\n\nexport const debounce = (func, context, delay = 0) => {\n let timeout;\n\n return (...args) => {\n if (timeout) clearTimeout(timeout);\n\n timeout = setTimeout(\n () => requestAnimationFrame(func.bind(context, ...args)),\n delay\n );\n };\n};\n\nexport const nearestPow = value =>\n Math.pow(2, Math.round(Math.log(value) / Math.log(2)));\n\nexport const getClientX = event =>\n event.targetTouches ? event.targetTouches[0].clientX : event.clientX;\n","import { max, leftIndexFromRatio, rightIndexFromRatio } from './utils';\n\nconst graphsMax = (graphs, imin, imax) =>\n max(\n Object.values(graphs).map(({ values, visible }) =>\n visible ? max(values.slice(imin, imax)) : Number.MIN_SAFE_INTEGER\n )\n );\n\nclass Plotter {\n constructor({ $svg, width, height, x, graphs }) {\n this.$svg = $svg;\n\n this.x = x;\n this.graphs = graphs;\n\n this.resize(width, height);\n }\n\n get screen() {\n return {\n x: [0, this.width],\n y: [0, this.height],\n };\n }\n\n /**\n * Conversion between coordinate systems.\n */\n\n toScreenX(v, domain = this.domain) {\n const dx = domain.x;\n const sx = this.screen.x;\n\n return sx[0] + ((v - dx[0]) / (dx[1] - dx[0])) * (sx[1] - sx[0]);\n }\n\n toScreenY(v, domain = this.domain) {\n const dy = domain.y;\n const sy = this.screen.y;\n\n return sy[1] - sy[0] - ((v - dy[0]) / (dy[1] - dy[0])) * (sy[1] - sy[0]);\n }\n\n toCurrentScreenX(v) {\n return this.toScreenX(v, this.current);\n }\n toCurrentScreenY(v) {\n return this.toScreenY(v, this.current);\n }\n\n toPreviousScreenX(v) {\n return this.toScreenX(v, this.previous);\n }\n toPreviousScreenY(v) {\n return this.toScreenY(v, this.previous);\n }\n\n toScreen(d) {\n return {\n x: [this.toScreenX(d.x[0]), this.toScreenX(d.x[1])],\n y: [this.toScreenY(d.y[1]), this.toScreenY(d.y[0])],\n };\n }\n\n toDomainX(v, domain = this.domain) {\n const dx = domain.x;\n const sx = this.screen.x;\n\n return dx[0] + ((v - sx[0]) / (sx[1] - sx[0])) * (dx[1] - dx[0]);\n }\n\n toDomainY(v, domain = this.domain) {\n const dy = domain.y;\n const sy = this.screen.y;\n\n return dy[1] - dy[0] - ((v - sy[0]) / (sy[1] - sy[0])) * (dy[1] - dy[0]);\n }\n\n toCurrentDomainX(v) {\n return this.toDomainX(v, this.current);\n }\n toCurrentDomainY(v) {\n return this.toDomainY(v, this.current);\n }\n\n toPreviousDomainX(v) {\n return this.toDomainX(v, this.previous);\n }\n toPreviousDomainY(v) {\n return this.toDomainY(v, this.previous);\n }\n\n /**\n * For initial PlotBase rendering.\n */\n\n viewBoxFromScreen(s = this.screen) {\n return `${s.x[0]} ${s.y[0]} ${s.x[1] - s.x[0]} ${s.y[1] - s.y[0]}`;\n }\n\n getPathLine(key) {\n return this.x.reduce((result, value, index) => {\n const x = this.toScreenX(value);\n const y = this.toScreenY(this.graphs[key].values[index]);\n\n return index === 0 ? `M${x} ${y}` : `${result} L ${x} ${y}`;\n }, '');\n }\n\n /**\n * For getting viewBox for a given selection.\n */\n\n domainFromRatios(left = this.left, right = this.right) {\n const imin = leftIndexFromRatio(this.x, left);\n const imax = rightIndexFromRatio(this.x, right);\n\n const xgmin = this.x[0];\n const xgmax = this.x[this.x.length - 1];\n\n const xmin = xgmin + (xgmax - xgmin) * left;\n const xmax = xgmin + (xgmax - xgmin) * right;\n const ymin = 0;\n const ymax = graphsMax(this.graphs, imin, imax);\n\n return {\n x: [xmin, xmax],\n y: [ymin, ymax],\n };\n }\n\n viewBoxFromRatios() {\n const s = this.toScreen(this.current);\n\n return this.viewBoxFromScreen(s);\n }\n\n /**\n * Update methods.\n */\n\n setRatios(a, b) {\n this.left = a;\n this.right = b;\n this.updateDomain();\n }\n\n updateDomain() {\n this.previous = Object.assign({}, this.current);\n this.current = this.domainFromRatios();\n }\n\n resize(width, height) {\n this.left = 0;\n this.right = 1;\n\n this.width = width;\n this.height = height;\n\n this.domain = this.domainFromRatios();\n this.current = Object.assign({}, this.domain);\n this.previous = Object.assign({}, this.domain);\n\n this.$svg.setAttribute('width', width);\n this.$svg.setAttribute('height', height);\n this.$svg.setAttribute('viewBox', this.viewBoxFromScreen());\n }\n}\n\nexport default Plotter;\n","import { viewBoxToArray } from './utils';\n\nexport const animateViewBox = ({ $svg, to, duration, callback }) => {\n let start;\n\n const from = $svg.getAttribute('viewBox');\n const [xminFrom, yminFrom, xmaxFrom, ymaxFrom] = viewBoxToArray(from);\n const [xminTo, yminTo, xmaxTo, ymaxTo] = viewBoxToArray(to);\n\n const xminDelta = xminTo - xminFrom;\n const yminDelta = yminTo - yminFrom;\n const xmaxDelta = xmaxTo - xmaxFrom;\n const ymaxDelta = ymaxTo - ymaxFrom;\n\n const animateStep = now => {\n if (!start) start = now;\n\n const progress = Math.min((now - start) / duration, 1);\n\n const xmin = xminFrom + xminDelta * progress;\n const ymin = yminFrom + yminDelta * progress;\n const xmax = xmaxFrom + xmaxDelta * progress;\n const ymax = ymaxFrom + ymaxDelta * progress;\n\n $svg.setAttribute('viewBox', `${xmin} ${ymin} ${xmax} ${ymax}`);\n\n if (progress < 1) {\n $svg.animation = requestAnimationFrame(animateStep);\n } else if (callback) {\n callback();\n }\n };\n\n cancelAnimationFrame($svg.animation);\n\n $svg.animation = requestAnimationFrame(animateStep);\n};\n","export const ChartType = Object.freeze({\n x: 'x',\n line: 'line',\n});\n\nexport const BaseAnimationDuration = 300;\n","import Plotter from './plotter';\nimport { animateViewBox } from './animation';\nimport { createSvgElement, objectForEach } from './utils';\nimport { BaseAnimationDuration } from './constants';\n\nclass PlotBase {\n constructor({ x, graphs }) {\n this.x = x;\n this.graphs = graphs;\n }\n\n appendTo($container) {\n this.$container = $container;\n this.render();\n }\n\n render() {\n const width = this.$container.clientWidth;\n const height = this.$container.clientHeight;\n\n this.$element = createSvgElement('svg', { preserveAspectRatio: 'none' });\n\n this.plotter = new Plotter({\n width,\n height,\n $svg: this.$element,\n x: this.x,\n graphs: this.graphs,\n });\n\n this.renderPathLines();\n this.$container.appendChild(this.$element);\n }\n\n renderPathLines() {\n this.$paths = {};\n\n objectForEach(this.graphs, (key, { color }) => {\n this.$paths[key] = createSvgElement('path', {\n d: this.plotter.getPathLine(key),\n stroke: color,\n });\n\n this.$element.appendChild(this.$paths[key]);\n });\n }\n\n updatePathLines() {\n objectForEach(this.graphs, key => {\n this.$paths[key].setAttribute('d', this.plotter.getPathLine(key));\n });\n }\n\n update() {\n objectForEach(this.graphs, (key, { visible }) => {\n this.$paths[key].classList.toggle('hidden', !visible);\n });\n\n this.plotter.updateDomain();\n\n animateViewBox({\n $svg: this.$element,\n to: this.plotter.viewBoxFromRatios(),\n duration: BaseAnimationDuration,\n });\n }\n\n resize() {\n const width = this.$container.clientWidth;\n const height = this.$container.clientHeight;\n\n this.plotter.resize(width, height);\n this.updatePathLines();\n }\n}\n\nexport default PlotBase;\n","import { createElement, formatAxisDate, nearestPow } from './utils';\n\nconst labelWidth = 50;\nconst initialDistance = 90;\n\nclass XAxis {\n constructor({ x, plotter }) {\n this.x = x;\n this.plotter = plotter;\n }\n\n appendTo($container) {\n this.$container = $container;\n this.render();\n }\n\n render() {\n this.$element = createElement('div', { classes: 'xaxis' });\n this.$container.appendChild(this.$element);\n\n this.$labels = [];\n this.visibleLabels = [];\n this.renderLabels();\n\n this.renderShadows();\n }\n\n renderLabels() {\n this.x.forEach((x, index) => {\n const value = formatAxisDate(x);\n const coord = this.plotter.toCurrentScreenX(x);\n\n const [xmin, xmax] = this.plotter.screen.x;\n this.delta = nearestPow(\n Math.floor((this.x.length * initialDistance) / (xmax - xmin))\n );\n\n const $label = createElement('div', {\n classes: `label ${index % this.delta === 0 ? '' : 'hidden'}`,\n style: {\n left: `${coord - labelWidth / 2}px`,\n width: `${labelWidth}px`,\n },\n });\n\n $label.innerText = value;\n $label.value = x;\n\n this.$labels.push($label);\n this.$element.appendChild($label);\n });\n }\n\n renderShadows() {\n const leftShadow = createElement('div', { classes: 'shadow left' });\n const rightShadow = createElement('div', { classes: 'shadow right' });\n\n this.$element.appendChild(leftShadow);\n this.$element.appendChild(rightShadow);\n }\n\n update() {\n const [xmin, xmax] = this.plotter.domain.x;\n const delta = nearestPow(\n Math.floor(\n (this.x.length * initialDistance) /\n (this.plotter.toCurrentScreenX(xmax) -\n this.plotter.toCurrentScreenX(xmin))\n )\n );\n\n if (delta >= 1) {\n if (delta < this.delta) {\n this.delta = this.delta / 2;\n } else if (delta > this.delta) {\n this.delta = this.delta * 2;\n }\n }\n\n this.$labels.forEach(($label, index) => {\n const coord = this.plotter.toCurrentScreenX($label.value);\n $label.style['left'] = `${coord - labelWidth / 2}px`;\n\n $label.classList.toggle('hidden', index % this.delta !== 0);\n });\n }\n}\n\nexport default XAxis;\n","import { createElement } from './utils';\n\nconst numberOfLabels = 6;\nconst topMargin = 20;\n\nclass YAxis {\n constructor({ x, graphs, plotter }) {\n this.x = x;\n this.graphs = graphs;\n this.plotter = plotter;\n }\n\n appendTo($container) {\n this.$container = $container;\n this.render();\n }\n\n render() {\n this.$element = createElement('div', { classes: 'yaxis' });\n this.$labels = [];\n\n this.renderLabels();\n\n this.$container.appendChild(this.$element);\n }\n\n renderLabels() {\n const [ymin, ymax] = this.plotter.screen.y;\n const delta = ymax - topMargin - ymin;\n const step = delta / (numberOfLabels - 1);\n\n for (let index = 0; index < numberOfLabels; ++index) {\n const y = ymin + topMargin + index * step;\n const value = Math.round(this.plotter.toCurrentDomainY(y));\n\n const $label = createElement('div', {\n classes: 'label',\n style: { top: `${y}px` },\n });\n\n const $line = createElement('hr', { classes: 'label-line' });\n const $text = createElement('span', { classes: 'label-text' });\n\n $text.innerText = value;\n\n $label.appendChild($text);\n $label.appendChild($line);\n\n const $altLabel = $label.cloneNode(true);\n\n $altLabel.classList.add('hidden');\n\n $label.y = y;\n $label.$text = $label.querySelector('.label-text');\n\n $altLabel.y = y;\n $altLabel.$text = $altLabel.querySelector('.label-text');\n\n this.$labels.push($label);\n this.$labels.push($altLabel);\n\n this.$element.appendChild($label);\n this.$element.appendChild($altLabel);\n }\n }\n\n update(domain = this.plotter.previous) {\n if (\n domain.y[0] === this.plotter.current.y[0] &&\n domain.y[1] === this.plotter.current.y[1]\n ) {\n return;\n }\n\n this.$labels.forEach($label => {\n const hidden = $label.classList.contains('hidden');\n\n if (hidden) {\n const to = $label.y;\n const value = this.plotter.toCurrentDomainY(to);\n const from = this.plotter.toScreenY(value, domain);\n const text = Math.round(value);\n\n $label.$text.innerText = text;\n\n // TODO: Set initial position without animation.\n $label.style['transform'] = `translateY(${to - from}px)`;\n $label.classList.remove('hidden');\n $label.style['transform'] = 'translateY(0)';\n } else {\n const from = $label.y;\n const value = this.plotter.toDomainY(from, domain);\n const to = this.plotter.toCurrentScreenY(value);\n\n $label.classList.add('hidden');\n $label.style['transform'] = `translateY(${to - from}px)`;\n }\n });\n }\n}\n\nexport default YAxis;\n","import { createElement, objectForEach, formatDate } from './utils';\n\nclass Legend {\n constructor({ x, graphs, plotter }) {\n this.x = x;\n this.graphs = graphs;\n this.plotter = plotter;\n }\n\n appendTo($container) {\n this.$container = $container;\n this.render();\n }\n\n render() {\n this.$element = createElement('div', { classes: 'legend' });\n this.$container.appendChild(this.$element);\n\n this.line = createElement('hr', { classes: 'legend-line' });\n\n this.$element.appendChild(this.line);\n\n this.circles = {};\n objectForEach(this.graphs, (key, { color }) => {\n this.circles[key] = createElement('div', {\n classes: 'legend-circle',\n style: { 'border-color': color },\n });\n\n this.$element.appendChild(this.circles[key]);\n });\n\n this.data = createElement('div', { classes: 'legend-data' });\n this.$element.appendChild(this.data);\n\n this.$element.addEventListener('mousemove', event => {\n this.onMouseMove(event);\n });\n this.$element.addEventListener('mouseleave', () => {\n this.onMouseLeave();\n });\n }\n\n onMouseMove(event) {\n const dateTime = this.plotter.toCurrentDomainX(event.offsetX);\n\n const position = this.x.reduce((prev, value, i, array) => {\n return Math.abs(value - dateTime) < Math.abs(array[prev] - dateTime)\n ? i\n : prev;\n }, 0);\n\n const x = this.x[position];\n const xCoord = this.plotter.toCurrentScreenX(x);\n\n this.line.style['left'] = `${xCoord}px`;\n this.line.style['opacity'] = 1;\n\n objectForEach(this.graphs, (key, { values, visible }) => {\n if (visible) {\n const yCoord = this.plotter.toCurrentScreenY(values[position]);\n this.circles[key].style['left'] = `${xCoord}px`;\n this.circles[key].style['top'] = `${yCoord}px`;\n this.circles[key].style['opacity'] = 1;\n } else {\n this.circles[key].style['opacity'] = 0;\n }\n });\n\n let text = formatDate(x);\n text += '