Add scatter plot operation
This commit is contained in:
parent
49ea532cdc
commit
39ab600887
@ -3510,6 +3510,54 @@ const OperationConfig = {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"Scatter chart": {
|
||||||
|
description: [].join("\n"),
|
||||||
|
run: Charts.runScatterChart,
|
||||||
|
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: "Use column headers as labels",
|
||||||
|
type: "boolean",
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "X label",
|
||||||
|
type: "string",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Y label",
|
||||||
|
type: "string",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Colour",
|
||||||
|
type: "string",
|
||||||
|
value: Charts.COLOURS.max,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Point radius",
|
||||||
|
type: "number",
|
||||||
|
value: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Use colour from third column",
|
||||||
|
type: "boolean",
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
"HTML to Text": {
|
"HTML to Text": {
|
||||||
description: [].join("\n"),
|
description: [].join("\n"),
|
||||||
run: HTML.runHTMLToText,
|
run: HTML.runHTMLToText,
|
||||||
|
@ -26,6 +26,49 @@ const Charts = {
|
|||||||
FIELD_DELIMITER_OPTIONS: ["Space", "Comma", "Semi-colon", "Colon", "Tab"],
|
FIELD_DELIMITER_OPTIONS: ["Space", "Comma", "Semi-colon", "Colon", "Tab"],
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default from colour
|
||||||
|
*
|
||||||
|
* @constant
|
||||||
|
* @default
|
||||||
|
*/
|
||||||
|
COLOURS: {
|
||||||
|
min: "white",
|
||||||
|
max: "black",
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets values from input for a plot.
|
||||||
|
*
|
||||||
|
* @param {string} input
|
||||||
|
* @param {string} recordDelimiter
|
||||||
|
* @param {string} fieldDelimiter
|
||||||
|
* @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
|
||||||
|
* @returns {Object[]}
|
||||||
|
*/
|
||||||
|
_getValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded, length) {
|
||||||
|
let headings;
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
input
|
||||||
|
.split(recordDelimiter)
|
||||||
|
.forEach((row, rowIndex) => {
|
||||||
|
let split = row.split(fieldDelimiter);
|
||||||
|
|
||||||
|
if (split.length !== length) throw `Each row must have length ${length}.`;
|
||||||
|
|
||||||
|
if (columnHeadingsAreIncluded && rowIndex === 0) {
|
||||||
|
headings = split;
|
||||||
|
} else {
|
||||||
|
values.push(split);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { headings, values};
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets values from input for a scatter plot.
|
* Gets values from input for a scatter plot.
|
||||||
*
|
*
|
||||||
@ -36,47 +79,64 @@ const Charts = {
|
|||||||
* @returns {Object[]}
|
* @returns {Object[]}
|
||||||
*/
|
*/
|
||||||
_getScatterValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
|
_getScatterValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
|
||||||
let headings;
|
let { headings, values } = Charts._getValues(
|
||||||
const values = [];
|
input,
|
||||||
|
recordDelimiter, fieldDelimiter,
|
||||||
|
columnHeadingsAreIncluded,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
input
|
if (headings) {
|
||||||
.split(recordDelimiter)
|
headings = {x: headings[0], y: headings[1]};
|
||||||
.forEach((row, rowIndex) => {
|
}
|
||||||
let split = row.split(fieldDelimiter);
|
|
||||||
|
|
||||||
if (split.length !== 2) throw "Each row must have length 2.";
|
values = values.map(row => {
|
||||||
|
let x = parseFloat(row[0], 10),
|
||||||
|
y = parseFloat(row[1], 10);
|
||||||
|
|
||||||
if (columnHeadingsAreIncluded && rowIndex === 0) {
|
if (Number.isNaN(x)) throw "Values must be numbers in base 10.";
|
||||||
headings = {};
|
if (Number.isNaN(y)) throw "Values must be numbers in base 10.";
|
||||||
headings.x = split[0];
|
|
||||||
headings.y = split[1];
|
|
||||||
} else {
|
|
||||||
let x = split[0],
|
|
||||||
y = split[1];
|
|
||||||
|
|
||||||
x = parseFloat(x, 10);
|
return [x, y];
|
||||||
if (Number.isNaN(x)) throw "Values must be numbers in base 10.";
|
});
|
||||||
|
|
||||||
y = parseFloat(y, 10);
|
return { headings, values };
|
||||||
if (Number.isNaN(y)) throw "Values must be numbers in base 10.";
|
|
||||||
|
|
||||||
values.push([x, y]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { headings, values};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default from colour
|
* Gets values from input for a scatter plot with colour from the third column.
|
||||||
*
|
*
|
||||||
* @constant
|
* @param {string} input
|
||||||
* @default
|
* @param {string} recordDelimiter
|
||||||
|
* @param {string} fieldDelimiter
|
||||||
|
* @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
|
||||||
|
* @returns {Object[]}
|
||||||
*/
|
*/
|
||||||
COLOURS: {
|
_getScatterValuesWithColour(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
|
||||||
min: "white",
|
let { headings, values } = Charts._getValues(
|
||||||
max: "black",
|
input,
|
||||||
|
recordDelimiter, fieldDelimiter,
|
||||||
|
columnHeadingsAreIncluded,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
|
||||||
|
if (headings) {
|
||||||
|
headings = {x: headings[0], y: headings[1]};
|
||||||
|
}
|
||||||
|
|
||||||
|
values = values.map(row => {
|
||||||
|
let x = parseFloat(row[0], 10),
|
||||||
|
y = parseFloat(row[1], 10),
|
||||||
|
colour = row[2];
|
||||||
|
|
||||||
|
if (Number.isNaN(x)) throw "Values must be numbers in base 10.";
|
||||||
|
if (Number.isNaN(y)) throw "Values must be numbers in base 10.";
|
||||||
|
|
||||||
|
return [x, y, colour];
|
||||||
|
});
|
||||||
|
|
||||||
|
return { headings, values };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
@ -451,6 +511,125 @@ const Charts = {
|
|||||||
|
|
||||||
return svg._groups[0][0].outerHTML;
|
return svg._groups[0][0].outerHTML;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scatter chart operation.
|
||||||
|
*
|
||||||
|
* @param {string} input
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {html}
|
||||||
|
*/
|
||||||
|
runScatterChart: function (input, args) {
|
||||||
|
const recordDelimiter = Utils.charRep[args[0]],
|
||||||
|
fieldDelimiter = Utils.charRep[args[1]],
|
||||||
|
columnHeadingsAreIncluded = args[2],
|
||||||
|
fillColour = args[5],
|
||||||
|
radius = args[6],
|
||||||
|
colourInInput = args[7],
|
||||||
|
dimension = 500;
|
||||||
|
|
||||||
|
let xLabel = args[3],
|
||||||
|
yLabel = args[4];
|
||||||
|
|
||||||
|
let dataFunction = colourInInput ? Charts._getScatterValuesWithColour : Charts._getScatterValues;
|
||||||
|
|
||||||
|
let { headings, values } = dataFunction(
|
||||||
|
input,
|
||||||
|
recordDelimiter,
|
||||||
|
fieldDelimiter,
|
||||||
|
columnHeadingsAreIncluded
|
||||||
|
);
|
||||||
|
|
||||||
|
if (headings) {
|
||||||
|
xLabel = headings.x;
|
||||||
|
yLabel = headings.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
let svg = document.createElement("svg");
|
||||||
|
svg = d3.select(svg)
|
||||||
|
.attr("width", "100%")
|
||||||
|
.attr("height", "100%")
|
||||||
|
.attr("viewBox", `0 0 ${dimension} ${dimension}`);
|
||||||
|
|
||||||
|
let margin = {
|
||||||
|
top: 10,
|
||||||
|
right: 0,
|
||||||
|
bottom: 40,
|
||||||
|
left: 30,
|
||||||
|
},
|
||||||
|
width = dimension - margin.left - margin.right,
|
||||||
|
height = dimension - margin.top - margin.bottom,
|
||||||
|
marginedSpace = svg.append("g")
|
||||||
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||||
|
|
||||||
|
let xExtent = d3.extent(values, d => d[0]),
|
||||||
|
xDelta = xExtent[1] - xExtent[0],
|
||||||
|
yExtent = d3.extent(values, d => d[1]),
|
||||||
|
yDelta = yExtent[1] - yExtent[0],
|
||||||
|
xAxis = d3.scaleLinear()
|
||||||
|
.domain([xExtent[0] - (0.1 * xDelta), xExtent[1] + (0.1 * xDelta)])
|
||||||
|
.range([0, width]),
|
||||||
|
yAxis = d3.scaleLinear()
|
||||||
|
.domain([yExtent[0] - (0.1 * yDelta), yExtent[1] + (0.1 * yDelta)])
|
||||||
|
.range([height, 0]);
|
||||||
|
|
||||||
|
marginedSpace.append("clipPath")
|
||||||
|
.attr("id", "clip")
|
||||||
|
.append("rect")
|
||||||
|
.attr("width", width)
|
||||||
|
.attr("height", height);
|
||||||
|
|
||||||
|
marginedSpace.append("g")
|
||||||
|
.attr("class", "points")
|
||||||
|
.attr("clip-path", "url(#clip)")
|
||||||
|
.selectAll("circle")
|
||||||
|
.data(values)
|
||||||
|
.enter()
|
||||||
|
.append("circle")
|
||||||
|
.attr("cx", (d) => xAxis(d[0]))
|
||||||
|
.attr("cy", (d) => yAxis(d[1]))
|
||||||
|
.attr("r", d => radius)
|
||||||
|
.attr("fill", d => {
|
||||||
|
return colourInInput ? d[2] : fillColour;
|
||||||
|
})
|
||||||
|
.attr("stroke", "rgba(0, 0, 0, 0.5)")
|
||||||
|
.attr("stroke-width", "0.5")
|
||||||
|
.append("title")
|
||||||
|
.text(d => {
|
||||||
|
let x = d[0],
|
||||||
|
y = d[1],
|
||||||
|
tooltip = `X: ${x}\n
|
||||||
|
Y: ${y}\n
|
||||||
|
`.replace(/\s{2,}/g, "\n");
|
||||||
|
return tooltip;
|
||||||
|
});
|
||||||
|
|
||||||
|
marginedSpace.append("g")
|
||||||
|
.attr("class", "axis axis--y")
|
||||||
|
.call(d3.axisLeft(yAxis).tickSizeOuter(-width));
|
||||||
|
|
||||||
|
svg.append("text")
|
||||||
|
.attr("transform", "rotate(-90)")
|
||||||
|
.attr("y", -margin.left)
|
||||||
|
.attr("x", -(height / 2))
|
||||||
|
.attr("dy", "1em")
|
||||||
|
.style("text-anchor", "middle")
|
||||||
|
.text(yLabel);
|
||||||
|
|
||||||
|
marginedSpace.append("g")
|
||||||
|
.attr("class", "axis axis--x")
|
||||||
|
.attr("transform", "translate(0," + height + ")")
|
||||||
|
.call(d3.axisBottom(xAxis).tickSizeOuter(-height));
|
||||||
|
|
||||||
|
svg.append("text")
|
||||||
|
.attr("x", width / 2)
|
||||||
|
.attr("y", dimension)
|
||||||
|
.style("text-anchor", "middle")
|
||||||
|
.text(xLabel);
|
||||||
|
|
||||||
|
return svg._groups[0][0].outerHTML;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Charts;
|
export default Charts;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user