|
@@ -103,7 +103,7 @@ const Charts = {
|
|
|
return { headings, values };
|
|
|
},
|
|
|
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* Gets values from input for a scatter plot with colour from the third column.
|
|
|
*
|
|
@@ -140,6 +140,50 @@ const Charts = {
|
|
|
},
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
+ * 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[]}
|
|
|
+ */
|
|
|
+ _getSeriesValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
|
|
|
+ let { headings, values } = Charts._getValues(
|
|
|
+ input,
|
|
|
+ recordDelimiter, fieldDelimiter,
|
|
|
+ false,
|
|
|
+ 3
|
|
|
+ );
|
|
|
+
|
|
|
+ let xValues = new Set(),
|
|
|
+ series = {};
|
|
|
+
|
|
|
+ values = values.forEach(row => {
|
|
|
+ let serie = row[0],
|
|
|
+ xVal = row[1],
|
|
|
+ val = parseFloat(row[2], 10);
|
|
|
+
|
|
|
+ if (Number.isNaN(val)) throw "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 (let seriesName in series) {
|
|
|
+ let serie = series[seriesName];
|
|
|
+ seriesList.push({name: seriesName, data: serie});
|
|
|
+ }
|
|
|
+
|
|
|
+ return { xValues, series: seriesList };
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
/**
|
|
|
* Hex Bin chart operation.
|
|
|
*
|
|
@@ -630,6 +674,168 @@ const Charts = {
|
|
|
|
|
|
return svg._groups[0][0].outerHTML;
|
|
|
},
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Series chart operation.
|
|
|
+ *
|
|
|
+ * @param {string} input
|
|
|
+ * @param {Object[]} args
|
|
|
+ * @returns {html}
|
|
|
+ */
|
|
|
+ runSeriesChart(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;
|
|
|
+
|
|
|
+ let { xValues, series } = Charts._getSeriesValues(input, recordDelimiter, fieldDelimiter),
|
|
|
+ allSeriesHeight = Object.keys(series).length * (interSeriesPadding + seriesHeight),
|
|
|
+ svgHeight = allSeriesHeight + xAxisHeight + interSeriesPadding;
|
|
|
+
|
|
|
+ let svg = document.createElement("svg");
|
|
|
+ svg = d3.select(svg)
|
|
|
+ .attr("width", "100%")
|
|
|
+ .attr("height", "100%")
|
|
|
+ .attr("viewBox", `0 0 ${svgWidth} ${svgHeight}`);
|
|
|
+
|
|
|
+ let 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);
|
|
|
+
|
|
|
+ let tooltipText = {},
|
|
|
+ tooltipAreaWidth = seriesWidth / xValues.length;
|
|
|
+
|
|
|
+ xValues.forEach(x => {
|
|
|
+ let tooltip = [];
|
|
|
+
|
|
|
+ series.forEach(serie => {
|
|
|
+ let y = serie.data[x];
|
|
|
+ if (typeof y === "undefined") return;
|
|
|
+
|
|
|
+ tooltip.push(`${serie.name}: ${y}`);
|
|
|
+ });
|
|
|
+
|
|
|
+ tooltipText[x] = tooltip.join("\n");
|
|
|
+ });
|
|
|
+
|
|
|
+ let 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");
|
|
|
+ });
|
|
|
+
|
|
|
+ let yAxesArea = svg.append("g")
|
|
|
+ .attr("transform", `translate(0, ${xAxisHeight})`);
|
|
|
+
|
|
|
+ series.forEach((serie, seriesIndex) => {
|
|
|
+ let yExtent = d3.extent(Object.values(serie.data)),
|
|
|
+ yAxis = d3.scaleLinear()
|
|
|
+ .domain(yExtent)
|
|
|
+ .range([seriesHeight, 0]);
|
|
|
+
|
|
|
+ let 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 => {
|
|
|
+ let 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 Charts;
|