Add Series chart operation
This commit is contained in:
parent
39ab600887
commit
6784a1c027
@ -3558,6 +3558,39 @@ const OperationConfig = {
|
||||
},
|
||||
]
|
||||
},
|
||||
"Series chart": {
|
||||
description: [].join("\n"),
|
||||
run: Charts.runSeriesChart,
|
||||
inputType: "string",
|
||||
outputType: "html",
|
||||
args: [
|
||||
{
|
||||
name: "Record delimiter",
|
||||
type: "option",
|
||||
value: Charts.RECORD_DELIMITER_OPTIONS,
|
||||
},
|
||||
{
|
||||
name: "Field delimiter",
|
||||
type: "option",
|
||||
value: Charts.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",
|
||||
},
|
||||
]
|
||||
},
|
||||
"HTML to Text": {
|
||||
description: [].join("\n"),
|
||||
run: HTML.runHTMLToText,
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user