소스 검색

Merge branch 'module-charts' of https://github.com/artemisbot/CyberChef into artemisbot-module-charts

n1474335 6 년 전
부모
커밋
3d80d66925

+ 367 - 98
package-lock.json

@@ -1374,6 +1374,12 @@
         "validator": "^9.4.1"
       },
       "dependencies": {
+        "acorn": {
+          "version": "5.7.3",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
+          "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==",
+          "dev": true
+        },
         "ansi-styles": {
           "version": "3.2.1",
           "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@@ -1394,6 +1400,46 @@
             "supports-color": "^5.3.0"
           }
         },
+        "jsdom": {
+          "version": "11.12.0",
+          "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz",
+          "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==",
+          "dev": true,
+          "requires": {
+            "abab": "^2.0.0",
+            "acorn": "^5.5.3",
+            "acorn-globals": "^4.1.0",
+            "array-equal": "^1.0.0",
+            "cssom": ">= 0.3.2 < 0.4.0",
+            "cssstyle": "^1.0.0",
+            "data-urls": "^1.0.0",
+            "domexception": "^1.0.1",
+            "escodegen": "^1.9.1",
+            "html-encoding-sniffer": "^1.0.2",
+            "left-pad": "^1.3.0",
+            "nwsapi": "^2.0.7",
+            "parse5": "4.0.0",
+            "pn": "^1.1.0",
+            "request": "^2.87.0",
+            "request-promise-native": "^1.0.5",
+            "sax": "^1.2.4",
+            "symbol-tree": "^3.2.2",
+            "tough-cookie": "^2.3.4",
+            "w3c-hr-time": "^1.0.1",
+            "webidl-conversions": "^4.0.2",
+            "whatwg-encoding": "^1.0.3",
+            "whatwg-mimetype": "^2.1.0",
+            "whatwg-url": "^6.4.1",
+            "ws": "^5.2.0",
+            "xml-name-validator": "^3.0.0"
+          }
+        },
+        "parse5": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
+          "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==",
+          "dev": true
+        },
         "supports-color": {
           "version": "5.5.0",
           "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -1408,6 +1454,26 @@
           "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz",
           "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==",
           "dev": true
+        },
+        "whatwg-url": {
+          "version": "6.5.0",
+          "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz",
+          "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==",
+          "dev": true,
+          "requires": {
+            "lodash.sortby": "^4.7.0",
+            "tr46": "^1.0.1",
+            "webidl-conversions": "^4.0.2"
+          }
+        },
+        "ws": {
+          "version": "5.2.2",
+          "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
+          "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
+          "dev": true,
+          "requires": {
+            "async-limiter": "~1.0.0"
+          }
         }
       }
     },
@@ -3408,15 +3474,15 @@
       "dev": true
     },
     "cssom": {
-      "version": "0.3.4",
-      "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.4.tgz",
-      "integrity": "sha512-+7prCSORpXNeR4/fUP3rL+TzqtiFfhMvTd7uEqMdgPvLPt4+uzFUeufx5RHjGTACCargg/DiEt/moMQmvnfkog==",
+      "version": "0.3.6",
+      "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz",
+      "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==",
       "dev": true
     },
     "cssstyle": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.1.1.tgz",
-      "integrity": "sha512-364AI1l/M5TYcFH83JnOH/pSqgaNnKmYgKrm0didZMGKWjQB60dymwWy1rKUgL3J1ffdq9xVi2yGLHdSjjSNog==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.2.1.tgz",
+      "integrity": "sha512-7DYm8qe+gPx/h77QlCyFmX80+fGaE/6A/Ekl0zaszYOubvySO2saYFdQ78P29D0UsULxFKCetDGNaNRUdSF+2A==",
       "dev": true,
       "requires": {
         "cssom": "0.3.x"
@@ -3448,6 +3514,266 @@
       "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=",
       "dev": true
     },
+    "d3": {
+      "version": "4.13.0",
+      "resolved": "https://registry.npmjs.org/d3/-/d3-4.13.0.tgz",
+      "integrity": "sha512-l8c4+0SldjVKLaE2WG++EQlqD7mh/dmQjvi2L2lKPadAVC+TbJC4ci7Uk9bRi+To0+ansgsS0iWfPjD7DBy+FQ==",
+      "requires": {
+        "d3-array": "1.2.1",
+        "d3-axis": "1.0.8",
+        "d3-brush": "1.0.4",
+        "d3-chord": "1.0.4",
+        "d3-collection": "1.0.4",
+        "d3-color": "1.0.3",
+        "d3-dispatch": "1.0.3",
+        "d3-drag": "1.2.1",
+        "d3-dsv": "1.0.8",
+        "d3-ease": "1.0.3",
+        "d3-force": "1.1.0",
+        "d3-format": "1.2.2",
+        "d3-geo": "1.9.1",
+        "d3-hierarchy": "1.1.5",
+        "d3-interpolate": "1.1.6",
+        "d3-path": "1.0.5",
+        "d3-polygon": "1.0.3",
+        "d3-quadtree": "1.0.3",
+        "d3-queue": "3.0.7",
+        "d3-random": "1.1.0",
+        "d3-request": "1.0.6",
+        "d3-scale": "1.0.7",
+        "d3-selection": "1.3.0",
+        "d3-shape": "1.2.0",
+        "d3-time": "1.0.8",
+        "d3-time-format": "2.1.1",
+        "d3-timer": "1.0.7",
+        "d3-transition": "1.1.1",
+        "d3-voronoi": "1.1.2",
+        "d3-zoom": "1.7.1"
+      }
+    },
+    "d3-array": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.1.tgz",
+      "integrity": "sha512-CyINJQ0SOUHojDdFDH4JEM0552vCR1utGyLHegJHyYH0JyCpSeTPxi4OBqHMA2jJZq4NH782LtaJWBImqI/HBw=="
+    },
+    "d3-axis": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.8.tgz",
+      "integrity": "sha1-MacFoLU15ldZ3hQXOjGTMTfxjvo="
+    },
+    "d3-brush": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.0.4.tgz",
+      "integrity": "sha1-AMLyOAGfJPbAoZSibUGhUw/+e8Q=",
+      "requires": {
+        "d3-dispatch": "1",
+        "d3-drag": "1",
+        "d3-interpolate": "1",
+        "d3-selection": "1",
+        "d3-transition": "1"
+      }
+    },
+    "d3-chord": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.4.tgz",
+      "integrity": "sha1-fexPC6iG9xP+ERxF92NBT290yiw=",
+      "requires": {
+        "d3-array": "1",
+        "d3-path": "1"
+      }
+    },
+    "d3-collection": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.4.tgz",
+      "integrity": "sha1-NC39EoN8kJdPM/HMCnha6lcNzcI="
+    },
+    "d3-color": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.0.3.tgz",
+      "integrity": "sha1-vHZD/KjlOoNH4vva/6I2eWtYUJs="
+    },
+    "d3-dispatch": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.3.tgz",
+      "integrity": "sha1-RuFJHqqbWMNY/OW+TovtYm54cfg="
+    },
+    "d3-drag": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.1.tgz",
+      "integrity": "sha512-Cg8/K2rTtzxzrb0fmnYOUeZHvwa4PHzwXOLZZPwtEs2SKLLKLXeYwZKBB+DlOxUvFmarOnmt//cU4+3US2lyyQ==",
+      "requires": {
+        "d3-dispatch": "1",
+        "d3-selection": "1"
+      }
+    },
+    "d3-dsv": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.0.8.tgz",
+      "integrity": "sha512-IVCJpQ+YGe3qu6odkPQI0KPqfxkhbP/oM1XhhE/DFiYmcXKfCRub4KXyiuehV1d4drjWVXHUWx4gHqhdZb6n/A==",
+      "requires": {
+        "commander": "2",
+        "iconv-lite": "0.4",
+        "rw": "1"
+      }
+    },
+    "d3-ease": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.3.tgz",
+      "integrity": "sha1-aL+8NJM4o4DETYrMT7wzBKotjA4="
+    },
+    "d3-force": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.1.0.tgz",
+      "integrity": "sha512-2HVQz3/VCQs0QeRNZTYb7GxoUCeb6bOzMp/cGcLa87awY9ZsPvXOGeZm0iaGBjXic6I1ysKwMn+g+5jSAdzwcg==",
+      "requires": {
+        "d3-collection": "1",
+        "d3-dispatch": "1",
+        "d3-quadtree": "1",
+        "d3-timer": "1"
+      }
+    },
+    "d3-format": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.2.2.tgz",
+      "integrity": "sha512-zH9CfF/3C8zUI47nsiKfD0+AGDEuM8LwBIP7pBVpyR4l/sKkZqITmMtxRp04rwBrlshIZ17XeFAaovN3++wzkw=="
+    },
+    "d3-geo": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.9.1.tgz",
+      "integrity": "sha512-l9wL/cEQkyZQYXw3xbmLsH3eQ5ij+icNfo4r0GrLa5rOCZR/e/3am45IQ0FvQ5uMsv+77zBRunLc9ufTWSQYFA==",
+      "requires": {
+        "d3-array": "1"
+      }
+    },
+    "d3-hexbin": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/d3-hexbin/-/d3-hexbin-0.2.2.tgz",
+      "integrity": "sha1-nFg32s/UcasFM3qeke8Qv8T5iDE="
+    },
+    "d3-hierarchy": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.5.tgz",
+      "integrity": "sha1-ochFxC+Eoga88cAcAQmOpN2qeiY="
+    },
+    "d3-interpolate": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.1.6.tgz",
+      "integrity": "sha512-mOnv5a+pZzkNIHtw/V6I+w9Lqm9L5bG3OTXPM5A+QO0yyVMQ4W1uZhR+VOJmazaOZXri2ppbiZ5BUNWT0pFM9A==",
+      "requires": {
+        "d3-color": "1"
+      }
+    },
+    "d3-path": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.5.tgz",
+      "integrity": "sha1-JB6xhJvZ6egCHA0KeZ+KDo5EF2Q="
+    },
+    "d3-polygon": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.3.tgz",
+      "integrity": "sha1-FoiOkCZGCTPysXllKtN4Ik04LGI="
+    },
+    "d3-quadtree": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.3.tgz",
+      "integrity": "sha1-rHmH4+I/6AWpkPKOG1DTj8uCJDg="
+    },
+    "d3-queue": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/d3-queue/-/d3-queue-3.0.7.tgz",
+      "integrity": "sha1-yTouVLQXwJWRKdfXP2z31Ckudhg="
+    },
+    "d3-random": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.0.tgz",
+      "integrity": "sha1-ZkLlBsb6OmSFldKyRpeIqNElKdM="
+    },
+    "d3-request": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/d3-request/-/d3-request-1.0.6.tgz",
+      "integrity": "sha512-FJj8ySY6GYuAJHZMaCQ83xEYE4KbkPkmxZ3Hu6zA1xxG2GD+z6P+Lyp+zjdsHf0xEbp2xcluDI50rCS855EQ6w==",
+      "requires": {
+        "d3-collection": "1",
+        "d3-dispatch": "1",
+        "d3-dsv": "1",
+        "xmlhttprequest": "1"
+      }
+    },
+    "d3-scale": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-1.0.7.tgz",
+      "integrity": "sha512-KvU92czp2/qse5tUfGms6Kjig0AhHOwkzXG0+PqIJB3ke0WUv088AHMZI0OssO9NCkXt4RP8yju9rpH8aGB7Lw==",
+      "requires": {
+        "d3-array": "^1.2.0",
+        "d3-collection": "1",
+        "d3-color": "1",
+        "d3-format": "1",
+        "d3-interpolate": "1",
+        "d3-time": "1",
+        "d3-time-format": "2"
+      }
+    },
+    "d3-selection": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.3.0.tgz",
+      "integrity": "sha512-qgpUOg9tl5CirdqESUAu0t9MU/t3O9klYfGfyKsXEmhyxyzLpzpeh08gaxBUTQw1uXIOkr/30Ut2YRjSSxlmHA=="
+    },
+    "d3-shape": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.2.0.tgz",
+      "integrity": "sha1-RdAVOPBkuv0F6j1tLLdI/YxB93c=",
+      "requires": {
+        "d3-path": "1"
+      }
+    },
+    "d3-time": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.8.tgz",
+      "integrity": "sha512-YRZkNhphZh3KcnBfitvF3c6E0JOFGikHZ4YqD+Lzv83ZHn1/u6yGenRU1m+KAk9J1GnZMnKcrtfvSktlA1DXNQ=="
+    },
+    "d3-time-format": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.1.tgz",
+      "integrity": "sha512-8kAkymq2WMfzW7e+s/IUNAtN/y3gZXGRrdGfo6R8NKPAA85UBTxZg5E61bR6nLwjPjj4d3zywSQe1CkYLPFyrw==",
+      "requires": {
+        "d3-time": "1"
+      }
+    },
+    "d3-timer": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.7.tgz",
+      "integrity": "sha512-vMZXR88XujmG/L5oB96NNKH5lCWwiLM/S2HyyAQLcjWJCloK5shxta4CwOFYLZoY3AWX73v8Lgv4cCAdWtRmOA=="
+    },
+    "d3-transition": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.1.1.tgz",
+      "integrity": "sha512-xeg8oggyQ+y5eb4J13iDgKIjUcEfIOZs2BqV/eEmXm2twx80wTzJ4tB4vaZ5BKfz7XsI/DFmQL5me6O27/5ykQ==",
+      "requires": {
+        "d3-color": "1",
+        "d3-dispatch": "1",
+        "d3-ease": "1",
+        "d3-interpolate": "1",
+        "d3-selection": "^1.1.0",
+        "d3-timer": "1"
+      }
+    },
+    "d3-voronoi": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.2.tgz",
+      "integrity": "sha1-Fodmfo8TotFYyAwUgMWinLDYlzw="
+    },
+    "d3-zoom": {
+      "version": "1.7.1",
+      "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.7.1.tgz",
+      "integrity": "sha512-sZHQ55DGq5BZBFGnRshUT8tm2sfhPHFnOlmPbbwTkAoPeVdRTkB4Xsf9GCY0TSHrTD8PeJPZGmP/TpGicwJDJQ==",
+      "requires": {
+        "d3-dispatch": "1",
+        "d3-drag": "1",
+        "d3-interpolate": "1",
+        "d3-selection": "1",
+        "d3-transition": "1"
+      }
+    },
     "dashdash": {
       "version": "1.14.1",
       "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -3472,19 +3798,6 @@
         "abab": "^2.0.0",
         "whatwg-mimetype": "^2.2.0",
         "whatwg-url": "^7.0.0"
-      },
-      "dependencies": {
-        "whatwg-url": {
-          "version": "7.0.0",
-          "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz",
-          "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==",
-          "dev": true,
-          "requires": {
-            "lodash.sortby": "^4.7.0",
-            "tr46": "^1.0.1",
-            "webidl-conversions": "^4.0.2"
-          }
-        }
       }
     },
     "datauri": {
@@ -6716,7 +7029,6 @@
       "version": "0.4.24",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
       "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
-      "dev": true,
       "requires": {
         "safer-buffer": ">= 2.1.2 < 3"
       }
@@ -7520,48 +7832,6 @@
       "integrity": "sha1-hCRCjVtWOtjFx/vsB5uaiwnI3Po=",
       "dev": true
     },
-    "jsdom": {
-      "version": "11.12.0",
-      "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz",
-      "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==",
-      "dev": true,
-      "requires": {
-        "abab": "^2.0.0",
-        "acorn": "^5.5.3",
-        "acorn-globals": "^4.1.0",
-        "array-equal": "^1.0.0",
-        "cssom": ">= 0.3.2 < 0.4.0",
-        "cssstyle": "^1.0.0",
-        "data-urls": "^1.0.0",
-        "domexception": "^1.0.1",
-        "escodegen": "^1.9.1",
-        "html-encoding-sniffer": "^1.0.2",
-        "left-pad": "^1.3.0",
-        "nwsapi": "^2.0.7",
-        "parse5": "4.0.0",
-        "pn": "^1.1.0",
-        "request": "^2.87.0",
-        "request-promise-native": "^1.0.5",
-        "sax": "^1.2.4",
-        "symbol-tree": "^3.2.2",
-        "tough-cookie": "^2.3.4",
-        "w3c-hr-time": "^1.0.1",
-        "webidl-conversions": "^4.0.2",
-        "whatwg-encoding": "^1.0.3",
-        "whatwg-mimetype": "^2.1.0",
-        "whatwg-url": "^6.4.1",
-        "ws": "^5.2.0",
-        "xml-name-validator": "^3.0.0"
-      },
-      "dependencies": {
-        "acorn": {
-          "version": "5.7.3",
-          "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
-          "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==",
-          "dev": true
-        }
-      }
-    },
     "jsesc": {
       "version": "2.5.2",
       "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@@ -9009,6 +9279,11 @@
         }
       }
     },
+    "nodom": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/nodom/-/nodom-2.2.0.tgz",
+      "integrity": "sha512-+W3jlsobV3NNkO15xQXkWoboeq1RPa/SKi8NMHmWF33SCMX4ALcM5dpPLEnUs69Gu+uZoCX9wcWXy866LXvd8w=="
+    },
     "nomnom": {
       "version": "1.5.2",
       "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.5.2.tgz",
@@ -9119,9 +9394,9 @@
       "integrity": "sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ=="
     },
     "nwsapi": {
-      "version": "2.0.9",
-      "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.0.9.tgz",
-      "integrity": "sha512-nlWFSCTYQcHk/6A9FFnfhKc14c3aFhfdNBXgo8Qgi9QTBu/qg3Ww+Uiz9wMzXd1T8GFxPc2QIHB6Qtf2XFryFQ==",
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.1.tgz",
+      "integrity": "sha512-T5GaA1J/d34AC8mkrFD2O0DR17kwJ702ZOtJOsS8RpbsQZVOC2/xYFb1i/cw+xdM54JIlMuojjDOYct8GIWtwg==",
       "dev": true
     },
     "oauth-sign": {
@@ -9601,12 +9876,6 @@
         "error-ex": "^1.2.0"
       }
     },
-    "parse5": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
-      "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==",
-      "dev": true
-    },
     "parseurl": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
@@ -10826,23 +11095,23 @@
       }
     },
     "request-promise-core": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz",
-      "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=",
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz",
+      "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==",
       "dev": true,
       "requires": {
-        "lodash": "^4.13.1"
+        "lodash": "^4.17.11"
       }
     },
     "request-promise-native": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz",
-      "integrity": "sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz",
+      "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==",
       "dev": true,
       "requires": {
-        "request-promise-core": "1.1.1",
-        "stealthy-require": "^1.1.0",
-        "tough-cookie": ">=2.3.3"
+        "request-promise-core": "1.1.2",
+        "stealthy-require": "^1.1.1",
+        "tough-cookie": "^2.3.3"
       }
     },
     "require-directory": {
@@ -11000,6 +11269,11 @@
         "aproba": "^1.1.1"
       }
     },
+    "rw": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+      "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q="
+    },
     "rxjs": {
       "version": "6.4.0",
       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz",
@@ -11032,8 +11306,7 @@
     "safer-buffer": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
-      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
-      "dev": true
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
     },
     "sanitize-html": {
       "version": "1.19.1",
@@ -13728,15 +14001,15 @@
       }
     },
     "whatwg-mimetype": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz",
-      "integrity": "sha512-5YSO1nMd5D1hY3WzAQV3PzZL83W3YeyR1yW9PcH26Weh1t+Vzh9B6XkDh7aXm83HBZ4nSMvkjvN2H2ySWIvBgw==",
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz",
+      "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==",
       "dev": true
     },
     "whatwg-url": {
-      "version": "6.5.0",
-      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz",
-      "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz",
+      "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==",
       "dev": true,
       "requires": {
         "lodash.sortby": "^4.7.0",
@@ -13870,15 +14143,6 @@
         "mkdirp": "^0.5.1"
       }
     },
-    "ws": {
-      "version": "5.2.2",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
-      "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
-      "dev": true,
-      "requires": {
-        "async-limiter": "~1.0.0"
-      }
-    },
     "xhr": {
       "version": "2.5.0",
       "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.5.0.tgz",
@@ -13934,6 +14198,11 @@
       "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz",
       "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk="
     },
+    "xmlhttprequest": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz",
+      "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw="
+    },
     "xpath": {
       "version": "0.0.27",
       "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz",

+ 3 - 0
package.json

@@ -92,6 +92,8 @@
     "crypto-api": "^0.8.3",
     "crypto-js": "^3.1.9-1",
     "ctph.js": "0.0.5",
+    "d3": "^4.9.1",
+    "d3-hexbin": "^0.2.2",
     "diff": "^3.5.0",
     "es6-promisify": "^6.0.1",
     "escodegen": "^1.11.0",
@@ -120,6 +122,7 @@
     "ngeohash": "^0.6.3",
     "node-forge": "^0.7.6",
     "node-md6": "^0.1.0",
+    "nodom": "^2.2.0",
     "notepack.io": "^2.2.0",
     "nwmatcher": "^1.4.4",
     "otp": "^0.1.3",

+ 1 - 0
src/core/Utils.mjs

@@ -1027,6 +1027,7 @@ class Utils {
             "Comma":         ",",
             "Semi-colon":    ";",
             "Colon":         ":",
+            "Tab":           "\t",
             "Line feed":     "\n",
             "CRLF":          "\r\n",
             "Forward slash": "/",

+ 4 - 0
src/core/config/Categories.json

@@ -366,6 +366,10 @@
             "Remove EXIF",
             "Extract EXIF",
             "Split Colour Channels",
+            "Hex Density chart",
+            "Scatter chart",
+            "Series chart",
+            "Heatmap chart",
             "Rotate Image",
             "Resize Image",
             "Blur Image",

+ 175 - 0
src/core/lib/Charts.mjs

@@ -0,0 +1,175 @@
+/**
+ * @author tlwr [toby@toby.codes]
+ * @author Matt C [me@mitt.dev]
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+
+import OperationError from "../errors/OperationError";
+
+ /**
+ * @constant
+ * @default
+ */
+export const RECORD_DELIMITER_OPTIONS = ["Line feed", "CRLF"];
+
+
+/**
+ * @constant
+ * @default
+ */
+export const FIELD_DELIMITER_OPTIONS = ["Space", "Comma", "Semi-colon", "Colon", "Tab"];
+
+
+/**
+ * Default from colour
+ *
+ * @constant
+ * @default
+ */
+export const COLOURS = {
+    min: "white",
+    max: "black"
+};
+
+
+/**
+ * Gets values from input for a plot.
+ *
+ * @param {string} input
+ * @param {string} recordDelimiter
+ * @param {string} fieldDelimiter
+ * @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
+ * @returns {Object[]}
+ */
+export function getValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded, length) {
+    let headings;
+    const values = [];
+
+    input
+        .split(recordDelimiter)
+        .forEach((row, rowIndex) => {
+            const split = row.split(fieldDelimiter);
+            if (split.length !== length) throw new OperationError(`Each row must have length ${length}.`);
+
+            if (columnHeadingsAreIncluded && rowIndex === 0) {
+                headings = split;
+            } else {
+                values.push(split);
+            }
+        });
+    return { headings, values};
+}
+
+
+/**
+ * Gets values from input for a scatter plot.
+ *
+ * @param {string} input
+ * @param {string} recordDelimiter
+ * @param {string} fieldDelimiter
+ * @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
+ * @returns {Object[]}
+ */
+export function getScatterValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
+    let { headings, values } = getValues(
+        input,
+        recordDelimiter, fieldDelimiter,
+        columnHeadingsAreIncluded,
+        2
+    );
+
+    if (headings) {
+        headings = {x: headings[0], y: headings[1]};
+    }
+
+    values = values.map(row => {
+        const x = parseFloat(row[0], 10),
+            y = parseFloat(row[1], 10);
+
+        if (Number.isNaN(x)) throw new OperationError("Values must be numbers in base 10.");
+        if (Number.isNaN(y)) throw new OperationError("Values must be numbers in base 10.");
+
+        return [x, y];
+    });
+
+    return { headings, values };
+}
+
+/**
+ * Gets values from input for a scatter plot with colour from the third column.
+ *
+ * @param {string} input
+ * @param {string} recordDelimiter
+ * @param {string} fieldDelimiter
+ * @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
+ * @returns {Object[]}
+ */
+export function getScatterValuesWithColour(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
+    let { headings, values } = getValues(
+        input,
+        recordDelimiter, fieldDelimiter,
+        columnHeadingsAreIncluded,
+        3
+    );
+
+    if (headings) {
+        headings = {x: headings[0], y: headings[1]};
+    }
+
+    values = values.map(row => {
+        const x = parseFloat(row[0], 10),
+            y = parseFloat(row[1], 10),
+            colour = row[2];
+
+        if (Number.isNaN(x)) throw new OperationError("Values must be numbers in base 10.");
+        if (Number.isNaN(y)) throw new OperationError("Values must be numbers in base 10.");
+
+        return [x, y, colour];
+    });
+
+    return { headings, values };
+}
+
+/**
+ * Gets values from input for a time series plot.
+ *
+ * @param {string} input
+ * @param {string} recordDelimiter
+ * @param {string} fieldDelimiter
+ * @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
+ * @returns {Object[]}
+ */
+export function getSeriesValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
+    const { values } = getValues(
+        input,
+        recordDelimiter, fieldDelimiter,
+        false,
+        3
+    );
+
+    let xValues = new Set();
+    const series = {};
+
+    values.forEach(row => {
+        const serie = row[0],
+            xVal = row[1],
+            val = parseFloat(row[2], 10);
+
+        if (Number.isNaN(val)) throw new OperationError("Values must be numbers in base 10.");
+
+        xValues.add(xVal);
+        if (typeof series[serie] === "undefined") series[serie] = {};
+        series[serie][xVal] = val;
+    });
+
+    xValues = new Array(...xValues);
+
+    const seriesList = [];
+    for (const seriesName in series) {
+        const serie = series[seriesName];
+        seriesList.push({name: seriesName, data: serie});
+    }
+
+    return { xValues, series: seriesList };
+}

+ 41 - 0
src/core/operations/HTMLToText.mjs

@@ -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 a HTML ouput 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;

+ 267 - 0
src/core/operations/HeatmapChart.mjs

@@ -0,0 +1,267 @@
+/**
+ * @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 - 0
src/core/operations/HexDensityChart.mjs

@@ -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;

+ 200 - 0
src/core/operations/ScatterChart.mjs

@@ -0,0 +1,200 @@
+/**
+ * @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://en.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 - 0
src/core/operations/SeriesChart.mjs

@@ -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;

+ 1 - 0
tests/operations/index.mjs

@@ -33,6 +33,7 @@ import "./tests/BitwiseOp";
 import "./tests/ByteRepr";
 import "./tests/CartesianProduct";
 import "./tests/CharEnc";
+import "./tests/Charts";
 import "./tests/Checksum";
 import "./tests/Ciphers";
 import "./tests/Code";

+ 55 - 0
tests/operations/tests/Charts.mjs

@@ -0,0 +1,55 @@
+/**
+ * Chart tests.
+ *
+ * @author Matt C [me@mitt.dev]
+ * @copyright Crown Copyright 2019
+ * @license Apache-2.0
+ */
+import TestRegister from "../TestRegister";
+
+TestRegister.addTests([
+    {
+        name: "Scatter chart",
+        input: "100 100\n200 200\n300 300\n400 400\n500 500",
+        expectedMatch: /^<svg width/,
+        recipeConfig: [
+            { 
+                "op": "Scatter chart",
+                "args": ["Line feed", "Space", false, "time", "stress", "black", 5, false]
+            }
+        ],
+    },
+    {
+        name: "Hex density chart",
+        input: "100 100\n200 200\n300 300\n400 400\n500 500",
+        expectedMatch: /^<svg width/,
+        recipeConfig: [
+            { 
+                "op": "Hex Density chart",
+                "args": ["Line feed", "Space", 25, 15, true, "", "", true, "white", "black", true] 
+            }
+        ],
+    },
+    {
+        name: "Series chart",
+        input: "100 100 100\n200 200 200\n300 300 300\n400 400 400\n500 500 500",
+        expectedMatch: /^<svg width/,
+        recipeConfig: [
+            { 
+                "op": "Series chart",
+                "args": ["Line feed", "Space", "", 1, "mediumseagreen, dodgerblue, tomato"] 
+            }
+        ],
+    },
+    {
+        name: "Heatmap chart",
+        input: "100 100\n200 200\n300 300\n400 400\n500 500",
+        expectedMatch: /^<svg width/,
+        recipeConfig: [
+            { 
+                "op": "Heatmap chart",
+                "args": ["Line feed", "Space", 25, 25, true, "", "", false, "white", "black"]
+            }
+        ],
+    },
+]);

+ 4 - 1
webpack.config.js

@@ -137,7 +137,10 @@ module.exports = {
         ],
     },
     node: {
-        fs: "empty"
+        fs: "empty",
+        "child_process": "empty",
+        net: "empty",
+        tls: "empty"
     },
     performance: {
         hints: false