SeriesChart.mjs 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  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 * as d3temp from "d3";
  8. import * as nodomtemp from "nodom";
  9. import { getSeriesValues, RECORD_DELIMITER_OPTIONS, FIELD_DELIMITER_OPTIONS } from "../lib/Charts";
  10. import Operation from "../Operation";
  11. import Utils from "../Utils";
  12. const d3 = d3temp.default ? d3temp.default : d3temp;
  13. const nodom = nodomtemp.default ? nodomtemp.default: nodomtemp;
  14. /**
  15. * Series chart operation
  16. */
  17. class SeriesChart extends Operation {
  18. /**
  19. * SeriesChart constructor
  20. */
  21. constructor() {
  22. super();
  23. this.name = "Series chart";
  24. this.module = "Charts";
  25. this.description = "A time series graph is a line graph of repeated measurements taken over regular time intervals.";
  26. this.inputType = "string";
  27. this.outputType = "html";
  28. this.args = [
  29. {
  30. name: "Record delimiter",
  31. type: "option",
  32. value: RECORD_DELIMITER_OPTIONS,
  33. },
  34. {
  35. name: "Field delimiter",
  36. type: "option",
  37. value: FIELD_DELIMITER_OPTIONS,
  38. },
  39. {
  40. name: "X label",
  41. type: "string",
  42. value: "",
  43. },
  44. {
  45. name: "Point radius",
  46. type: "number",
  47. value: 1,
  48. },
  49. {
  50. name: "Series colours",
  51. type: "string",
  52. value: "mediumseagreen, dodgerblue, tomato",
  53. },
  54. ];
  55. }
  56. /**
  57. * Series chart operation.
  58. *
  59. * @param {string} input
  60. * @param {Object[]} args
  61. * @returns {html}
  62. */
  63. run(input, args) {
  64. const recordDelimiter = Utils.charRep(args[0]),
  65. fieldDelimiter = Utils.charRep(args[1]),
  66. xLabel = args[2],
  67. pipRadius = args[3],
  68. seriesColours = args[4].split(","),
  69. svgWidth = 500,
  70. interSeriesPadding = 20,
  71. xAxisHeight = 50,
  72. seriesLabelWidth = 50,
  73. seriesHeight = 100,
  74. seriesWidth = svgWidth - seriesLabelWidth - interSeriesPadding;
  75. const { xValues, series } = getSeriesValues(input, recordDelimiter, fieldDelimiter),
  76. allSeriesHeight = Object.keys(series).length * (interSeriesPadding + seriesHeight),
  77. svgHeight = allSeriesHeight + xAxisHeight + interSeriesPadding;
  78. const document = new nodom.Document();
  79. let svg = document.createElement("svg");
  80. svg = d3.select(svg)
  81. .attr("width", "100%")
  82. .attr("height", "100%")
  83. .attr("viewBox", `0 0 ${svgWidth} ${svgHeight}`);
  84. const xAxis = d3.scalePoint()
  85. .domain(xValues)
  86. .range([0, seriesWidth]);
  87. svg.append("g")
  88. .attr("class", "axis axis--x")
  89. .attr("transform", `translate(${seriesLabelWidth}, ${xAxisHeight})`)
  90. .call(
  91. d3.axisTop(xAxis).tickValues(xValues.filter((x, i) => {
  92. return [0, Math.round(xValues.length / 2), xValues.length -1].indexOf(i) >= 0;
  93. }))
  94. );
  95. svg.append("text")
  96. .attr("x", svgWidth / 2)
  97. .attr("y", xAxisHeight / 2)
  98. .style("text-anchor", "middle")
  99. .text(xLabel);
  100. const tooltipText = {},
  101. tooltipAreaWidth = seriesWidth / xValues.length;
  102. xValues.forEach(x => {
  103. const tooltip = [];
  104. series.forEach(serie => {
  105. const y = serie.data[x];
  106. if (typeof y === "undefined") return;
  107. tooltip.push(`${serie.name}: ${y}`);
  108. });
  109. tooltipText[x] = tooltip.join("\n");
  110. });
  111. const chartArea = svg.append("g")
  112. .attr("transform", `translate(${seriesLabelWidth}, ${xAxisHeight})`);
  113. chartArea
  114. .append("g")
  115. .selectAll("rect")
  116. .data(xValues)
  117. .enter()
  118. .append("rect")
  119. .attr("x", x => {
  120. return xAxis(x) - (tooltipAreaWidth / 2);
  121. })
  122. .attr("y", 0)
  123. .attr("width", tooltipAreaWidth)
  124. .attr("height", allSeriesHeight)
  125. .attr("stroke", "none")
  126. .attr("fill", "transparent")
  127. .append("title")
  128. .text(x => {
  129. return `${x}\n
  130. --\n
  131. ${tooltipText[x]}\n
  132. `.replace(/\s{2,}/g, "\n");
  133. });
  134. const yAxesArea = svg.append("g")
  135. .attr("transform", `translate(0, ${xAxisHeight})`);
  136. series.forEach((serie, seriesIndex) => {
  137. const yExtent = d3.extent(Object.values(serie.data)),
  138. yAxis = d3.scaleLinear()
  139. .domain(yExtent)
  140. .range([seriesHeight, 0]);
  141. const seriesGroup = chartArea
  142. .append("g")
  143. .attr("transform", `translate(0, ${seriesHeight * seriesIndex + interSeriesPadding * (seriesIndex + 1)})`);
  144. let path = "";
  145. xValues.forEach((x, xIndex) => {
  146. let nextX = xValues[xIndex + 1],
  147. y = serie.data[x],
  148. nextY= serie.data[nextX];
  149. if (typeof y === "undefined" || typeof nextY === "undefined") return;
  150. x = xAxis(x); nextX = xAxis(nextX);
  151. y = yAxis(y); nextY = yAxis(nextY);
  152. path += `M ${x} ${y} L ${nextX} ${nextY} z `;
  153. });
  154. seriesGroup
  155. .append("path")
  156. .attr("d", path)
  157. .attr("fill", "none")
  158. .attr("stroke", seriesColours[seriesIndex % seriesColours.length])
  159. .attr("stroke-width", "1");
  160. xValues.forEach(x => {
  161. const y = serie.data[x];
  162. if (typeof y === "undefined") return;
  163. seriesGroup
  164. .append("circle")
  165. .attr("cx", xAxis(x))
  166. .attr("cy", yAxis(y))
  167. .attr("r", pipRadius)
  168. .attr("fill", seriesColours[seriesIndex % seriesColours.length])
  169. .append("title")
  170. .text(d => {
  171. return `${x}\n
  172. --\n
  173. ${tooltipText[x]}\n
  174. `.replace(/\s{2,}/g, "\n");
  175. });
  176. });
  177. yAxesArea
  178. .append("g")
  179. .attr("transform", `translate(${seriesLabelWidth - interSeriesPadding}, ${seriesHeight * seriesIndex + interSeriesPadding * (seriesIndex + 1)})`)
  180. .attr("class", "axis axis--y")
  181. .call(d3.axisLeft(yAxis).ticks(5));
  182. yAxesArea
  183. .append("g")
  184. .attr("transform", `translate(0, ${seriesHeight / 2 + seriesHeight * seriesIndex + interSeriesPadding * (seriesIndex + 1)})`)
  185. .append("text")
  186. .style("text-anchor", "middle")
  187. .attr("transform", "rotate(-90)")
  188. .text(serie.name);
  189. });
  190. return svg._groups[0][0].outerHTML;
  191. }
  192. }
  193. export default SeriesChart;