diff --git a/src/core/Chef.mjs b/src/core/Chef.mjs
index 79172479..a7302377 100755
--- a/src/core/Chef.mjs
+++ b/src/core/Chef.mjs
@@ -60,6 +60,12 @@ class Chef {
recipe.setBreakpoint(progress + 1, true);
}
+ // If the previously run operation presented a different value to its
+ // normal output, we need to recalculate it.
+ if (recipe.lastOpPresented(progress)) {
+ progress = 0;
+ }
+
// If stepping with flow control, we have to start from the beginning
// but still want to skip all previous breakpoints
if (progress > 0 && containsFc) {
diff --git a/src/core/Operation.mjs b/src/core/Operation.mjs
index e0b587c6..297aef93 100755
--- a/src/core/Operation.mjs
+++ b/src/core/Operation.mjs
@@ -81,9 +81,10 @@ class Operation {
* this behaviour.
*
* @param {*} data - The result of the run() function
+ * @param {Object[]} args - The operation's arguments
* @returns {*} - A human-readable version of the data
*/
- present(data) {
+ present(data, args) {
return data;
}
diff --git a/src/core/Recipe.mjs b/src/core/Recipe.mjs
index a1364f26..4ba07edb 100755
--- a/src/core/Recipe.mjs
+++ b/src/core/Recipe.mjs
@@ -212,7 +212,10 @@ class Recipe {
async present(dish) {
if (!this.lastRunOp) return;
- const output = await this.lastRunOp.present(await dish.get(this.lastRunOp.outputType));
+ const output = await this.lastRunOp.present(
+ await dish.get(this.lastRunOp.outputType),
+ this.lastRunOp.ingValues
+ );
dish.set(output, this.lastRunOp.presentType);
}
@@ -270,6 +273,18 @@ class Recipe {
return highlights;
}
+
+ /**
+ * Determines whether the previous operation has a different presentation type to its normal output.
+ *
+ * @param {number} progress
+ * @returns {boolean}
+ */
+ lastOpPresented(progress) {
+ if (progress < 1) return false;
+ return this.opList[progress-1].presentType !== this.opList[progress-1].outputType;
+ }
+
}
export default Recipe;
diff --git a/src/core/lib/CanvasComponents.mjs b/src/core/lib/CanvasComponents.mjs
new file mode 100755
index 00000000..04664c3d
--- /dev/null
+++ b/src/core/lib/CanvasComponents.mjs
@@ -0,0 +1,204 @@
+/**
+ * Various components for drawing diagrams on an HTML5 canvas.
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+/**
+ * Draws a line from one point to another
+ *
+ * @param ctx
+ * @param startX
+ * @param startY
+ * @param endX
+ * @param endY
+ */
+export function drawLine(ctx, startX, startY, endX, endY) {
+ ctx.beginPath();
+ ctx.moveTo(startX, startY);
+ ctx.lineTo(endX, endY);
+ ctx.closePath();
+ ctx.stroke();
+}
+
+/**
+ * Draws a bar chart on the canvas.
+ *
+ * @param canvas
+ * @param scores
+ * @param xAxisLabel
+ * @param yAxisLabel
+ * @param numXLabels
+ * @param numYLabels
+ * @param fontSize
+ */
+export function drawBarChart(canvas, scores, xAxisLabel, yAxisLabel, numXLabels, numYLabels, fontSize) {
+ fontSize = fontSize || 15;
+ if (!numXLabels || numXLabels > Math.round(canvas.width / 50)) {
+ numXLabels = Math.round(canvas.width / 50);
+ }
+ if (!numYLabels || numYLabels > Math.round(canvas.width / 50)) {
+ numYLabels = Math.round(canvas.height / 50);
+ }
+
+ // Graph properties
+ const ctx = canvas.getContext("2d"),
+ leftPadding = canvas.width * 0.08,
+ rightPadding = canvas.width * 0.03,
+ topPadding = canvas.height * 0.08,
+ bottomPadding = canvas.height * 0.15,
+ graphHeight = canvas.height - topPadding - bottomPadding,
+ graphWidth = canvas.width - leftPadding - rightPadding,
+ base = topPadding + graphHeight,
+ ceil = topPadding;
+
+ ctx.font = fontSize + "px Arial";
+
+ // Draw axis
+ ctx.lineWidth = "1.0";
+ ctx.strokeStyle = "#444";
+ drawLine(ctx, leftPadding, base, graphWidth + leftPadding, base); // x
+ drawLine(ctx, leftPadding, base, leftPadding, ceil); // y
+
+ // Bar properties
+ const barPadding = graphWidth * 0.003,
+ barWidth = (graphWidth - (barPadding * scores.length)) / scores.length,
+ max = Math.max.apply(Math, scores);
+ let currX = leftPadding + barPadding;
+
+ // Draw bars
+ ctx.fillStyle = "green";
+ for (let i = 0; i < scores.length; i++) {
+ const h = scores[i] / max * graphHeight;
+ ctx.fillRect(currX, base - h, barWidth, h);
+ currX += barWidth + barPadding;
+ }
+
+ // Mark x axis
+ ctx.fillStyle = "black";
+ ctx.textAlign = "center";
+ currX = leftPadding + barPadding;
+ if (numXLabels >= scores.length) {
+ // Mark every score
+ for (let i = 0; i <= scores.length; i++) {
+ ctx.fillText(i, currX, base + (bottomPadding * 0.3));
+ currX += barWidth + barPadding;
+ }
+ } else {
+ // Mark some scores
+ for (let i = 0; i <= numXLabels; i++) {
+ const val = Math.ceil((scores.length / numXLabels) * i);
+ currX = (graphWidth / numXLabels) * i + leftPadding;
+ ctx.fillText(val, currX, base + (bottomPadding * 0.3));
+ }
+ }
+
+ // Mark y axis
+ ctx.textAlign = "right";
+ let currY;
+ if (numYLabels >= max) {
+ // Mark every increment
+ for (let i = 0; i <= max; i++) {
+ currY = base - (i / max * graphHeight) + fontSize / 3;
+ ctx.fillText(i, leftPadding * 0.8, currY);
+ }
+ } else {
+ // Mark some increments
+ for (let i = 0; i <= numYLabels; i++) {
+ const val = Math.ceil((max / numYLabels) * i);
+ currY = base - (val / max * graphHeight) + fontSize / 3;
+ ctx.fillText(val, leftPadding * 0.8, currY);
+ }
+ }
+
+ // Label x axis
+ if (xAxisLabel) {
+ ctx.textAlign = "center";
+ ctx.fillText(xAxisLabel, graphWidth / 2 + leftPadding, base + bottomPadding * 0.8);
+ }
+
+ // Label y axis
+ if (yAxisLabel) {
+ ctx.save();
+ const x = leftPadding * 0.3,
+ y = graphHeight / 2 + topPadding;
+ ctx.translate(x, y);
+ ctx.rotate(-Math.PI / 2);
+ ctx.textAlign = "center";
+ ctx.fillText(yAxisLabel, 0, 0);
+ ctx.restore();
+ }
+}
+
+/**
+ * Draws a scale bar on the canvas.
+ *
+ * @param canvas
+ * @param score
+ * @param max
+ * @param markings
+ */
+export function drawScaleBar(canvas, score, max, markings) {
+ // Bar properties
+ const ctx = canvas.getContext("2d"),
+ leftPadding = canvas.width * 0.01,
+ rightPadding = canvas.width * 0.01,
+ topPadding = canvas.height * 0.1,
+ bottomPadding = canvas.height * 0.3,
+ barHeight = canvas.height - topPadding - bottomPadding,
+ barWidth = canvas.width - leftPadding - rightPadding;
+
+ // Scale properties
+ const proportion = score / max;
+
+ // Draw bar outline
+ ctx.strokeRect(leftPadding, topPadding, barWidth, barHeight);
+
+ // Shade in up to proportion
+ const grad = ctx.createLinearGradient(leftPadding, 0, barWidth + leftPadding, 0);
+ grad.addColorStop(0, "green");
+ grad.addColorStop(0.5, "gold");
+ grad.addColorStop(1, "red");
+ ctx.fillStyle = grad;
+ ctx.fillRect(leftPadding, topPadding, barWidth * proportion, barHeight);
+
+ // Add markings
+ let x0, y0, x1, y1;
+ ctx.fillStyle = "black";
+ ctx.textAlign = "center";
+ ctx.font = "13px Arial";
+ for (let i = 0; i < markings.length; i++) {
+ // Draw min line down
+ x0 = barWidth / max * markings[i].min + leftPadding;
+ y0 = topPadding + barHeight + (bottomPadding * 0.1);
+ x1 = x0;
+ y1 = topPadding + barHeight + (bottomPadding * 0.3);
+ drawLine(ctx, x0, y0, x1, y1);
+
+ // Draw max line down
+ x0 = barWidth / max * markings[i].max + leftPadding;
+ x1 = x0;
+ drawLine(ctx, x0, y0, x1, y1);
+
+ // Join min and max lines
+ x0 = barWidth / max * markings[i].min + leftPadding;
+ y0 = topPadding + barHeight + (bottomPadding * 0.3);
+ x1 = barWidth / max * markings[i].max + leftPadding;
+ y1 = y0;
+ drawLine(ctx, x0, y0, x1, y1);
+
+ // Add label
+ if (markings[i].max >= max * 0.9) {
+ ctx.textAlign = "right";
+ x0 = x1;
+ } else if (markings[i].max <= max * 0.1) {
+ ctx.textAlign = "left";
+ } else {
+ x0 = x0 + (x1 - x0) / 2;
+ }
+ y0 = topPadding + barHeight + (bottomPadding * 0.8);
+ ctx.fillText(markings[i].label, x0, y0);
+ }
+}
diff --git a/src/core/operations/ChiSquare.mjs b/src/core/operations/ChiSquare.mjs
new file mode 100644
index 00000000..89cb2214
--- /dev/null
+++ b/src/core/operations/ChiSquare.mjs
@@ -0,0 +1,53 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+
+/**
+ * Chi Square operation
+ */
+class ChiSquare extends Operation {
+
+ /**
+ * ChiSquare constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Chi Square";
+ this.module = "Default";
+ this.description = "Calculates the Chi Square distribution of values.";
+ this.inputType = "ArrayBuffer";
+ this.outputType = "number";
+ this.args = [];
+ }
+
+ /**
+ * @param {ArrayBuffer} input
+ * @param {Object[]} args
+ * @returns {number}
+ */
+ run(input, args) {
+ const data = new Uint8Array(input);
+ const distArray = new Array(256).fill(0);
+ let total = 0;
+
+ for (let i = 0; i < data.length; i++) {
+ distArray[data[i]]++;
+ }
+
+ for (let i = 0; i < distArray.length; i++) {
+ if (distArray[i] > 0) {
+ total += Math.pow(distArray[i] - data.length / 256, 2) / (data.length / 256);
+ }
+ }
+
+ return total;
+ }
+
+}
+
+export default ChiSquare;
diff --git a/src/core/operations/Entropy.mjs b/src/core/operations/Entropy.mjs
new file mode 100644
index 00000000..5accf07a
--- /dev/null
+++ b/src/core/operations/Entropy.mjs
@@ -0,0 +1,96 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import Utils from "../Utils";
+
+/**
+ * Entropy operation
+ */
+class Entropy extends Operation {
+
+ /**
+ * Entropy constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Entropy";
+ this.module = "Default";
+ this.description = "Calculates the Shannon entropy of the input data which gives an idea of its randomness. 8 is the maximum.";
+ this.inputType = "byteArray";
+ this.outputType = "number";
+ this.presentType = "html";
+ this.args = [];
+ }
+
+ /**
+ * @param {byteArray} input
+ * @param {Object[]} args
+ * @returns {number}
+ */
+ run(input, args) {
+ const prob = [],
+ uniques = input.unique(),
+ str = Utils.byteArrayToChars(input);
+ let i;
+
+ for (i = 0; i < uniques.length; i++) {
+ prob.push(str.count(Utils.chr(uniques[i])) / input.length);
+ }
+
+ let entropy = 0,
+ p;
+
+ for (i = 0; i < prob.length; i++) {
+ p = prob[i];
+ entropy += p * Math.log(p) / Math.log(2);
+ }
+
+ return -entropy;
+ }
+
+ /**
+ * Displays the entropy as a scale bar for web apps.
+ *
+ * @param {number} entropy
+ * @returns {html}
+ */
+ present(entropy) {
+ return `Shannon entropy: ${entropy}
+
+- 0 represents no randomness (i.e. all the bytes in the data have the same value) whereas 8, the maximum, represents a completely random string.
+- Standard English text usually falls somewhere between 3.5 and 5.
+- Properly encrypted or compressed data of a reasonable length should have an entropy of over 7.5.
+
+The following results show the entropy of chunks of the input data. Chunks with particularly high entropy could suggest encrypted or compressed sections.
+
+
`;
+ }
+
+}
+
+export default Entropy;
diff --git a/src/core/operations/FrequencyDistribution.mjs b/src/core/operations/FrequencyDistribution.mjs
new file mode 100644
index 00000000..bf82f1d4
--- /dev/null
+++ b/src/core/operations/FrequencyDistribution.mjs
@@ -0,0 +1,110 @@
+/**
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2016
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+import Utils from "../Utils";
+import OperationError from "../errors/OperationError";
+
+/**
+ * Frequency distribution operation
+ */
+class FrequencyDistribution extends Operation {
+
+ /**
+ * FrequencyDistribution constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Frequency distribution";
+ this.module = "Default";
+ this.description = "Displays the distribution of bytes in the data as a graph.";
+ this.inputType = "ArrayBuffer";
+ this.outputType = "json";
+ this.presentType = "html";
+ this.args = [
+ {
+ "name": "Show 0%s",
+ "type": "boolean",
+ "value": "Entropy.FREQ_ZEROS"
+ }
+ ];
+ }
+
+ /**
+ * @param {ArrayBuffer} input
+ * @param {Object[]} args
+ * @returns {json}
+ */
+ run(input, args) {
+ const data = new Uint8Array(input);
+ if (!data.length) throw new OperationError("No data");
+
+ const distrib = new Array(256).fill(0),
+ percentages = new Array(256),
+ len = data.length;
+ let i;
+
+ // Count bytes
+ for (i = 0; i < len; i++) {
+ distrib[data[i]]++;
+ }
+
+ // Calculate percentages
+ let repr = 0;
+ for (i = 0; i < 256; i++) {
+ if (distrib[i] > 0) repr++;
+ percentages[i] = distrib[i] / len * 100;
+ }
+
+ return {
+ "dataLength": len,
+ "percentages": percentages,
+ "distribution": distrib,
+ "bytesRepresented": repr
+ };
+ }
+
+ /**
+ * Displays the frequency distribution as a bar chart for web apps.
+ *
+ * @param {json} freq
+ * @returns {html}
+ */
+ present(freq, args) {
+ const showZeroes = args[0];
+ // Print
+ let output = `
+Total data length: ${freq.dataLength}
+Number of bytes represented: ${freq.bytesRepresented}
+Number of bytes not represented: ${256 - freq.bytesRepresented}
+
+Byte Percentage
+`;
+
+ for (let i = 0; i < 256; i++) {
+ if (freq.distribution[i] || showZeroes) {
+ output += " " + Utils.hex(i, 2) + " (" +
+ (freq.percentages[i].toFixed(2).replace(".00", "") + "%)").padEnd(8, " ") +
+ Array(Math.ceil(freq.percentages[i])+1).join("|") + "\n";
+ }
+ }
+
+ return output;
+ }
+
+}
+
+export default FrequencyDistribution;
diff --git a/src/core/operations/MicrosoftScriptDecoder.mjs b/src/core/operations/MicrosoftScriptDecoder.mjs
new file mode 100644
index 00000000..cc9d407f
--- /dev/null
+++ b/src/core/operations/MicrosoftScriptDecoder.mjs
@@ -0,0 +1,217 @@
+/**
+ * @author bmwhitn [brian.m.whitney@outlook.com]
+ * @copyright Crown Copyright 2017
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation";
+
+/**
+ * Microsoft Script Decoder operation
+ */
+class MicrosoftScriptDecoder extends Operation {
+
+ /**
+ * MicrosoftScriptDecoder constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "Microsoft Script Decoder";
+ this.module = "Default";
+ this.description = "Decodes Microsoft Encoded Script files that have been encoded with Microsoft's custom encoding. These are often VBS (Visual Basic Script) files that are encoded and renamed with a '.vbe' extention or JS (JScript) files renamed with a '.jse' extention.
Sample
Encoded:
#@~^RQAAAA==-mD~sX|:/TP{~J:+dYbxL~@!F@*@!+@*@!&@*eEI@#@&@#@&.jm.raY 214Wv:zms/obI0xEAAA==^#~@
Decoded:
var my_msg = "Testing <1><2><3>!";\n\nVScript.Echo(my_msg);
";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const matcher = /#@~\^.{6}==(.+).{6}==\^#~@/;
+ const encodedData = matcher.exec(input);
+ if (encodedData){
+ return MicrosoftScriptDecoder._decode(encodedData[1]);
+ } else {
+ return "";
+ }
+ }
+
+ /**
+ * Decodes Microsoft Encoded Script files that can be read and executed by cscript.exe/wscript.exe.
+ * This is a conversion of a Python script that was originally created by Didier Stevens
+ * (https://DidierStevens.com).
+ *
+ * @private
+ * @param {string} data
+ * @returns {string}
+ */
+ static _decode(data) {
+ const result = [];
+ let index = -1;
+ data = data.replace(/@&/g, String.fromCharCode(10))
+ .replace(/@#/g, String.fromCharCode(13))
+ .replace(/@\*/g, ">")
+ .replace(/@!/g, "<")
+ .replace(/@\$/g, "@");
+
+ for (let i = 0; i < data.length; i++) {
+ const byte = data.charCodeAt(i);
+ let char = data.charAt(i);
+ if (byte < 128) {
+ index++;
+ }
+
+ if ((byte === 9 || byte > 31 && byte < 128) &&
+ byte !== 60 &&
+ byte !== 62 &&
+ byte !== 64) {
+ char = D_DECODE[byte].charAt(D_COMBINATION[index % 64]);
+ }
+ result.push(char);
+ }
+ return result.join("");
+ }
+
+}
+
+const D_DECODE = [
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "\x57\x6E\x7B",
+ "\x4A\x4C\x41",
+ "\x0B\x0B\x0B",
+ "\x0C\x0C\x0C",
+ "\x4A\x4C\x41",
+ "\x0E\x0E\x0E",
+ "\x0F\x0F\x0F",
+ "\x10\x10\x10",
+ "\x11\x11\x11",
+ "\x12\x12\x12",
+ "\x13\x13\x13",
+ "\x14\x14\x14",
+ "\x15\x15\x15",
+ "\x16\x16\x16",
+ "\x17\x17\x17",
+ "\x18\x18\x18",
+ "\x19\x19\x19",
+ "\x1A\x1A\x1A",
+ "\x1B\x1B\x1B",
+ "\x1C\x1C\x1C",
+ "\x1D\x1D\x1D",
+ "\x1E\x1E\x1E",
+ "\x1F\x1F\x1F",
+ "\x2E\x2D\x32",
+ "\x47\x75\x30",
+ "\x7A\x52\x21",
+ "\x56\x60\x29",
+ "\x42\x71\x5B",
+ "\x6A\x5E\x38",
+ "\x2F\x49\x33",
+ "\x26\x5C\x3D",
+ "\x49\x62\x58",
+ "\x41\x7D\x3A",
+ "\x34\x29\x35",
+ "\x32\x36\x65",
+ "\x5B\x20\x39",
+ "\x76\x7C\x5C",
+ "\x72\x7A\x56",
+ "\x43\x7F\x73",
+ "\x38\x6B\x66",
+ "\x39\x63\x4E",
+ "\x70\x33\x45",
+ "\x45\x2B\x6B",
+ "\x68\x68\x62",
+ "\x71\x51\x59",
+ "\x4F\x66\x78",
+ "\x09\x76\x5E",
+ "\x62\x31\x7D",
+ "\x44\x64\x4A",
+ "\x23\x54\x6D",
+ "\x75\x43\x71",
+ "\x4A\x4C\x41",
+ "\x7E\x3A\x60",
+ "\x4A\x4C\x41",
+ "\x5E\x7E\x53",
+ "\x40\x4C\x40",
+ "\x77\x45\x42",
+ "\x4A\x2C\x27",
+ "\x61\x2A\x48",
+ "\x5D\x74\x72",
+ "\x22\x27\x75",
+ "\x4B\x37\x31",
+ "\x6F\x44\x37",
+ "\x4E\x79\x4D",
+ "\x3B\x59\x52",
+ "\x4C\x2F\x22",
+ "\x50\x6F\x54",
+ "\x67\x26\x6A",
+ "\x2A\x72\x47",
+ "\x7D\x6A\x64",
+ "\x74\x39\x2D",
+ "\x54\x7B\x20",
+ "\x2B\x3F\x7F",
+ "\x2D\x38\x2E",
+ "\x2C\x77\x4C",
+ "\x30\x67\x5D",
+ "\x6E\x53\x7E",
+ "\x6B\x47\x6C",
+ "\x66\x34\x6F",
+ "\x35\x78\x79",
+ "\x25\x5D\x74",
+ "\x21\x30\x43",
+ "\x64\x23\x26",
+ "\x4D\x5A\x76",
+ "\x52\x5B\x25",
+ "\x63\x6C\x24",
+ "\x3F\x48\x2B",
+ "\x7B\x55\x28",
+ "\x78\x70\x23",
+ "\x29\x69\x41",
+ "\x28\x2E\x34",
+ "\x73\x4C\x09",
+ "\x59\x21\x2A",
+ "\x33\x24\x44",
+ "\x7F\x4E\x3F",
+ "\x6D\x50\x77",
+ "\x55\x09\x3B",
+ "\x53\x56\x55",
+ "\x7C\x73\x69",
+ "\x3A\x35\x61",
+ "\x5F\x61\x63",
+ "\x65\x4B\x50",
+ "\x46\x58\x67",
+ "\x58\x3B\x51",
+ "\x31\x57\x49",
+ "\x69\x22\x4F",
+ "\x6C\x6D\x46",
+ "\x5A\x4D\x68",
+ "\x48\x25\x7C",
+ "\x27\x28\x36",
+ "\x5C\x46\x70",
+ "\x3D\x4A\x6E",
+ "\x24\x32\x7A",
+ "\x79\x41\x2F",
+ "\x37\x3D\x5F",
+ "\x60\x5F\x4B",
+ "\x51\x4F\x5A",
+ "\x20\x42\x2C",
+ "\x36\x65\x57"
+];
+
+const D_COMBINATION = [
+ 0, 1, 2, 0, 1, 2, 1, 2, 2, 1, 2, 1, 0, 2, 1, 2, 0, 2, 1, 2, 0, 0, 1, 2, 2, 1, 0, 2, 1, 2, 2, 1,
+ 0, 0, 2, 1, 2, 1, 2, 0, 2, 0, 0, 1, 2, 0, 2, 1, 0, 2, 1, 2, 0, 0, 1, 2, 2, 0, 0, 1, 2, 0, 2, 1
+];
+
+export default MicrosoftScriptDecoder;
diff --git a/src/core/vendor/canvascomponents.js b/src/core/vendor/canvascomponents.js
deleted file mode 100755
index f6ea0538..00000000
--- a/src/core/vendor/canvascomponents.js
+++ /dev/null
@@ -1,186 +0,0 @@
-"use strict";
-
-/**
- * Various components for drawing diagrams on an HTML5 canvas.
- *
- * @author n1474335 [n1474335@gmail.com]
- * @copyright Crown Copyright 2016
- * @license Apache-2.0
- *
- * @constant
- * @namespace
- */
-const CanvasComponents = {
-
- drawLine: function(ctx, startX, startY, endX, endY) {
- ctx.beginPath();
- ctx.moveTo(startX, startY);
- ctx.lineTo(endX, endY);
- ctx.closePath();
- ctx.stroke();
- },
-
- drawBarChart: function(canvas, scores, xAxisLabel, yAxisLabel, numXLabels, numYLabels, fontSize) {
- fontSize = fontSize || 15;
- if (!numXLabels || numXLabels > Math.round(canvas.width / 50)) {
- numXLabels = Math.round(canvas.width / 50);
- }
- if (!numYLabels || numYLabels > Math.round(canvas.width / 50)) {
- numYLabels = Math.round(canvas.height / 50);
- }
-
- // Graph properties
- var ctx = canvas.getContext("2d"),
- leftPadding = canvas.width * 0.08,
- rightPadding = canvas.width * 0.03,
- topPadding = canvas.height * 0.08,
- bottomPadding = canvas.height * 0.15,
- graphHeight = canvas.height - topPadding - bottomPadding,
- graphWidth = canvas.width - leftPadding - rightPadding,
- base = topPadding + graphHeight,
- ceil = topPadding;
-
- ctx.font = fontSize + "px Arial";
-
- // Draw axis
- ctx.lineWidth = "1.0";
- ctx.strokeStyle = "#444";
- CanvasComponents.drawLine(ctx, leftPadding, base, graphWidth + leftPadding, base); // x
- CanvasComponents.drawLine(ctx, leftPadding, base, leftPadding, ceil); // y
-
- // Bar properties
- var barPadding = graphWidth * 0.003,
- barWidth = (graphWidth - (barPadding * scores.length)) / scores.length,
- currX = leftPadding + barPadding,
- max = Math.max.apply(Math, scores);
-
- // Draw bars
- ctx.fillStyle = "green";
- for (var i = 0; i < scores.length; i++) {
- var h = scores[i] / max * graphHeight;
- ctx.fillRect(currX, base - h, barWidth, h);
- currX += barWidth + barPadding;
- }
-
- // Mark x axis
- ctx.fillStyle = "black";
- ctx.textAlign = "center";
- currX = leftPadding + barPadding;
- if (numXLabels >= scores.length) {
- // Mark every score
- for (i = 0; i <= scores.length; i++) {
- ctx.fillText(i, currX, base + (bottomPadding * 0.3));
- currX += barWidth + barPadding;
- }
- } else {
- // Mark some scores
- for (i = 0; i <= numXLabels; i++) {
- var val = Math.ceil((scores.length / numXLabels) * i);
- currX = (graphWidth / numXLabels) * i + leftPadding;
- ctx.fillText(val, currX, base + (bottomPadding * 0.3));
- }
- }
-
- // Mark y axis
- ctx.textAlign = "right";
- var currY;
- if (numYLabels >= max) {
- // Mark every increment
- for (i = 0; i <= max; i++) {
- currY = base - (i / max * graphHeight) + fontSize / 3;
- ctx.fillText(i, leftPadding * 0.8, currY);
- }
- } else {
- // Mark some increments
- for (i = 0; i <= numYLabels; i++) {
- val = Math.ceil((max / numYLabels) * i);
- currY = base - (val / max * graphHeight) + fontSize / 3;
- ctx.fillText(val, leftPadding * 0.8, currY);
- }
- }
-
- // Label x axis
- if (xAxisLabel) {
- ctx.textAlign = "center";
- ctx.fillText(xAxisLabel, graphWidth / 2 + leftPadding, base + bottomPadding * 0.8);
- }
-
- // Label y axis
- if (yAxisLabel) {
- ctx.save();
- var x = leftPadding * 0.3,
- y = graphHeight / 2 + topPadding;
- ctx.translate(x, y);
- ctx.rotate(-Math.PI / 2);
- ctx.textAlign = "center";
- ctx.fillText(yAxisLabel, 0, 0);
- ctx.restore();
- }
- },
-
- drawScaleBar: function(canvas, score, max, markings) {
- // Bar properties
- var ctx = canvas.getContext("2d"),
- leftPadding = canvas.width * 0.01,
- rightPadding = canvas.width * 0.01,
- topPadding = canvas.height * 0.1,
- bottomPadding = canvas.height * 0.3,
- barHeight = canvas.height - topPadding - bottomPadding,
- barWidth = canvas.width - leftPadding - rightPadding;
-
- // Scale properties
- var proportion = score / max;
-
- // Draw bar outline
- ctx.strokeRect(leftPadding, topPadding, barWidth, barHeight);
-
- // Shade in up to proportion
- var grad = ctx.createLinearGradient(leftPadding, 0, barWidth + leftPadding, 0);
- grad.addColorStop(0, "green");
- grad.addColorStop(0.5, "gold");
- grad.addColorStop(1, "red");
- ctx.fillStyle = grad;
- ctx.fillRect(leftPadding, topPadding, barWidth * proportion, barHeight);
-
- // Add markings
- var x0, y0, x1, y1;
- ctx.fillStyle = "black";
- ctx.textAlign = "center";
- ctx.font = "13px Arial";
- for (var i = 0; i < markings.length; i++) {
- // Draw min line down
- x0 = barWidth / max * markings[i].min + leftPadding;
- y0 = topPadding + barHeight + (bottomPadding * 0.1);
- x1 = x0;
- y1 = topPadding + barHeight + (bottomPadding * 0.3);
- CanvasComponents.drawLine(ctx, x0, y0, x1, y1);
-
- // Draw max line down
- x0 = barWidth / max * markings[i].max + leftPadding;
- x1 = x0;
- CanvasComponents.drawLine(ctx, x0, y0, x1, y1);
-
- // Join min and max lines
- x0 = barWidth / max * markings[i].min + leftPadding;
- y0 = topPadding + barHeight + (bottomPadding * 0.3);
- x1 = barWidth / max * markings[i].max + leftPadding;
- y1 = y0;
- CanvasComponents.drawLine(ctx, x0, y0, x1, y1);
-
- // Add label
- if (markings[i].max >= max * 0.9) {
- ctx.textAlign = "right";
- x0 = x1;
- } else if (markings[i].max <= max * 0.1) {
- ctx.textAlign = "left";
- } else {
- x0 = x0 + (x1 - x0) / 2;
- }
- y0 = topPadding + barHeight + (bottomPadding * 0.8);
- ctx.fillText(markings[i].label, x0, y0);
- }
- },
-
-};
-
-export default CanvasComponents;
diff --git a/src/web/index.js b/src/web/index.js
index 9e7f045d..b15488c5 100755
--- a/src/web/index.js
+++ b/src/web/index.js
@@ -13,7 +13,7 @@ import "bootstrap";
import "bootstrap-switch";
import "bootstrap-colorpicker";
import moment from "moment-timezone";
-import CanvasComponents from "../core/vendor/canvascomponents.js";
+import * as CanvasComponents from "../core/lib/CanvasComponents";
// CyberChef
import App from "./App";