Charts.mjs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. /**
  2. * @author tlwr [toby@toby.codes]
  3. * @author Matt C [me@mitt.dev]
  4. * @copyright Crown Copyright 2019
  5. * @license Apache-2.0
  6. */
  7. import OperationError from "../errors/OperationError.mjs";
  8. import Utils from "../Utils.mjs";
  9. /**
  10. * @constant
  11. * @default
  12. */
  13. export const RECORD_DELIMITER_OPTIONS = ["Line feed", "CRLF"];
  14. /**
  15. * @constant
  16. * @default
  17. */
  18. export const FIELD_DELIMITER_OPTIONS = ["Space", "Comma", "Semi-colon", "Colon", "Tab"];
  19. /**
  20. * Default from colour
  21. *
  22. * @constant
  23. * @default
  24. */
  25. export const COLOURS = {
  26. min: "white",
  27. max: "black"
  28. };
  29. /**
  30. * Gets values from input for a plot.
  31. *
  32. * @param {string} input
  33. * @param {string} recordDelimiter
  34. * @param {string} fieldDelimiter
  35. * @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
  36. * @param {number} length
  37. * @returns {Object[]}
  38. */
  39. export function getValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded, length) {
  40. let headings;
  41. const values = [];
  42. input
  43. .split(recordDelimiter)
  44. .forEach((row, rowIndex) => {
  45. const split = row.split(fieldDelimiter);
  46. if (split.length !== length) throw new OperationError(`Each row must have length ${length}.`);
  47. if (columnHeadingsAreIncluded && rowIndex === 0) {
  48. headings = split;
  49. } else {
  50. values.push(split);
  51. }
  52. });
  53. return { headings, values };
  54. }
  55. /**
  56. * Gets values from input for a scatter plot.
  57. *
  58. * @param {string} input
  59. * @param {string} recordDelimiter
  60. * @param {string} fieldDelimiter
  61. * @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
  62. * @returns {Object[]}
  63. */
  64. export function getScatterValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
  65. let { headings, values } = getValues(
  66. input,
  67. recordDelimiter,
  68. fieldDelimiter,
  69. columnHeadingsAreIncluded,
  70. 2
  71. );
  72. if (headings) {
  73. headings = {x: headings[0], y: headings[1]};
  74. }
  75. values = values.map(row => {
  76. const x = parseFloat(row[0]),
  77. y = parseFloat(row[1]);
  78. if (Number.isNaN(x)) throw new OperationError("Values must be numbers in base 10.");
  79. if (Number.isNaN(y)) throw new OperationError("Values must be numbers in base 10.");
  80. return [x, y];
  81. });
  82. return { headings, values };
  83. }
  84. /**
  85. * Gets values from input for a scatter plot with colour from the third column.
  86. *
  87. * @param {string} input
  88. * @param {string} recordDelimiter
  89. * @param {string} fieldDelimiter
  90. * @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
  91. * @returns {Object[]}
  92. */
  93. export function getScatterValuesWithColour(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
  94. let { headings, values } = getValues(
  95. input,
  96. recordDelimiter, fieldDelimiter,
  97. columnHeadingsAreIncluded,
  98. 3
  99. );
  100. if (headings) {
  101. headings = {x: headings[0], y: headings[1]};
  102. }
  103. values = values.map(row => {
  104. const x = parseFloat(row[0]),
  105. y = parseFloat(row[1]),
  106. colour = row[2];
  107. if (Number.isNaN(x)) throw new OperationError("Values must be numbers in base 10.");
  108. if (Number.isNaN(y)) throw new OperationError("Values must be numbers in base 10.");
  109. return [x, y, Utils.escapeHtml(colour)];
  110. });
  111. return { headings, values };
  112. }
  113. /**
  114. * Gets values from input for a time series plot.
  115. *
  116. * @param {string} input
  117. * @param {string} recordDelimiter
  118. * @param {string} fieldDelimiter
  119. * @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
  120. * @returns {Object[]}
  121. */
  122. export function getSeriesValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
  123. const { values } = getValues(
  124. input,
  125. recordDelimiter, fieldDelimiter,
  126. false,
  127. 3
  128. );
  129. let xValues = new Set();
  130. const series = {};
  131. values.forEach(row => {
  132. const serie = row[0],
  133. xVal = row[1],
  134. val = parseFloat(row[2]);
  135. if (Number.isNaN(val)) throw new OperationError("Values must be numbers in base 10.");
  136. xValues.add(xVal);
  137. if (typeof series[serie] === "undefined") series[serie] = {};
  138. series[serie][xVal] = val;
  139. });
  140. xValues = new Array(...xValues);
  141. const seriesList = [];
  142. for (const seriesName in series) {
  143. const serie = series[seriesName];
  144. seriesList.push({name: seriesName, data: serie});
  145. }
  146. return { xValues, series: seriesList };
  147. }