123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227 |
- /**
- * @author tlwr [toby@toby.codes]
- * @author Matt C [me@mitt.dev]
- * @copyright Crown Copyright 2019
- * @license Apache-2.0
- */
- import * as d3temp from "d3";
- import * as nodomtemp from "nodom";
- import { getSeriesValues, RECORD_DELIMITER_OPTIONS, FIELD_DELIMITER_OPTIONS } from "../lib/Charts";
- import Operation from "../Operation";
- import Utils from "../Utils";
- const d3 = d3temp.default ? d3temp.default : d3temp;
- const nodom = nodomtemp.default ? nodomtemp.default: nodomtemp;
- /**
- * Series chart operation
- */
- class SeriesChart extends Operation {
- /**
- * SeriesChart constructor
- */
- constructor() {
- super();
- this.name = "Series chart";
- this.module = "Charts";
- this.description = "A time series graph is a line graph of repeated measurements taken over regular time intervals.";
- this.inputType = "string";
- this.outputType = "html";
- this.args = [
- {
- name: "Record delimiter",
- type: "option",
- value: RECORD_DELIMITER_OPTIONS,
- },
- {
- name: "Field delimiter",
- type: "option",
- value: FIELD_DELIMITER_OPTIONS,
- },
- {
- name: "X label",
- type: "string",
- value: "",
- },
- {
- name: "Point radius",
- type: "number",
- value: 1,
- },
- {
- name: "Series colours",
- type: "string",
- value: "mediumseagreen, dodgerblue, tomato",
- },
- ];
- }
- /**
- * Series chart operation.
- *
- * @param {string} input
- * @param {Object[]} args
- * @returns {html}
- */
- run(input, args) {
- const recordDelimiter = Utils.charRep(args[0]),
- fieldDelimiter = Utils.charRep(args[1]),
- xLabel = args[2],
- pipRadius = args[3],
- seriesColours = args[4].split(","),
- svgWidth = 500,
- interSeriesPadding = 20,
- xAxisHeight = 50,
- seriesLabelWidth = 50,
- seriesHeight = 100,
- seriesWidth = svgWidth - seriesLabelWidth - interSeriesPadding;
- const { xValues, series } = getSeriesValues(input, recordDelimiter, fieldDelimiter),
- allSeriesHeight = Object.keys(series).length * (interSeriesPadding + seriesHeight),
- svgHeight = allSeriesHeight + xAxisHeight + interSeriesPadding;
- const document = new nodom.Document();
- let svg = document.createElement("svg");
- svg = d3.select(svg)
- .attr("width", "100%")
- .attr("height", "100%")
- .attr("viewBox", `0 0 ${svgWidth} ${svgHeight}`);
- const xAxis = d3.scalePoint()
- .domain(xValues)
- .range([0, seriesWidth]);
- svg.append("g")
- .attr("class", "axis axis--x")
- .attr("transform", `translate(${seriesLabelWidth}, ${xAxisHeight})`)
- .call(
- d3.axisTop(xAxis).tickValues(xValues.filter((x, i) => {
- return [0, Math.round(xValues.length / 2), xValues.length -1].indexOf(i) >= 0;
- }))
- );
- svg.append("text")
- .attr("x", svgWidth / 2)
- .attr("y", xAxisHeight / 2)
- .style("text-anchor", "middle")
- .text(xLabel);
- const tooltipText = {},
- tooltipAreaWidth = seriesWidth / xValues.length;
- xValues.forEach(x => {
- const tooltip = [];
- series.forEach(serie => {
- const y = serie.data[x];
- if (typeof y === "undefined") return;
- tooltip.push(`${serie.name}: ${y}`);
- });
- tooltipText[x] = tooltip.join("\n");
- });
- const chartArea = svg.append("g")
- .attr("transform", `translate(${seriesLabelWidth}, ${xAxisHeight})`);
- chartArea
- .append("g")
- .selectAll("rect")
- .data(xValues)
- .enter()
- .append("rect")
- .attr("x", x => {
- return xAxis(x) - (tooltipAreaWidth / 2);
- })
- .attr("y", 0)
- .attr("width", tooltipAreaWidth)
- .attr("height", allSeriesHeight)
- .attr("stroke", "none")
- .attr("fill", "transparent")
- .append("title")
- .text(x => {
- return `${x}\n
- --\n
- ${tooltipText[x]}\n
- `.replace(/\s{2,}/g, "\n");
- });
- const yAxesArea = svg.append("g")
- .attr("transform", `translate(0, ${xAxisHeight})`);
- series.forEach((serie, seriesIndex) => {
- const yExtent = d3.extent(Object.values(serie.data)),
- yAxis = d3.scaleLinear()
- .domain(yExtent)
- .range([seriesHeight, 0]);
- const seriesGroup = chartArea
- .append("g")
- .attr("transform", `translate(0, ${seriesHeight * seriesIndex + interSeriesPadding * (seriesIndex + 1)})`);
- let path = "";
- xValues.forEach((x, xIndex) => {
- let nextX = xValues[xIndex + 1],
- y = serie.data[x],
- nextY= serie.data[nextX];
- if (typeof y === "undefined" || typeof nextY === "undefined") return;
- x = xAxis(x); nextX = xAxis(nextX);
- y = yAxis(y); nextY = yAxis(nextY);
- path += `M ${x} ${y} L ${nextX} ${nextY} z `;
- });
- seriesGroup
- .append("path")
- .attr("d", path)
- .attr("fill", "none")
- .attr("stroke", seriesColours[seriesIndex % seriesColours.length])
- .attr("stroke-width", "1");
- xValues.forEach(x => {
- const y = serie.data[x];
- if (typeof y === "undefined") return;
- seriesGroup
- .append("circle")
- .attr("cx", xAxis(x))
- .attr("cy", yAxis(y))
- .attr("r", pipRadius)
- .attr("fill", seriesColours[seriesIndex % seriesColours.length])
- .append("title")
- .text(d => {
- return `${x}\n
- --\n
- ${tooltipText[x]}\n
- `.replace(/\s{2,}/g, "\n");
- });
- });
- yAxesArea
- .append("g")
- .attr("transform", `translate(${seriesLabelWidth - interSeriesPadding}, ${seriesHeight * seriesIndex + interSeriesPadding * (seriesIndex + 1)})`)
- .attr("class", "axis axis--y")
- .call(d3.axisLeft(yAxis).ticks(5));
- yAxesArea
- .append("g")
- .attr("transform", `translate(0, ${seriesHeight / 2 + seriesHeight * seriesIndex + interSeriesPadding * (seriesIndex + 1)})`)
- .append("text")
- .style("text-anchor", "middle")
- .attr("transform", "rotate(-90)")
- .text(serie.name);
- });
- return svg._groups[0][0].outerHTML;
- }
- }
- export default SeriesChart;
|