mirror of
https://github.com/gchq/CyberChef.git
synced 2025-04-22 15:56:16 -04:00
Merge gchq/master into bz2-comp
This commit is contained in:
commit
8b12caad78
37 changed files with 4362 additions and 2965 deletions
79
src/core/operations/BLAKE2b.mjs
Normal file
79
src/core/operations/BLAKE2b.mjs
Normal file
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* @author h345983745
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import blakejs from "blakejs";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Utils from "../Utils";
|
||||
import { toBase64 } from "../lib/Base64";
|
||||
|
||||
/**
|
||||
* BLAKE2b operation
|
||||
*/
|
||||
class BLAKE2b extends Operation {
|
||||
|
||||
/**
|
||||
* BLAKE2b constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "BLAKE2b";
|
||||
this.module = "Hashing";
|
||||
this.description = `Performs BLAKE2b hashing on the input.
|
||||
<br><br> BLAKE2b is a flavour of the BLAKE cryptographic hash function that is optimized for 64-bit platforms and produces digests of any size between 1 and 64 bytes.
|
||||
<br><br> Supports the use of an optional key.`;
|
||||
this.infoURL = "https://wikipedia.org/wiki/BLAKE_(hash_function)#BLAKE2b_algorithm";
|
||||
this.inputType = "ArrayBuffer";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
{
|
||||
"name": "Size",
|
||||
"type": "option",
|
||||
"value": ["512", "384", "256", "160", "128"]
|
||||
}, {
|
||||
"name": "Output Encoding",
|
||||
"type": "option",
|
||||
"value": ["Hex", "Base64", "Raw"]
|
||||
}, {
|
||||
"name": "Key",
|
||||
"type": "toggleString",
|
||||
"value": "",
|
||||
"toggleValues": ["UTF8", "Decimal", "Base64", "Hex", "Latin1"]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string} The input having been hashed with BLAKE2b in the encoding format speicifed.
|
||||
*/
|
||||
run(input, args) {
|
||||
const [outSize, outFormat] = args;
|
||||
let key = Utils.convertToByteArray(args[2].string || "", args[2].option);
|
||||
if (key.length === 0) {
|
||||
key = null;
|
||||
} else if (key.length > 64) {
|
||||
throw new OperationError(["Key cannot be greater than 64 bytes", "It is currently " + key.length + " bytes."].join("\n"));
|
||||
}
|
||||
|
||||
input = new Uint8Array(input);
|
||||
switch (outFormat) {
|
||||
case "Hex":
|
||||
return blakejs.blake2bHex(input, key, outSize / 8);
|
||||
case "Base64":
|
||||
return toBase64(blakejs.blake2b(input, key, outSize / 8));
|
||||
case "Raw":
|
||||
return Utils.arrayBufferToStr(blakejs.blake2b(input, key, outSize / 8).buffer);
|
||||
default:
|
||||
return new OperationError("Unsupported Output Type");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BLAKE2b;
|
80
src/core/operations/BLAKE2s.mjs
Normal file
80
src/core/operations/BLAKE2s.mjs
Normal file
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* @author h345983745
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import blakejs from "blakejs";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Utils from "../Utils";
|
||||
import { toBase64 } from "../lib/Base64";
|
||||
|
||||
/**
|
||||
* BLAKE2s Operation
|
||||
*/
|
||||
class BLAKE2s extends Operation {
|
||||
|
||||
/**
|
||||
* BLAKE2s constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "BLAKE2s";
|
||||
this.module = "Hashing";
|
||||
this.description = `Performs BLAKE2s hashing on the input.
|
||||
<br><br>BLAKE2s is a flavour of the BLAKE cryptographic hash function that is optimized for 8- to 32-bit platforms and produces digests of any size between 1 and 32 bytes.
|
||||
<br><br>Supports the use of an optional key.`;
|
||||
this.infoURL = "https://wikipedia.org/wiki/BLAKE_(hash_function)#BLAKE2";
|
||||
this.inputType = "ArrayBuffer";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
{
|
||||
"name": "Size",
|
||||
"type": "option",
|
||||
"value": ["256", "160", "128"]
|
||||
}, {
|
||||
"name": "Output Encoding",
|
||||
"type": "option",
|
||||
"value": ["Hex", "Base64", "Raw"]
|
||||
},
|
||||
{
|
||||
"name": "Key",
|
||||
"type": "toggleString",
|
||||
"value": "",
|
||||
"toggleValues": ["UTF8", "Decimal", "Base64", "Hex", "Latin1"]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string} The input having been hashed with BLAKE2s in the encoding format speicifed.
|
||||
*/
|
||||
run(input, args) {
|
||||
const [outSize, outFormat] = args;
|
||||
let key = Utils.convertToByteArray(args[2].string || "", args[2].option);
|
||||
if (key.length === 0) {
|
||||
key = null;
|
||||
} else if (key.length > 32) {
|
||||
throw new OperationError(["Key cannot be greater than 32 bytes", "It is currently " + key.length + " bytes."].join("\n"));
|
||||
}
|
||||
|
||||
input = new Uint8Array(input);
|
||||
switch (outFormat) {
|
||||
case "Hex":
|
||||
return blakejs.blake2sHex(input, key, outSize / 8);
|
||||
case "Base64":
|
||||
return toBase64(blakejs.blake2s(input, key, outSize / 8));
|
||||
case "Raw":
|
||||
return Utils.arrayBufferToStr(blakejs.blake2s(input, key, outSize / 8).buffer);
|
||||
default:
|
||||
return new OperationError("Unsupported Output Type");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BLAKE2s;
|
|
@ -23,7 +23,7 @@ class ExtractFiles extends Operation {
|
|||
|
||||
this.name = "Extract Files";
|
||||
this.module = "Default";
|
||||
this.description = "TODO";
|
||||
this.description = "Performs file carving to attempt to extract files from the input.<br><br>This operation is currently capable of carving out the following formats:<ul><li>JPG</li><li>EXE</li><li>ZIP</li><li>PDF</li><li>PNG</li><li>BMP</li><li>FLV</li><li>RTF</li><li>DOCX, PPTX, XLSX</li><li>EPUB</li><li>GZIP</li><li>ZLIB</li><li>ELF, BIN, AXF, O, PRX, SO</li></ul>";
|
||||
this.infoURL = "https://forensicswiki.org/wiki/File_Carving";
|
||||
this.inputType = "ArrayBuffer";
|
||||
this.outputType = "List<File>";
|
||||
|
|
|
@ -28,6 +28,8 @@ import Fletcher64Checksum from "./Fletcher64Checksum";
|
|||
import Adler32Checksum from "./Adler32Checksum";
|
||||
import CRC16Checksum from "./CRC16Checksum";
|
||||
import CRC32Checksum from "./CRC32Checksum";
|
||||
import BLAKE2b from "./BLAKE2b";
|
||||
import BLAKE2s from "./BLAKE2s";
|
||||
|
||||
/**
|
||||
* Generate all hashes operation
|
||||
|
@ -86,6 +88,14 @@ class GenerateAllHashes extends Operation {
|
|||
"\nWhirlpool-0: " + (new Whirlpool()).run(arrayBuffer, ["Whirlpool-0"]) +
|
||||
"\nWhirlpool-T: " + (new Whirlpool()).run(arrayBuffer, ["Whirlpool-T"]) +
|
||||
"\nWhirlpool: " + (new Whirlpool()).run(arrayBuffer, ["Whirlpool"]) +
|
||||
"\nBLAKE2b-128: " + (new BLAKE2b).run(arrayBuffer, ["128", "Hex", {string: "", option: "UTF8"}]) +
|
||||
"\nBLAKE2b-160: " + (new BLAKE2b).run(arrayBuffer, ["160", "Hex", {string: "", option: "UTF8"}]) +
|
||||
"\nBLAKE2b-256: " + (new BLAKE2b).run(arrayBuffer, ["256", "Hex", {string: "", option: "UTF8"}]) +
|
||||
"\nBLAKE2b-384: " + (new BLAKE2b).run(arrayBuffer, ["384", "Hex", {string: "", option: "UTF8"}]) +
|
||||
"\nBLAKE2b-512: " + (new BLAKE2b).run(arrayBuffer, ["512", "Hex", {string: "", option: "UTF8"}]) +
|
||||
"\nBLAKE2s-128: " + (new BLAKE2s).run(arrayBuffer, ["128", "Hex", {string: "", option: "UTF8"}]) +
|
||||
"\nBLAKE2s-160: " + (new BLAKE2s).run(arrayBuffer, ["160", "Hex", {string: "", option: "UTF8"}]) +
|
||||
"\nBLAKE2s-256: " + (new BLAKE2s).run(arrayBuffer, ["256", "Hex", {string: "", option: "UTF8"}]) +
|
||||
"\nSSDEEP: " + (new SSDEEP()).run(str) +
|
||||
"\nCTPH: " + (new CTPH()).run(str) +
|
||||
"\n\nChecksums:" +
|
||||
|
|
41
src/core/operations/HTMLToText.mjs
Normal file
41
src/core/operations/HTMLToText.mjs
Normal file
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* @author tlwr [toby@toby.codes]
|
||||
* @author Matt C [me@mitt.dev]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
|
||||
/**
|
||||
* HTML To Text operation
|
||||
*/
|
||||
class HTMLToText extends Operation {
|
||||
|
||||
/**
|
||||
* HTMLToText constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "HTML To Text";
|
||||
this.module = "Default";
|
||||
this.description = "Converts an HTML output from an operation to a readable string instead of being rendered in the DOM.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "html";
|
||||
this.outputType = "string";
|
||||
this.args = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {html} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
return input;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default HTMLToText;
|
266
src/core/operations/HeatmapChart.mjs
Normal file
266
src/core/operations/HeatmapChart.mjs
Normal file
|
@ -0,0 +1,266 @@
|
|||
/**
|
||||
* @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 { getScatterValues, RECORD_DELIMITER_OPTIONS, COLOURS, FIELD_DELIMITER_OPTIONS } from "../lib/Charts";
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Utils from "../Utils";
|
||||
|
||||
const d3 = d3temp.default ? d3temp.default : d3temp;
|
||||
const nodom = nodomtemp.default ? nodomtemp.default: nodomtemp;
|
||||
|
||||
/**
|
||||
* Heatmap chart operation
|
||||
*/
|
||||
class HeatmapChart extends Operation {
|
||||
|
||||
/**
|
||||
* HeatmapChart constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Heatmap chart";
|
||||
this.module = "Charts";
|
||||
this.description = "A heatmap is a graphical representation of data where the individual values contained in a matrix are represented as colors.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Heat_map";
|
||||
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: "Number of vertical bins",
|
||||
type: "number",
|
||||
value: 25,
|
||||
},
|
||||
{
|
||||
name: "Number of horizontal bins",
|
||||
type: "number",
|
||||
value: 25,
|
||||
},
|
||||
{
|
||||
name: "Use column headers as labels",
|
||||
type: "boolean",
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: "X label",
|
||||
type: "string",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "Y label",
|
||||
type: "string",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "Draw bin edges",
|
||||
type: "boolean",
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: "Min colour value",
|
||||
type: "string",
|
||||
value: COLOURS.min,
|
||||
},
|
||||
{
|
||||
name: "Max colour value",
|
||||
type: "string",
|
||||
value: COLOURS.max,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Heatmap chart operation.
|
||||
*
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {html}
|
||||
*/
|
||||
run(input, args) {
|
||||
const recordDelimiter = Utils.charRep(args[0]),
|
||||
fieldDelimiter = Utils.charRep(args[1]),
|
||||
vBins = args[2],
|
||||
hBins = args[3],
|
||||
columnHeadingsAreIncluded = args[4],
|
||||
drawEdges = args[7],
|
||||
minColour = args[8],
|
||||
maxColour = args[9],
|
||||
dimension = 500;
|
||||
if (vBins <= 0) throw new OperationError("Number of vertical bins must be greater than 0");
|
||||
if (hBins <= 0) throw new OperationError("Number of horizontal bins must be greater than 0");
|
||||
|
||||
let xLabel = args[5],
|
||||
yLabel = args[6];
|
||||
const { headings, values } = getScatterValues(
|
||||
input,
|
||||
recordDelimiter,
|
||||
fieldDelimiter,
|
||||
columnHeadingsAreIncluded
|
||||
);
|
||||
|
||||
if (headings) {
|
||||
xLabel = headings.x;
|
||||
yLabel = headings.y;
|
||||
}
|
||||
|
||||
const document = new nodom.Document();
|
||||
let svg = document.createElement("svg");
|
||||
svg = d3.select(svg)
|
||||
.attr("width", "100%")
|
||||
.attr("height", "100%")
|
||||
.attr("viewBox", `0 0 ${dimension} ${dimension}`);
|
||||
|
||||
const margin = {
|
||||
top: 10,
|
||||
right: 0,
|
||||
bottom: 40,
|
||||
left: 30,
|
||||
},
|
||||
width = dimension - margin.left - margin.right,
|
||||
height = dimension - margin.top - margin.bottom,
|
||||
binWidth = width / hBins,
|
||||
binHeight = height/ vBins,
|
||||
marginedSpace = svg.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
const bins = this.getHeatmapPacking(values, vBins, hBins),
|
||||
maxCount = Math.max(...bins.map(row => {
|
||||
const lengths = row.map(cell => cell.length);
|
||||
return Math.max(...lengths);
|
||||
}));
|
||||
|
||||
const xExtent = d3.extent(values, d => d[0]),
|
||||
yExtent = d3.extent(values, d => d[1]);
|
||||
|
||||
const xAxis = d3.scaleLinear()
|
||||
.domain(xExtent)
|
||||
.range([0, width]);
|
||||
const yAxis = d3.scaleLinear()
|
||||
.domain(yExtent)
|
||||
.range([height, 0]);
|
||||
|
||||
const colour = d3.scaleSequential(d3.interpolateLab(minColour, maxColour))
|
||||
.domain([0, maxCount]);
|
||||
|
||||
marginedSpace.append("clipPath")
|
||||
.attr("id", "clip")
|
||||
.append("rect")
|
||||
.attr("width", width)
|
||||
.attr("height", height);
|
||||
|
||||
marginedSpace.append("g")
|
||||
.attr("class", "bins")
|
||||
.attr("clip-path", "url(#clip)")
|
||||
.selectAll("g")
|
||||
.data(bins)
|
||||
.enter()
|
||||
.append("g")
|
||||
.selectAll("rect")
|
||||
.data(d => d)
|
||||
.enter()
|
||||
.append("rect")
|
||||
.attr("x", (d) => binWidth * d.x)
|
||||
.attr("y", (d) => (height - binHeight * (d.y + 1)))
|
||||
.attr("width", binWidth)
|
||||
.attr("height", binHeight)
|
||||
.attr("fill", (d) => colour(d.length))
|
||||
.attr("stroke", drawEdges ? "rgba(0, 0, 0, 0.5)" : "none")
|
||||
.attr("stroke-width", drawEdges ? "0.5" : "none")
|
||||
.append("title")
|
||||
.text(d => {
|
||||
const count = d.length,
|
||||
perc = 100.0 * d.length / values.length,
|
||||
tooltip = `Count: ${count}\n
|
||||
Percentage: ${perc.toFixed(2)}%\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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Packs a list of x, y coordinates into a number of bins for use in a heatmap.
|
||||
*
|
||||
* @param {Object[]} points
|
||||
* @param {number} number of vertical bins
|
||||
* @param {number} number of horizontal bins
|
||||
* @returns {Object[]} a list of bins (each bin is an Array) with x y coordinates, filled with the points
|
||||
*/
|
||||
getHeatmapPacking(values, vBins, hBins) {
|
||||
const xBounds = d3.extent(values, d => d[0]),
|
||||
yBounds = d3.extent(values, d => d[1]),
|
||||
bins = [];
|
||||
|
||||
if (xBounds[0] === xBounds[1]) throw "Cannot pack points. There is no difference between the minimum and maximum X coordinate.";
|
||||
if (yBounds[0] === yBounds[1]) throw "Cannot pack points. There is no difference between the minimum and maximum Y coordinate.";
|
||||
|
||||
for (let y = 0; y < vBins; y++) {
|
||||
bins.push([]);
|
||||
for (let x = 0; x < hBins; x++) {
|
||||
const item = [];
|
||||
item.y = y;
|
||||
item.x = x;
|
||||
|
||||
bins[y].push(item);
|
||||
} // x
|
||||
} // y
|
||||
|
||||
const epsilon = 0.000000001; // This is to clamp values that are exactly the maximum;
|
||||
|
||||
values.forEach(v => {
|
||||
const fractionOfY = (v[1] - yBounds[0]) / ((yBounds[1] + epsilon) - yBounds[0]),
|
||||
fractionOfX = (v[0] - xBounds[0]) / ((xBounds[1] + epsilon) - xBounds[0]),
|
||||
y = Math.floor(vBins * fractionOfY),
|
||||
x = Math.floor(hBins * fractionOfX);
|
||||
|
||||
bins[y][x].push({x: v[0], y: v[1]});
|
||||
});
|
||||
|
||||
return bins;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default HeatmapChart;
|
296
src/core/operations/HexDensityChart.mjs
Normal file
296
src/core/operations/HexDensityChart.mjs
Normal file
|
@ -0,0 +1,296 @@
|
|||
/**
|
||||
* @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 d3hexbintemp from "d3-hexbin";
|
||||
import * as nodomtemp from "nodom";
|
||||
import { getScatterValues, RECORD_DELIMITER_OPTIONS, COLOURS, FIELD_DELIMITER_OPTIONS } from "../lib/Charts";
|
||||
|
||||
import Operation from "../Operation";
|
||||
import Utils from "../Utils";
|
||||
|
||||
const d3 = d3temp.default ? d3temp.default : d3temp;
|
||||
const d3hexbin = d3hexbintemp.default ? d3hexbintemp.default : d3hexbintemp;
|
||||
const nodom = nodomtemp.default ? nodomtemp.default: nodomtemp;
|
||||
|
||||
|
||||
/**
|
||||
* Hex Density chart operation
|
||||
*/
|
||||
class HexDensityChart extends Operation {
|
||||
|
||||
/**
|
||||
* HexDensityChart constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Hex Density chart";
|
||||
this.module = "Charts";
|
||||
this.description = "Hex density charts are used in a similar way to scatter charts, however rather than rendering tens of thousands of points, it groups the points into a few hundred hexagons to show the distribution.";
|
||||
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: "Pack radius",
|
||||
type: "number",
|
||||
value: 25,
|
||||
},
|
||||
{
|
||||
name: "Draw radius",
|
||||
type: "number",
|
||||
value: 15,
|
||||
},
|
||||
{
|
||||
name: "Use column headers as labels",
|
||||
type: "boolean",
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: "X label",
|
||||
type: "string",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "Y label",
|
||||
type: "string",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "Draw hexagon edges",
|
||||
type: "boolean",
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: "Min colour value",
|
||||
type: "string",
|
||||
value: COLOURS.min,
|
||||
},
|
||||
{
|
||||
name: "Max colour value",
|
||||
type: "string",
|
||||
value: COLOURS.max,
|
||||
},
|
||||
{
|
||||
name: "Draw empty hexagons within data boundaries",
|
||||
type: "boolean",
|
||||
value: false,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hex Bin chart operation.
|
||||
*
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {html}
|
||||
*/
|
||||
run(input, args) {
|
||||
const recordDelimiter = Utils.charRep(args[0]),
|
||||
fieldDelimiter = Utils.charRep(args[1]),
|
||||
packRadius = args[2],
|
||||
drawRadius = args[3],
|
||||
columnHeadingsAreIncluded = args[4],
|
||||
drawEdges = args[7],
|
||||
minColour = args[8],
|
||||
maxColour = args[9],
|
||||
drawEmptyHexagons = args[10],
|
||||
dimension = 500;
|
||||
|
||||
let xLabel = args[5],
|
||||
yLabel = args[6];
|
||||
const { headings, values } = getScatterValues(
|
||||
input,
|
||||
recordDelimiter,
|
||||
fieldDelimiter,
|
||||
columnHeadingsAreIncluded
|
||||
);
|
||||
|
||||
if (headings) {
|
||||
xLabel = headings.x;
|
||||
yLabel = headings.y;
|
||||
}
|
||||
|
||||
const document = new nodom.Document();
|
||||
let svg = document.createElement("svg");
|
||||
svg = d3.select(svg)
|
||||
.attr("width", "100%")
|
||||
.attr("height", "100%")
|
||||
.attr("viewBox", `0 0 ${dimension} ${dimension}`);
|
||||
|
||||
const 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 + ")");
|
||||
|
||||
const hexbin = d3hexbin.hexbin()
|
||||
.radius(packRadius)
|
||||
.extent([0, 0], [width, height]);
|
||||
|
||||
const hexPoints = hexbin(values),
|
||||
maxCount = Math.max(...hexPoints.map(b => b.length));
|
||||
|
||||
const xExtent = d3.extent(hexPoints, d => d.x),
|
||||
yExtent = d3.extent(hexPoints, d => d.y);
|
||||
xExtent[0] -= 2 * packRadius;
|
||||
xExtent[1] += 3 * packRadius;
|
||||
yExtent[0] -= 2 * packRadius;
|
||||
yExtent[1] += 2 * packRadius;
|
||||
|
||||
const xAxis = d3.scaleLinear()
|
||||
.domain(xExtent)
|
||||
.range([0, width]);
|
||||
const yAxis = d3.scaleLinear()
|
||||
.domain(yExtent)
|
||||
.range([height, 0]);
|
||||
|
||||
const colour = d3.scaleSequential(d3.interpolateLab(minColour, maxColour))
|
||||
.domain([0, maxCount]);
|
||||
|
||||
marginedSpace.append("clipPath")
|
||||
.attr("id", "clip")
|
||||
.append("rect")
|
||||
.attr("width", width)
|
||||
.attr("height", height);
|
||||
|
||||
if (drawEmptyHexagons) {
|
||||
marginedSpace.append("g")
|
||||
.attr("class", "empty-hexagon")
|
||||
.selectAll("path")
|
||||
.data(this.getEmptyHexagons(hexPoints, packRadius))
|
||||
.enter()
|
||||
.append("path")
|
||||
.attr("d", d => {
|
||||
return `M${xAxis(d.x)},${yAxis(d.y)} ${hexbin.hexagon(drawRadius)}`;
|
||||
})
|
||||
.attr("fill", (d) => colour(0))
|
||||
.attr("stroke", drawEdges ? "black" : "none")
|
||||
.attr("stroke-width", drawEdges ? "0.5" : "none")
|
||||
.append("title")
|
||||
.text(d => {
|
||||
const count = 0,
|
||||
perc = 0,
|
||||
tooltip = `Count: ${count}\n
|
||||
Percentage: ${perc.toFixed(2)}%\n
|
||||
Center: ${d.x.toFixed(2)}, ${d.y.toFixed(2)}\n
|
||||
`.replace(/\s{2,}/g, "\n");
|
||||
return tooltip;
|
||||
});
|
||||
}
|
||||
|
||||
marginedSpace.append("g")
|
||||
.attr("class", "hexagon")
|
||||
.attr("clip-path", "url(#clip)")
|
||||
.selectAll("path")
|
||||
.data(hexPoints)
|
||||
.enter()
|
||||
.append("path")
|
||||
.attr("d", d => {
|
||||
return `M${xAxis(d.x)},${yAxis(d.y)} ${hexbin.hexagon(drawRadius)}`;
|
||||
})
|
||||
.attr("fill", (d) => colour(d.length))
|
||||
.attr("stroke", drawEdges ? "black" : "none")
|
||||
.attr("stroke-width", drawEdges ? "0.5" : "none")
|
||||
.append("title")
|
||||
.text(d => {
|
||||
const count = d.length,
|
||||
perc = 100.0 * d.length / values.length,
|
||||
CX = d.x,
|
||||
CY = d.y,
|
||||
xMin = Math.min(...d.map(d => d[0])),
|
||||
xMax = Math.max(...d.map(d => d[0])),
|
||||
yMin = Math.min(...d.map(d => d[1])),
|
||||
yMax = Math.max(...d.map(d => d[1])),
|
||||
tooltip = `Count: ${count}\n
|
||||
Percentage: ${perc.toFixed(2)}%\n
|
||||
Center: ${CX.toFixed(2)}, ${CY.toFixed(2)}\n
|
||||
Min X: ${xMin.toFixed(2)}\n
|
||||
Max X: ${xMax.toFixed(2)}\n
|
||||
Min Y: ${yMin.toFixed(2)}\n
|
||||
Max Y: ${yMax.toFixed(2)}
|
||||
`.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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hex Bin chart operation.
|
||||
*
|
||||
* @param {Object[]} - centres
|
||||
* @param {number} - radius
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
getEmptyHexagons(centres, radius) {
|
||||
const emptyCentres = [],
|
||||
boundingRect = [d3.extent(centres, d => d.x), d3.extent(centres, d => d.y)],
|
||||
hexagonCenterToEdge = Math.cos(2 * Math.PI / 12) * radius,
|
||||
hexagonEdgeLength = Math.sin(2 * Math.PI / 12) * radius;
|
||||
let indent = false;
|
||||
|
||||
for (let y = boundingRect[1][0]; y <= boundingRect[1][1] + radius; y += hexagonEdgeLength + radius) {
|
||||
for (let x = boundingRect[0][0]; x <= boundingRect[0][1] + radius; x += 2 * hexagonCenterToEdge) {
|
||||
let cx = x;
|
||||
const cy = y;
|
||||
|
||||
if (indent && x >= boundingRect[0][1]) break;
|
||||
if (indent) cx += hexagonCenterToEdge;
|
||||
|
||||
emptyCentres.push({x: cx, y: cy});
|
||||
}
|
||||
indent = !indent;
|
||||
}
|
||||
|
||||
return emptyCentres;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default HexDensityChart;
|
|
@ -21,7 +21,7 @@ class JavaScriptParser extends Operation {
|
|||
this.name = "JavaScript Parser";
|
||||
this.module = "Code";
|
||||
this.description = "Returns an Abstract Syntax Tree for valid JavaScript code.";
|
||||
this.infoURL = "https://en.wikipedia.org/wiki/Abstract_syntax_tree";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Abstract_syntax_tree";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
|
|
|
@ -21,7 +21,7 @@ class PEMToHex extends Operation {
|
|||
this.name = "PEM to Hex";
|
||||
this.module = "PublicKey";
|
||||
this.description = "Converts PEM (Privacy Enhanced Mail) format to a hexadecimal DER (Distinguished Encoding Rules) string.";
|
||||
this.infoURL = "https://en.wikipedia.org/wiki/X.690#DER_encoding";
|
||||
this.infoURL = "https://wikipedia.org/wiki/X.690#DER_encoding";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [];
|
||||
|
|
199
src/core/operations/ScatterChart.mjs
Normal file
199
src/core/operations/ScatterChart.mjs
Normal file
|
@ -0,0 +1,199 @@
|
|||
/**
|
||||
* @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 { getScatterValues, getScatterValuesWithColour, RECORD_DELIMITER_OPTIONS, COLOURS, 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;
|
||||
|
||||
/**
|
||||
* Scatter chart operation
|
||||
*/
|
||||
class ScatterChart extends Operation {
|
||||
|
||||
/**
|
||||
* ScatterChart constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Scatter chart";
|
||||
this.module = "Charts";
|
||||
this.description = "Plots two-variable data as single points on a graph.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Scatter_plot";
|
||||
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: "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: COLOURS.max,
|
||||
},
|
||||
{
|
||||
name: "Point radius",
|
||||
type: "number",
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
name: "Use colour from third column",
|
||||
type: "boolean",
|
||||
value: false,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scatter chart operation.
|
||||
*
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {html}
|
||||
*/
|
||||
run(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];
|
||||
|
||||
const dataFunction = colourInInput ? getScatterValuesWithColour : getScatterValues;
|
||||
const { headings, values } = dataFunction(
|
||||
input,
|
||||
recordDelimiter,
|
||||
fieldDelimiter,
|
||||
columnHeadingsAreIncluded
|
||||
);
|
||||
|
||||
if (headings) {
|
||||
xLabel = headings.x;
|
||||
yLabel = headings.y;
|
||||
}
|
||||
|
||||
const document = new nodom.Document();
|
||||
let svg = document.createElement("svg");
|
||||
svg = d3.select(svg)
|
||||
.attr("width", "100%")
|
||||
.attr("height", "100%")
|
||||
.attr("viewBox", `0 0 ${dimension} ${dimension}`);
|
||||
|
||||
const 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 + ")");
|
||||
|
||||
const 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 => {
|
||||
const 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 ScatterChart;
|
227
src/core/operations/SeriesChart.mjs
Normal file
227
src/core/operations/SeriesChart.mjs
Normal file
|
@ -0,0 +1,227 @@
|
|||
/**
|
||||
* @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;
|
Loading…
Add table
Add a link
Reference in a new issue