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": {
|
"HTML to Text": {
|
||||||
description: [].join("\n"),
|
description: [].join("\n"),
|
||||||
run: HTML.runHTMLToText,
|
run: HTML.runHTMLToText,
|
||||||
|
@ -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.
|
* Hex Bin chart operation.
|
||||||
*
|
*
|
||||||
@ -630,6 +674,168 @@ const Charts = {
|
|||||||
|
|
||||||
return svg._groups[0][0].outerHTML;
|
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;
|
export default Charts;
|
||||||
|
Loading…
Reference in New Issue
Block a user