mirror of
https://github.com/gchq/CyberChef
synced 2025-01-08 10:38:46 +00:00
Add hex density chart
This commit is contained in:
parent
fa89713f19
commit
281d558111
3 changed files with 245 additions and 0 deletions
|
@ -1021,6 +1021,7 @@ const Utils = {
|
||||||
"Comma": ",",
|
"Comma": ",",
|
||||||
"Semi-colon": ";",
|
"Semi-colon": ";",
|
||||||
"Colon": ":",
|
"Colon": ":",
|
||||||
|
"Tab": "\t",
|
||||||
"Line feed": "\n",
|
"Line feed": "\n",
|
||||||
"CRLF": "\r\n",
|
"CRLF": "\r\n",
|
||||||
"Forward slash": "/",
|
"Forward slash": "/",
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Base64 from "../operations/Base64.js";
|
||||||
import BitwiseOp from "../operations/BitwiseOp.js";
|
import BitwiseOp from "../operations/BitwiseOp.js";
|
||||||
import ByteRepr from "../operations/ByteRepr.js";
|
import ByteRepr from "../operations/ByteRepr.js";
|
||||||
import CharEnc from "../operations/CharEnc.js";
|
import CharEnc from "../operations/CharEnc.js";
|
||||||
|
import Charts from "../operations/Charts.js";
|
||||||
import Checksum from "../operations/Checksum.js";
|
import Checksum from "../operations/Checksum.js";
|
||||||
import Cipher from "../operations/Cipher.js";
|
import Cipher from "../operations/Cipher.js";
|
||||||
import Code from "../operations/Code.js";
|
import Code from "../operations/Code.js";
|
||||||
|
@ -3388,6 +3389,44 @@ const OperationConfig = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"Hex Density chart": {
|
||||||
|
description: [].join("\n"),
|
||||||
|
run: Charts.runHexDensityChart,
|
||||||
|
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: "Radius",
|
||||||
|
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: "",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OperationConfig;
|
export default OperationConfig;
|
||||||
|
|
205
src/core/operations/Charts.js
Executable file
205
src/core/operations/Charts.js
Executable file
|
@ -0,0 +1,205 @@
|
||||||
|
import * as d3 from "d3";
|
||||||
|
import {hexbin as d3hexbin} from "d3-hexbin";
|
||||||
|
import Utils from "../Utils.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Charting operations.
|
||||||
|
*
|
||||||
|
* @author tlwr [toby@toby.com]
|
||||||
|
* @copyright Crown Copyright 2016
|
||||||
|
* @license Apache-2.0
|
||||||
|
*
|
||||||
|
* @namespace
|
||||||
|
*/
|
||||||
|
const Charts = {
|
||||||
|
/**
|
||||||
|
* @constant
|
||||||
|
* @default
|
||||||
|
*/
|
||||||
|
RECORD_DELIMITER_OPTIONS: ["Line feed", "CRLF"],
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constant
|
||||||
|
* @default
|
||||||
|
*/
|
||||||
|
FIELD_DELIMITER_OPTIONS: ["Space", "Comma", "Semi-colon", "Colon", "Tab"],
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets values from input for a scatter plot.
|
||||||
|
*
|
||||||
|
* @param {string} input
|
||||||
|
* @param {string} recordDelimiter
|
||||||
|
* @param {string} fieldDelimiter
|
||||||
|
* @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
|
||||||
|
* @returns {Object[]}
|
||||||
|
*/
|
||||||
|
_getScatterValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
|
||||||
|
let headings;
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
input
|
||||||
|
.split(recordDelimiter)
|
||||||
|
.forEach((row, rowIndex) => {
|
||||||
|
let split = row.split(fieldDelimiter);
|
||||||
|
|
||||||
|
if (split.length !== 2) throw "Each row must have length 2.";
|
||||||
|
|
||||||
|
if (columnHeadingsAreIncluded && rowIndex === 0) {
|
||||||
|
headings = {};
|
||||||
|
headings.x = split[0];
|
||||||
|
headings.y = split[1];
|
||||||
|
} else {
|
||||||
|
let x = split[0],
|
||||||
|
y = split[1];
|
||||||
|
|
||||||
|
x = parseFloat(x, 10);
|
||||||
|
if (Number.isNaN(x)) throw "Values must be numbers in base 10.";
|
||||||
|
|
||||||
|
y = parseFloat(y, 10);
|
||||||
|
if (Number.isNaN(y)) throw "Values must be numbers in base 10.";
|
||||||
|
|
||||||
|
values.push([x, y]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { headings, values};
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hex Bin chart operation.
|
||||||
|
*
|
||||||
|
* @param {string} input
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {html}
|
||||||
|
*/
|
||||||
|
runHexDensityChart: function (input, args) {
|
||||||
|
const recordDelimiter = Utils.charRep[args[0]],
|
||||||
|
fieldDelimiter = Utils.charRep[args[1]],
|
||||||
|
radius = args[2],
|
||||||
|
columnHeadingsAreIncluded = args[3],
|
||||||
|
dimension = 500;
|
||||||
|
|
||||||
|
let xLabel = args[4],
|
||||||
|
yLabel = args[5],
|
||||||
|
{ headings, values } = Charts._getScatterValues(
|
||||||
|
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: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 30,
|
||||||
|
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 hexbin = d3hexbin()
|
||||||
|
.radius(radius)
|
||||||
|
.extent([0, 0], [width, height]);
|
||||||
|
|
||||||
|
let hexPoints = hexbin(values),
|
||||||
|
maxCount = Math.max(...hexPoints.map(b => b.length));
|
||||||
|
|
||||||
|
let xExtent = d3.extent(hexPoints, d => d.x),
|
||||||
|
yExtent = d3.extent(hexPoints, d => d.y);
|
||||||
|
xExtent[0] -= 2 * radius;
|
||||||
|
xExtent[1] += 2 * radius;
|
||||||
|
yExtent[0] -= 2 * radius;
|
||||||
|
yExtent[1] += 2 * radius;
|
||||||
|
|
||||||
|
let xAxis = d3.scaleLinear()
|
||||||
|
.domain(xExtent)
|
||||||
|
.range([0, width]);
|
||||||
|
let yAxis = d3.scaleLinear()
|
||||||
|
.domain(yExtent)
|
||||||
|
.range([height, 0]);
|
||||||
|
|
||||||
|
let color = d3.scaleSequential(d3.interpolateLab("white", "steelblue"))
|
||||||
|
.domain([0, maxCount]);
|
||||||
|
|
||||||
|
marginedSpace.append("clipPath")
|
||||||
|
.attr("id", "clip")
|
||||||
|
.append("rect")
|
||||||
|
.attr("width", width)
|
||||||
|
.attr("height", height);
|
||||||
|
|
||||||
|
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(radius * 0.75)}`;
|
||||||
|
})
|
||||||
|
.attr("fill", (d) => color(d.length))
|
||||||
|
.append("title")
|
||||||
|
.text(d => {
|
||||||
|
let 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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Charts;
|
Loading…
Reference in a new issue