From 84df055888477a48aa67697d0d10e6d986278beb Mon Sep 17 00:00:00 2001 From: n1474335 Date: Wed, 16 May 2018 11:39:30 +0100 Subject: [PATCH] ESM: Ported MS and Entropy operations --- src/core/Chef.mjs | 6 + src/core/Operation.mjs | 3 +- src/core/Recipe.mjs | 17 +- src/core/lib/CanvasComponents.mjs | 204 ++++++++++++++++ src/core/operations/ChiSquare.mjs | 53 +++++ src/core/operations/Entropy.mjs | 96 ++++++++ src/core/operations/FrequencyDistribution.mjs | 110 +++++++++ .../operations/MicrosoftScriptDecoder.mjs | 217 ++++++++++++++++++ src/core/vendor/canvascomponents.js | 186 --------------- src/web/index.js | 2 +- 10 files changed, 705 insertions(+), 189 deletions(-) create mode 100755 src/core/lib/CanvasComponents.mjs create mode 100644 src/core/operations/ChiSquare.mjs create mode 100644 src/core/operations/Entropy.mjs create mode 100644 src/core/operations/FrequencyDistribution.mjs create mode 100644 src/core/operations/MicrosoftScriptDecoder.mjs delete mode 100755 src/core/vendor/canvascomponents.js diff --git a/src/core/Chef.mjs b/src/core/Chef.mjs index 7917247..a730237 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 e0b587c..297aef9 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 a1364f2..4ba07ed 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 0000000..04664c3 --- /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 0000000..89cb221 --- /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 0000000..5accf07 --- /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 0000000..bf82f1d --- /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 0000000..cc9d407 --- /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 f6ea053..0000000 --- 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 9e7f045..b15488c 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";