{"version":3,"file":"dataviz.js","sources":["../src/polyfills/array-from.js","../src/polyfills/array-find.js","../node_modules/fastdom/fastdom.js","../src/components/chart.js","../src/base/css-classes.js","../src/helpers/utils.js","../src/helpers/dates.js","../src/helpers/date-labels.js","../src/helpers/data-format.js","../src/helpers/nonconcurrent-raf.js","../src/helpers/overlay.js","../src/helpers/styles.js","../src/helpers/svg.js","../src/components/line-chart.js","../src/components/point-label-editor.js"],"sourcesContent":["// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from\nif (!Array.from) {\n Array.from = (function () {\n var toStr = Object.prototype.toString;\n var isCallable = function (fn) {\n return typeof fn === 'function' || toStr.call(fn) === '[object Function]';\n };\n var toInteger = function (value) {\n var number = Number(value);\n if (isNaN(number)) { return 0; }\n if (number === 0 || !isFinite(number)) { return number; }\n return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number));\n };\n var maxSafeInteger = Math.pow(2, 53) - 1;\n var toLength = function (value) {\n var len = toInteger(value);\n return Math.min(Math.max(len, 0), maxSafeInteger);\n };\n\n // The length property of the from method is 1.\n return function from(arrayLike/*, mapFn, thisArg */) {\n // 1. Let C be the this value.\n var C = this;\n\n // 2. Let items be ToObject(arrayLike).\n var items = Object(arrayLike);\n\n // 3. ReturnIfAbrupt(items).\n if (arrayLike == null) {\n throw new TypeError(\"Array.from requires an array-like object - not null or undefined\");\n }\n\n // 4. If mapfn is undefined, then let mapping be false.\n var mapFn = arguments.length > 1 ? arguments[1] : void undefined;\n var T;\n if (typeof mapFn !== 'undefined') {\n // 5. else\n // 5. a If IsCallable(mapfn) is false, throw a TypeError exception.\n if (!isCallable(mapFn)) {\n throw new TypeError('Array.from: when provided, the second argument must be a function');\n }\n\n // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined.\n if (arguments.length > 2) {\n T = arguments[2];\n }\n }\n\n // 10. Let lenValue be Get(items, \"length\").\n // 11. Let len be ToLength(lenValue).\n var len = toLength(items.length);\n\n // 13. If IsConstructor(C) is true, then\n // 13. a. Let A be the result of calling the [[Construct]] internal method of C with an argument list containing the single item len.\n // 14. a. Else, Let A be ArrayCreate(len).\n var A = isCallable(C) ? Object(new C(len)) : new Array(len);\n\n // 16. Let k be 0.\n var k = 0;\n // 17. Repeat, while k < len… (also steps a - h)\n var kValue;\n while (k < len) {\n kValue = items[k];\n if (mapFn) {\n A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k);\n } else {\n A[k] = kValue;\n }\n k += 1;\n }\n // 18. Let putStatus be Put(A, \"length\", len, true).\n A.length = len;\n // 20. Return A.\n return A;\n };\n }());\n}","// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find\nif (!Array.prototype.find) {\n Array.prototype.find = function(predicate) {\n 'use strict';\n if (this == null) {\n throw new TypeError('Array.prototype.find called on null or undefined');\n }\n if (typeof predicate !== 'function') {\n throw new TypeError('predicate must be a function');\n }\n var list = Object(this);\n var length = list.length >>> 0;\n var thisArg = arguments[1];\n var value;\n\n for (var i = 0; i < length; i++) {\n value = list[i];\n if (predicate.call(thisArg, value, i, list)) {\n return value;\n }\n }\n return undefined;\n };\n}\n","!(function(win) {\n\n/**\n * FastDom\n *\n * Eliminates layout thrashing\n * by batching DOM read/write\n * interactions.\n *\n * @author Wilson Page \n * @author Kornel Lesinski \n */\n\n'use strict';\n\n/**\n * Mini logger\n *\n * @return {Function}\n */\nvar debug = 0 ? console.log.bind(console, '[fastdom]') : function() {};\n\n/**\n * Normalized rAF\n *\n * @type {Function}\n */\nvar raf = win.requestAnimationFrame\n || win.webkitRequestAnimationFrame\n || win.mozRequestAnimationFrame\n || win.msRequestAnimationFrame\n || function(cb) { return setTimeout(cb, 16); };\n\n/**\n * Initialize a `FastDom`.\n *\n * @constructor\n */\nfunction FastDom() {\n var self = this;\n self.reads = [];\n self.writes = [];\n self.raf = raf.bind(win); // test hook\n debug('initialized', self);\n}\n\nFastDom.prototype = {\n constructor: FastDom,\n\n /**\n * Adds a job to the read batch and\n * schedules a new frame if need be.\n *\n * @param {Function} fn\n * @param {Object} ctx the context to be bound to `fn` (optional).\n * @public\n */\n measure: function(fn, ctx) {\n debug('measure');\n var task = !ctx ? fn : fn.bind(ctx);\n this.reads.push(task);\n scheduleFlush(this);\n return task;\n },\n\n /**\n * Adds a job to the\n * write batch and schedules\n * a new frame if need be.\n *\n * @param {Function} fn\n * @param {Object} ctx the context to be bound to `fn` (optional).\n * @public\n */\n mutate: function(fn, ctx) {\n debug('mutate');\n var task = !ctx ? fn : fn.bind(ctx);\n this.writes.push(task);\n scheduleFlush(this);\n return task;\n },\n\n /**\n * Clears a scheduled 'read' or 'write' task.\n *\n * @param {Object} task\n * @return {Boolean} success\n * @public\n */\n clear: function(task) {\n debug('clear', task);\n return remove(this.reads, task) || remove(this.writes, task);\n },\n\n /**\n * Extend this FastDom with some\n * custom functionality.\n *\n * Because fastdom must *always* be a\n * singleton, we're actually extending\n * the fastdom instance. This means tasks\n * scheduled by an extension still enter\n * fastdom's global task queue.\n *\n * The 'super' instance can be accessed\n * from `this.fastdom`.\n *\n * @example\n *\n * var myFastdom = fastdom.extend({\n * initialize: function() {\n * // runs on creation\n * },\n *\n * // override a method\n * measure: function(fn) {\n * // do extra stuff ...\n *\n * // then call the original\n * return this.fastdom.measure(fn);\n * },\n *\n * ...\n * });\n *\n * @param {Object} props properties to mixin\n * @return {FastDom}\n */\n extend: function(props) {\n debug('extend', props);\n if (typeof props != 'object') throw new Error('expected object');\n\n var child = Object.create(this);\n mixin(child, props);\n child.fastdom = this;\n\n // run optional creation hook\n if (child.initialize) child.initialize();\n\n return child;\n },\n\n // override this with a function\n // to prevent Errors in console\n // when tasks throw\n catch: null\n};\n\n/**\n * Schedules a new read/write\n * batch if one isn't pending.\n *\n * @private\n */\nfunction scheduleFlush(fastdom) {\n if (!fastdom.scheduled) {\n fastdom.scheduled = true;\n fastdom.raf(flush.bind(null, fastdom));\n debug('flush scheduled');\n }\n}\n\n/**\n * Runs queued `read` and `write` tasks.\n *\n * Errors are caught and thrown by default.\n * If a `.catch` function has been defined\n * it is called instead.\n *\n * @private\n */\nfunction flush(fastdom) {\n debug('flush');\n\n var writes = fastdom.writes;\n var reads = fastdom.reads;\n var error;\n\n try {\n debug('flushing reads', reads.length);\n runTasks(reads);\n debug('flushing writes', writes.length);\n runTasks(writes);\n } catch (e) { error = e; }\n\n fastdom.scheduled = false;\n\n // If the batch errored we may still have tasks queued\n if (reads.length || writes.length) scheduleFlush(fastdom);\n\n if (error) {\n debug('task errored', error.message);\n if (fastdom.catch) fastdom.catch(error);\n else throw error;\n }\n}\n\n/**\n * We run this inside a try catch\n * so that if any jobs error, we\n * are able to recover and continue\n * to flush the batch until it's empty.\n *\n * @private\n */\nfunction runTasks(tasks) {\n debug('run tasks');\n var task; while (task = tasks.shift()) task();\n}\n\n/**\n * Remove an item from an Array.\n *\n * @param {Array} array\n * @param {*} item\n * @return {Boolean}\n */\nfunction remove(array, item) {\n var index = array.indexOf(item);\n return !!~index && !!array.splice(index, 1);\n}\n\n/**\n * Mixin own properties of source\n * object into the target.\n *\n * @param {Object} target\n * @param {Object} source\n */\nfunction mixin(target, source) {\n for (var key in source) {\n if (source.hasOwnProperty(key)) target[key] = source[key];\n }\n}\n\n// There should never be more than\n// one instance of `FastDom` in an app\nvar exports = win.fastdom = (win.fastdom || new FastDom()); // jshint ignore:line\n\n// Expose to CJS & AMD\nif ((typeof define) == 'function') define(function() { return exports; });\nelse if ((typeof module) == 'object') module.exports = exports;\n\n})( typeof window !== 'undefined' ? window : this);\n","// Namespace for custom events (e.g. dv.init)\nconst EVENT_NS = 'dv';\n// Error codes\nconst ERROR_GENERIC = 0;\nconst ERROR_INVALID_DATA = 1;\nconst ERROR_INCOMPLETE_DATA = 2;\n\nclass Chart {\n /** Attaches an event listener\n * @param {String} type - The event type to listen to (e.g. click)\n * @param {Function} listener - The handler function\n * @returns {Chart} - The Chart instance\n */\n on(type, listener) {\n this.el.addEventListener(type, listener);\n return this;\n }\n\n /** Detaches an event listener\n * @param {String} type - The event type to listen to (e.g. click)\n * @param {Function} listener - The handler function\n * @returns {Chart} - The Chart instance\n */\n off(type, listener) {\n this.el.removeEventListener(type, listener);\n return this;\n }\n\n /** Emits an event\n * @param {String} type - The event type to emit (e.g. click)\n * @param {Object} data - Data to add to the Event object\n * @returns {Chart} - The Chart instance\n */\n emit(type, data) {\n const e = document.createEvent('Event');\n e.dvData = data;\n e.initEvent(`${EVENT_NS}.${type}`, true, true);\n this.el.dispatchEvent(e);\n return this;\n }\n\n /** Emits a warning event\n * @param {String} message - The warning message\n * @param {Number} code - The error code\n * @param {Object} extras - Data to attach to the Event object\n */\n warn(message, code = ERROR_GENERIC, extras = null) {\n return this.emit('warn', { code, message, extras });\n }\n\n /** Emits an error event\n * @param {String} message - The error message\n * @param {Number} code - The error code\n * @param {Object} extras - Data to attach to the Event object\n */\n error(message, code = ERROR_GENERIC, extras = null) {\n return this.emit('error', { code, message, extras });\n }\n}\n\n// Expose some useful constants on Chart class\nObject.assign(Chart, {\n ERROR_GENERIC,\n ERROR_INVALID_DATA,\n ERROR_INCOMPLETE_DATA,\n});\n\nexport default Chart;\n","// CSS class names -- prefixed with 'dv-' to avoid collsions with external CSS\n\nexport default {\n // Chart types\n line_chart: 'dv-line-chart',\n\n // Chart states\n chart_zoomed: 'dv-zoomed',\n chart_panning: 'dv-panning',\n chart_viewport_hover: 'dv-viewport-hover',\n\n // Header\n header: 'dv-header',\n title: 'dv-title',\n subtitle: 'dv-subtitle',\n\n // Legend\n legend: 'dv-legend',\n legend_item: 'dv-legend-item',\n\n // Plot area\n plot_area: 'dv-plot-area',\n plot_bg: 'dv-plot-background',\n viewport: 'dv-viewport',\n\n // Lines\n lines_group: 'dv-lines-group',\n line: 'dv-line',\n point: 'dv-point',\n point_label: 'dv-point-label',\n point_label_line: 'dv-point-label-line',\n annotation: 'dv-annotation',\n annotation_bg: 'dv-annotation-bg',\n\n // Axes\n x_axis: 'dv-x-axis',\n y_axis: 'dv-y-axis',\n axis_line: 'dv-axis-line',\n axis_at_zero: 'dv-axis-at-zero',\n\n // Grid/ticks\n tick: 'dv-tick',\n grid_line: 'dv-grid-line',\n x_axis_grid_lines: 'dv-grid-lines-v',\n y_axis_grid_lines: 'dv-grid-lines-h',\n\n // Footer\n footer: 'dv-footer',\n footnote: 'dv-footnote',\n source: 'dv-source',\n\n // Tooltips\n tooltip: 'dv-tooltip',\n tooltip_left: 'dv-tooltip-left',\n tooltip_right: 'dv-tooltip-right',\n group_tooltip: 'dv-group-tooltip',\n tooltip_label: 'dv-tooltip-label',\n tooltip_value: 'dv-tooltip-value',\n tooltip_line_label: 'dv-tooltip-line-label',\n tooltip_hidden: 'dv-hidden',\n tooltip_point: 'dv-tooltip-point',\n\n // Label editor\n label_editor: 'dv-label-editor',\n label_editor_closing: 'dv-editor-closing',\n selected_point: 'dv-selected-point',\n label_handle: 'dv-label-handle',\n\n // Other\n placeholder: 'dv-placeholder',\n no_select: 'dv-no-select',\n};\n","/*\n** Text\n*/\n\n// Using canvas instead of SVG for speed reasons\nconst textMeasureContext = document.createElement('canvas').getContext('2d');\n// Hash table of measured strings and their width\nlet textMeasureDictionary = {};\n\nexport function measureText(text, font) {\n const key = `${text}|${font}`;\n // Check cache first\n if (textMeasureDictionary[key]) return textMeasureDictionary[key];\n\n textMeasureContext.font = font;\n // Ceil value to normalize differences in browser implementation\n const width = Math.ceil(textMeasureContext.measureText(text).width);\n\n // Cache width for repeat use\n textMeasureDictionary[key] = width;\n\n return width;\n}\n\nexport function measureWidestText(strings, font) {\n let longest = 0;\n let length;\n\n for (let i = 0; i < strings.length; i++) {\n length = measureText(strings[i], font);\n if (length > longest) {\n longest = length;\n }\n }\n return longest;\n}\n\nexport function clearTextMeasureDictionary() {\n textMeasureDictionary = {};\n}\n\nexport function wrapText(text, containerWidth, font, letterSpacing = 0) {\n const adjustedWidth = containerWidth - (containerWidth * letterSpacing);\n const lines = [];\n\n text.split('\\n').forEach((p) => {\n let line;\n\n p.trim().split(' ').forEach((word, index) => {\n if (index === 0) {\n line = word;\n } else if (measureText(`${line} ${word}`, font) < adjustedWidth) {\n line += ` ${word}`;\n } else {\n lines.push(line);\n line = word;\n }\n });\n\n lines.push(line);\n });\n\n return lines.join('\\n');\n}\n\n\n/*\n** Math\n*/\n\nexport function floorToMultiple(value, multiplier) {\n if (multiplier === 0) {\n return 0;\n }\n return Math.floor(value / multiplier) * multiplier;\n}\n\nexport function ceilToMultiple(value, multiplier) {\n if (multiplier === 0) {\n return 0;\n }\n return Math.ceil(value / multiplier) * multiplier;\n}\n\nexport function orderOfMagnitude(value) {\n return Math.pow(10, Math.floor(Math.log(Math.abs(value)) / Math.LN10));\n}\n\nexport function decimalPlaces(number) {\n const match = (`${number}`).match(/(?:\\.(\\d+))?(?:[eE]([+-]?\\d+))?$/);\n if (!match) return 0;\n\n return Math.max(\n 0,\n // Number of digits right of decimal point\n (match[1] ? match[1].length : 0)\n // Adjust for scientific notation\n - (match[2] ? +match[2] : 0),\n );\n}\n\nexport function numDigits(number) {\n return Math.max(Math.floor(Math.log(Math.abs(number)) * Math.LOG10E), 0) + 1;\n}\n\nexport function roundTo(number, precision) {\n const mult = Math.pow(10, precision);\n return Math.round(number * mult) / mult;\n}\n\nexport function roundPixels(value) {\n return roundTo(value, 2);\n}\n\nexport function readableNumber(number, decimals = null, useUnits = false) {\n const digits = numDigits(number);\n let num = number;\n let precision = decimals;\n let unit = '';\n\n if (useUnits) {\n if (digits > 12) {\n num /= Math.pow(10, 12);\n unit = 'T';\n } else if (digits > 9) {\n num /= Math.pow(10, 9);\n unit = 'B';\n } else if (digits > 6) {\n num /= Math.pow(10, 6);\n unit = 'M';\n }\n\n precision = decimalPlaces(num);\n }\n\n const stringNumber = precision !== null ? num.toFixed(precision) : num.toString();\n const parts = stringNumber.split('.');\n parts[0] = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',');\n\n return `${parts.join('.')}${unit}`;\n}\n\n\n/*\n** Positioning\n*/\n\n// Emulate event.offsetX and event.offsetY which are inconsistently implemented\nexport function mouseOffset(event, target = event.target) {\n const rect = target.getBoundingClientRect();\n return { x: event.clientX - rect.left, y: event.clientY - rect.top };\n}\n\nexport function inBounds(x, y, left, top, right, bottom) {\n return x >= left && x <= right && y >= top && y <= bottom;\n}\n\nexport function distanceBetweenPoints(x1, y1, x2, y2) {\n return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));\n}\n\nexport function distanceToElement(x, y, left, top, width, height) {\n const right = left + width;\n const bottom = top + height;\n\n let closestX;\n if (left <= x && x <= right) {\n closestX = x;\n } else {\n closestX = Math.abs(left - x) < Math.abs(right - x) ? left : right;\n }\n\n let closestY;\n if (top <= y && y <= bottom) {\n closestY = y;\n } else {\n closestY = Math.abs(top - y) < Math.abs(bottom - y) ? top : bottom;\n }\n\n return distanceBetweenPoints(x, y, closestX, closestY);\n}\n\nexport function getAnchorPosition(x, y, left, top, width, height) {\n const right = left + width;\n const bottom = top + height;\n\n let anchorX;\n if (left <= x && x <= right) {\n anchorX = Math.round(left + (width / 2));\n } else {\n anchorX = Math.abs(left - x) < Math.abs(right - x) ? left : right;\n }\n\n let anchorY;\n if (top <= y && y <= bottom) {\n anchorY = Math.round(top + (height / 2));\n } else {\n anchorY = Math.abs(top - y) < Math.abs(bottom - y) ? top : bottom;\n }\n\n return { x: Math.round(anchorX), y: Math.round(anchorY) };\n}\n\nexport function rectsOverlap(rectA, rectB, minSpacing = 0) {\n return !(\n rectA.right + minSpacing < rectB.left ||\n rectA.left > rectB.right + minSpacing ||\n rectA.bottom + minSpacing < rectB.top ||\n rectA.top > rectB.bottom + minSpacing\n );\n}\n\nexport function elementsOverlap(elA, elB, minSpacing = 0) {\n return rectsOverlap(elA.getBoundingClientRect(), elB.getBoundingClientRect(), minSpacing);\n}\n\n\n/*\n** Other\n*/\n\nexport function supportsCssProperty(prop) {\n const el = document.createElement('div');\n if (prop in el.style) return true;\n\n const vendors = ['Khtml', 'Ms', 'O', 'Moz', 'Webkit'];\n const propCapitalized = prop.replace(/^[a-z]/, firstLetter => firstLetter.toUpperCase());\n\n for (let i = 0; i < vendors.length; i++) {\n if (vendors[i] + propCapitalized in el.style) {\n return true;\n }\n }\n\n return false;\n}\n\nexport function truncateOrDefault(value, maxChars, defaultValue = null) {\n if (typeof value !== 'undefined' && value !== null) {\n return maxChars > 0 ? value.toString().slice(0, maxChars) : value.toString();\n }\n return defaultValue;\n}\n\nexport function isFirstOccurrence(value, index, array) {\n return array.indexOf(value) === index;\n}\n\nexport function compareX(a, b) {\n return a.x - b.x;\n}\n","export const MONTH_NAMES = [\n 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',\n 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',\n];\nconst ONE_DAY_IN_MS = 86.4e6; // 1000 * 60 * 60 * 24\nconst EPOCH = Date.UTC(1970, 0, 1);\n\nexport function getQuarterFromMonth(month) {\n return Math.ceil((month + 1) / 3);\n}\n\nexport function standardFormat(date) {\n return `${date.getFullYear()} ${MONTH_NAMES[date.getMonth()]} ${date.getDate()}`;\n}\n\nexport function epochDays(date) {\n const dateUtc = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());\n return Math.floor(Math.abs(EPOCH - dateUtc) / ONE_DAY_IN_MS) * (date < EPOCH ? -1 : 1);\n}\n\nexport function dateFromEpochDays(days) {\n const date = new Date(EPOCH);\n date.setUTCDate(date.getUTCDate() + days);\n return new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());\n}\n","import * as datesHelper from './dates';\n\nexport const STD_MONTH_FORMAT = /^\\d{2,4} (jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)$/i;\nexport const STD_DATE_FORMAT = /^\\d{2,4} (jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec) \\d{1,2}$/i;\nexport const STD_DATE_RANGE_FORMAT = /^\\d{2,4} (jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec) \\d{1,2}-\\d{2,4} (jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec) \\d{1,2}$/i;\n\n\nfunction getDateLabels(begin, end, formatter, next) {\n const dateLabels = [];\n const date = new Date(begin);\n while (date <= end) {\n dateLabels.push({\n x: datesHelper.epochDays(date),\n text: formatter(date),\n });\n next(date);\n }\n return dateLabels;\n}\n\nfunction formatYear(date, twoDigit = false) {\n const year = date.getFullYear().toString();\n return twoDigit ? `'${year.substr(2)}` : year;\n}\n\nfunction resolveAutoFormat(first, last) {\n if (last.getFullYear() - first.getFullYear() >= 4) {\n return 'year';\n } else if (first.getFullYear() === last.getFullYear() && last.getMonth() - first.getMonth() < 4) {\n return 'day';\n }\n return 'month';\n}\n\nfunction getFormatter(dateFormat, first, last) {\n const shortYear = dateFormat.indexOf('_yy') > -1;\n switch (dateFormat.replace('_yy', '')) {\n case 'auto':\n return getFormatter(`${resolveAutoFormat(first, last)}${shortYear ? '_yy' : ''}`, first, last);\n\n case 'year':\n return d => formatYear(d, shortYear);\n\n case 'quarter':\n return d => `Q${datesHelper.getQuarterFromMonth(d.getMonth())} ${formatYear(d, shortYear)}`;\n\n case 'month':\n if (last.getFullYear() - first.getFullYear() > 0) {\n return d => `${datesHelper.MONTH_NAMES[d.getMonth()]} ${formatYear(d, shortYear)}`;\n }\n return d => datesHelper.MONTH_NAMES[d.getMonth()];\n\n case 'day':\n default:\n if (last.getFullYear() - first.getFullYear() > 0) {\n return d => `${formatYear(d, shortYear)} ${datesHelper.MONTH_NAMES[d.getMonth()]} ${d.getDate()}`;\n }\n return d => `${datesHelper.MONTH_NAMES[d.getMonth()]} ${d.getDate()}`;\n }\n}\n\nexport function parse(label) {\n const labelText = label.toString().trim();\n\n if (STD_DATE_FORMAT.test(labelText)) {\n return new Date(labelText);\n } else if (STD_MONTH_FORMAT.test(labelText)) {\n return new Date(`${labelText} 1`);\n } else if (STD_DATE_RANGE_FORMAT.test(labelText)) {\n return new Date(labelText.substr(labelText.indexOf('-') + 1));\n }\n\n return null;\n}\n\nexport function format(date, dateFormat, first, last) {\n const shortYear = dateFormat.indexOf('_yy') > -1;\n let tickDate;\n\n switch (dateFormat.replace('_yy', '')) {\n case 'auto':\n return format(date, `${resolveAutoFormat(first, last)}${shortYear ? '_yy' : ''}`, first, last);\n\n case 'year':\n tickDate = new Date(date.getFullYear(), 0, 1);\n break;\n\n case 'quarter':\n tickDate = new Date(\n date.getFullYear(),\n (datesHelper.getQuarterFromMonth(date.getMonth()) - 1) * 3,\n 1,\n );\n break;\n\n case 'month':\n tickDate = new Date(date.getFullYear(), date.getMonth(), 1);\n break;\n\n case 'day':\n default:\n tickDate = new Date(date);\n }\n\n return {\n x: datesHelper.epochDays(tickDate),\n text: getFormatter(dateFormat, first, last)(tickDate),\n };\n}\n\nexport function formatAll(labels, dateFormat) {\n const shortYear = dateFormat.indexOf('_yy') > -1;\n const first = parse(labels[0].text);\n const last = parse(labels[labels.length - 1].text);\n let begin;\n let end;\n let next;\n\n switch (dateFormat.replace('_yy', '')) {\n case 'auto':\n return formatAll(labels, `${resolveAutoFormat(first, last)}${shortYear ? '_yy' : ''}`);\n\n case 'year':\n next = d => d.setFullYear(d.getFullYear() + 1);\n end = new Date(last.getFullYear(), 0, 1);\n\n if (first.getFullYear() < last.getFullYear()) {\n begin = first.getMonth() === 0 && first.getDate() === 1 ? first : new Date(first.getFullYear() + 1, 0, 1);\n } else {\n begin = new Date(last.getFullYear(), 0, 1);\n }\n break;\n\n case 'quarter':\n next = d => d.setMonth(d.getMonth() + 3);\n begin = (first.getMonth() + 1) % 4 === 0 && first.getDate() === 1\n ? first\n : new Date(first.getFullYear(), first.getMonth() + (3 - (first.getMonth() % 3)), 1);\n\n end = (last.getMonth() + 1) % 4 === 0 && last.getDate() === 1\n ? last\n : new Date(last.getFullYear(), last.getMonth() + (3 - (last.getMonth() % 3)), 1);\n break;\n\n case 'month':\n begin = first.getDate() === 1 ? first : new Date(first.getFullYear(), first.getMonth() + 1, 1);\n end = last.getDate() === 1 ? last : new Date(last.getFullYear(), last.getMonth(), 1);\n next = d => d.setMonth(d.getMonth() + 1);\n break;\n\n case 'day':\n default:\n begin = first;\n end = last;\n next = d => d.setDate(d.getDate() + 1);\n }\n\n if (end < begin) {\n end = begin;\n }\n\n return getDateLabels(begin, end, getFormatter(dateFormat, first, last), next);\n}\n","import Chart from '../components/chart';\nimport { truncateOrDefault, isFirstOccurrence, compareX } from './utils';\nimport * as dateLabels from './date-labels';\nimport * as datesHelper from './dates';\n\nconst COLOR_PALETTES = {\n gallup: ['#4d9c2d', '#012168', '#00827d', '#b24b58'],\n polcategorical: ['#4d9c2d', '#00827d', '#b24b58', '#692044'],\n democratic: ['#012168', '#3e8ddd', '#0055b7', '#002a48'],\n republican: ['#f42434', '#ab162b', '#79232e', '#51252f'],\n independent: ['#8a8b8c', '#404040', '#666666', '#2b2b2b'],\n disapproval: ['#51252f', '#b24b58', '#00827d', '#4d9c2d'],\n approvalFlexchart: ['#4d9c2d', '#692044'],\n};\n\nconst DASH_STYLES = {\n gallup: [null, null, '5 5', '2 2'],\n polcategorical: [null, '5 5', null, '2 2'],\n democratic: [null, null, '5 5', '2 2'],\n republican: [null, '5 5', '2 2', null],\n independent: [null, '5 5', '2 2', null],\n};\nconst DEFAULT_SOURCE = 'Gallup';\nconst TITLE_CHARS = 150;\nconst SOURCE_CHARS = 40;\nconst LEGEND_ITEM_CHARS = 50;\n\nexport function validateData(data) {\n const warnings = [];\n const errors = [];\n let valid = true;\n\n // Test: Data contains lines\n if (!data.lines || !data.lines.length) {\n warnings.push(['Data contains no lines', Chart.ERROR_INCOMPLETE_DATA]);\n valid = false;\n } else {\n // Test: Lines are valid\n data.lines.forEach((line, lineIndex) => {\n const lineDesc = `Line #${lineIndex + 1} (${line.title || 'untitled'})`;\n // Test: Lines have points\n if (!line.points || !line.points.length) {\n warnings.push([\n `${lineDesc} contains no points`,\n Chart.ERROR_INCOMPLETE_DATA,\n { line },\n ]);\n valid = false;\n }\n\n // Test: Lines have more than 1 point\n if (line.points.length === 1) {\n warnings.push([\n `${lineDesc} only contains 1 point (need at least 2)`,\n Chart.ERROR_INCOMPLETE_DATA,\n { line },\n ]);\n valid = false;\n }\n\n // Test: Points are valid\n line.points.forEach((point, pointIndex) => {\n // Check if point is a number\n if (!isNaN(point)) {\n // do nothing\n } else if ('x' in point) {\n if (isNaN(point.x)) {\n errors.push([\n `The X-coordinate of point #${\n pointIndex + 1\n } on ${lineDesc} is not a valid number: ${point.x}`,\n Chart.ERROR_INVALID_DATA,\n { point },\n ]);\n valid = false;\n }\n\n if ('y' in point) {\n if (isNaN(point.y)) {\n errors.push([\n `The Y-coordinate of point #${\n pointIndex + 1\n } on ${lineDesc} is not a valid number: ${point.y}`,\n Chart.ERROR_INVALID_DATA,\n { point },\n ]);\n valid = false;\n }\n } else if ('date' in point) {\n if (!(point.date instanceof Date) || isNaN(point.date)) {\n errors.push([\n `The date value of point #${\n pointIndex + 1\n } on ${lineDesc} is not a valid date: ${point.date}`,\n Chart.ERROR_INVALID_DATA,\n { point },\n ]);\n valid = false;\n }\n }\n } else {\n errors.push([\n `Point #${\n pointIndex + 1\n } on ${lineDesc} is in an invalid format: ${point}`,\n Chart.ERROR_INVALID_DATA,\n { point },\n ]);\n valid = false;\n }\n });\n });\n }\n\n // Test: Data contains labels\n if (!data.labels || !data.labels.length) {\n warnings.push(['Data contains no labels', Chart.ERROR_INCOMPLETE_DATA]);\n valid = false;\n } else {\n // Test: Labels are valid\n data.labels.forEach((label, labelIndex) => {\n if (typeof label !== 'string' && !label.text) {\n errors.push([\n `Label #${labelIndex + 1} is in an invalid format: ${label}`,\n Chart.ERROR_INVALID_DATA,\n ]);\n valid = false;\n }\n });\n\n // Test: Labels are consistently formatted\n const formatTests = [\n // number\n (label) => !isNaN(label),\n // standard date format\n (label) => dateLabels.STD_DATE_FORMAT.test(label),\n // standard date range format\n (label) => dateLabels.STD_DATE_RANGE_FORMAT.test(label),\n // standard month format\n (label) => dateLabels.STD_MONTH_FORMAT.test(label),\n ];\n\n const firstLabel = data.labels[0].text || data.labels[0];\n let test;\n\n for (let i = 0; i < formatTests.length; i++) {\n if (formatTests[i](firstLabel)) {\n test = formatTests[i];\n break;\n }\n }\n\n if (test && !data.labels.every(test)) {\n errors.push([\n 'Labels are not formatted consistently',\n Chart.ERROR_INVALID_DATA,\n { labels: data.labels },\n ]);\n valid = false;\n }\n }\n\n return { valid, warnings, errors };\n}\n\nexport function normalizeData(data, chartCodeName) {\n const normalized = {\n title: truncateOrDefault(data.title, TITLE_CHARS),\n subtitle: truncateOrDefault(data.subtitle, -1, null),\n footnote: truncateOrDefault(data.footnote, -1, null),\n source: truncateOrDefault(data.source, SOURCE_CHARS, DEFAULT_SOURCE),\n labels: [],\n lines: [],\n };\n const lines = (data.lines || []).map((line) =>\n Object.assign({ points: [] }, line)\n );\n let labels = data.labels || [];\n let xValues;\n\n // Check for { date: , y: } points format\n // If points are in this format, labels do not need to be provided\n if (lines.length && lines[0].points[0] && lines[0].points[0].date) {\n const dates = lines\n .map((line) => line.points)\n .reduce((a, b) => a.concat(b), [])\n .map((point) => point.date)\n .sort((a, b) => a - b);\n\n // Labels are still relied upon at this point, so we need to generate them\n labels = dates.map(datesHelper.standardFormat).filter(isFirstOccurrence);\n xValues = dates.map(datesHelper.epochDays).filter(isFirstOccurrence);\n normalized.labelType = 'date';\n\n // Check if labels are either strings in standard date format or numerical\n } else if (labels.length && typeof labels[0] !== 'object') {\n // Check for standard date format or numbers\n if (dateLabels.STD_DATE_FORMAT.test(labels[0].toString().trim())) {\n xValues = labels.map((label) => datesHelper.epochDays(new Date(label)));\n normalized.labelType = 'date';\n } else if (dateLabels.STD_MONTH_FORMAT.test(labels[0].toString().trim())) {\n xValues = labels.map((label) =>\n datesHelper.epochDays(new Date(`${label} 1`))\n );\n normalized.labelType = 'date';\n } else if (\n dateLabels.STD_DATE_RANGE_FORMAT.test(labels[0].toString().trim())\n ) {\n xValues = labels.map((label) =>\n datesHelper.epochDays(new Date(label.substr(label.indexOf('-') + 1)))\n );\n normalized.labelType = 'date_range';\n } else if (!isNaN(labels[0])) {\n xValues = labels.map((label) => Number(label));\n normalized.labelType = 'number';\n }\n } else {\n normalized.labelType = 'string';\n }\n\n // Normalize label format to { x: , text: