/** * @author tlwr [toby@toby.codes] * @author Matt C [me@mitt.dev] * @copyright Crown Copyright 2019 * @license Apache-2.0 */ import OperationError from "../errors/OperationError.mjs"; /** * @constant * @default */ export const RECORD_DELIMITER_OPTIONS = ["Line feed", "CRLF"]; /** * @constant * @default */ export const FIELD_DELIMITER_OPTIONS = ["Space", "Comma", "Semi-colon", "Colon", "Tab"]; /** * Default from colour * * @constant * @default */ export const COLOURS = { min: "white", max: "black" }; /** * Gets values from input for a plot. * * @param {string} input * @param {string} recordDelimiter * @param {string} fieldDelimiter * @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record * @param {number} length * @returns {Object[]} */ export function getValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded, length) { let headings; const values = []; input .split(recordDelimiter) .forEach((row, rowIndex) => { const split = row.split(fieldDelimiter); if (split.length !== length) throw new OperationError(`Each row must have length ${length}.`); if (columnHeadingsAreIncluded && rowIndex === 0) { headings = split; } else { values.push(split); } }); return { headings, values }; } /** * Gets values from input for a scatter plot. * * @param {string} input * @param {string} recordDelimiter * @param {string} fieldDelimiter * @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record * @returns {Object[]} */ export function getScatterValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) { let { headings, values } = getValues( input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded, 2 ); if (headings) { headings = {x: headings[0], y: headings[1]}; } values = values.map(row => { const x = parseFloat(row[0]), y = parseFloat(row[1]); if (Number.isNaN(x)) throw new OperationError("Values must be numbers in base 10."); if (Number.isNaN(y)) throw new OperationError("Values must be numbers in base 10."); return [x, y]; }); return { headings, values }; } /** * Gets values from input for a scatter plot with colour from the third column. * * @param {string} input * @param {string} recordDelimiter * @param {string} fieldDelimiter * @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record * @returns {Object[]} */ export function getScatterValuesWithColour(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) { let { headings, values } = getValues( input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded, 3 ); if (headings) { headings = {x: headings[0], y: headings[1]}; } values = values.map(row => { const x = parseFloat(row[0]), y = parseFloat(row[1]), colour = row[2]; if (Number.isNaN(x)) throw new OperationError("Values must be numbers in base 10."); if (Number.isNaN(y)) throw new OperationError("Values must be numbers in base 10."); return [x, y, colour]; }); return { headings, values }; } /** * Gets values from input for a time series plot. * * @param {string} input * @param {string} recordDelimiter * @param {string} fieldDelimiter * @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record * @returns {Object[]} */ export function getSeriesValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) { const { values } = getValues( input, recordDelimiter, fieldDelimiter, false, 3 ); let xValues = new Set(); const series = {}; values.forEach(row => { const serie = row[0], xVal = row[1], val = parseFloat(row[2]); if (Number.isNaN(val)) throw new OperationError("Values must be numbers in base 10."); xValues.add(xVal); if (typeof series[serie] === "undefined") series[serie] = {}; series[serie][xVal] = val; }); xValues = new Array(...xValues); const seriesList = []; for (const seriesName in series) { const serie = series[seriesName]; seriesList.push({name: seriesName, data: serie}); } return { xValues, series: seriesList }; }