Bläddra i källkod

Initial commit

Michael Zitzmann 5 år sedan
incheckning
5f947ad26a
71 ändrade filer med 3662 tillägg och 0 borttagningar
  1. 27 0
      .babelrc.js
  2. 14 0
      .editorconfig
  3. 2 0
      .eslintignore
  4. 15 0
      .eslintrc.json
  5. 6 0
      .gitignore
  6. 82 0
      README.md
  7. 0 0
      assets/blob-stream.min.js
  8. 0 0
      assets/pdfkit.min.js
  9. 36 0
      assets/shared/css/main.css
  10. BIN
      assets/shared/fonts/lora/Lora-Regular.ttf
  11. 44 0
      assets/shared/fonts/lora/SIL Open Font License.txt
  12. BIN
      assets/shared/fonts/montserrat/Montserrat-SemiBold.otf
  13. 43 0
      assets/shared/fonts/montserrat/SIL Open Font License.txt
  14. 172 0
      aurelia_project/aurelia.json
  15. 4 0
      aurelia_project/environments/dev.js
  16. 4 0
      aurelia_project/environments/prod.js
  17. 4 0
      aurelia_project/environments/stage.js
  18. 44 0
      aurelia_project/generators/attribute.js
  19. 4 0
      aurelia_project/generators/attribute.json
  20. 41 0
      aurelia_project/generators/binding-behavior.js
  21. 4 0
      aurelia_project/generators/binding-behavior.json
  22. 48 0
      aurelia_project/generators/element.js
  23. 4 0
      aurelia_project/generators/element.json
  24. 73 0
      aurelia_project/generators/generator.js
  25. 4 0
      aurelia_project/generators/generator.json
  26. 41 0
      aurelia_project/generators/task.js
  27. 4 0
      aurelia_project/generators/task.json
  28. 41 0
      aurelia_project/generators/value-converter.js
  29. 4 0
      aurelia_project/generators/value-converter.json
  30. 24 0
      aurelia_project/tasks/build.js
  31. 11 0
      aurelia_project/tasks/build.json
  32. 12 0
      aurelia_project/tasks/process-css.js
  33. 10 0
      aurelia_project/tasks/process-markup.js
  34. 67 0
      aurelia_project/tasks/run.js
  35. 16 0
      aurelia_project/tasks/run.json
  36. 11 0
      aurelia_project/tasks/test.js
  37. 16 0
      aurelia_project/tasks/test.json
  38. 32 0
      aurelia_project/tasks/transpile.js
  39. 11 0
      fact-box-editor.html
  40. BIN
      favicon.ico
  41. 1 0
      fonts
  42. 49 0
      index.html
  43. 7 0
      jsconfig.json
  44. 40 0
      karma.conf.js
  45. 83 0
      package.json
  46. 36 0
      scripts/require.js
  47. 391 0
      scripts/text.js
  48. 190 0
      src/app.html
  49. 364 0
      src/app.js
  50. 230 0
      src/configuration.js
  51. 2 0
      src/d3custom.js
  52. 4 0
      src/environment.js
  53. 26 0
      src/main.js
  54. 36 0
      src/messages.js
  55. 5 0
      src/number-format.js
  56. 168 0
      src/pdfWorker.js
  57. BIN
      src/resources/Verdana Bold.ttf
  58. BIN
      src/resources/Verdana.ttf
  59. 3 0
      src/resources/index.js
  60. 3 0
      src/sampler-visual.html
  61. 209 0
      src/sampler-visual.js
  62. 58 0
      src/sampler.js
  63. 17 0
      src/scss/_custom.scss
  64. 152 0
      src/scss/_globals.scss
  65. 1 0
      src/scss/font-awesome
  66. 116 0
      src/scss/partials/_colours.scss
  67. 145 0
      src/scss/partials/_form_elements.scss
  68. 260 0
      src/scss/styles.scss
  69. 81 0
      test/aurelia-karma.js
  70. 7 0
      test/unit/app.spec.js
  71. 3 0
      test/unit/setup.js

+ 27 - 0
.babelrc.js

@@ -0,0 +1,27 @@
+module.exports = api => {
+  api.cache.using(() => {
+    // cache based on the two env vars
+    return 'babel:' + process.env.BABEL_TARGET +
+      ' protractor:' + process.env.IN_PROTRACTOR;
+  });
+
+  return {
+    "plugins": [
+      ['@babel/plugin-proposal-decorators', { legacy: true }],
+      ['@babel/plugin-proposal-class-properties', { loose: true }]
+    ],
+    "presets": [
+      [
+        "@babel/preset-env", {
+          "targets": process.env.BABEL_TARGET === 'node' ? {
+            "node": process.env.IN_PROTRACTOR ? '6' : 'current'
+          } : {
+            "browsers": [ "last 2 versions" ]
+          },
+          "loose": true,
+          "modules": process.env.BABEL_TARGET === 'node' ? 'commonjs' : false
+        }
+      ]
+    ]
+  }
+}

+ 14 - 0
.editorconfig

@@ -0,0 +1,14 @@
+# EditorConfig is awesome: http://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+
+# 2 space indentation
+[**.*]
+indent_style = space
+indent_size = 4

+ 2 - 0
.eslintignore

@@ -0,0 +1,2 @@
+src/hardingcenter_logo_de.js
+src/hardingcenter_logo_en.js

+ 15 - 0
.eslintrc.json

@@ -0,0 +1,15 @@
+{
+  "extends": "./node_modules/aurelia-tools/.eslintrc.json",
+  "parserOptions": {
+    "ecmaFeatures": {
+      "legacyDecorators": true
+    }
+  },  
+  "rules": {
+      "camelcase": "off",
+      "indent": ["warn", 4],
+      "padded-blocks": "off",
+      "radix": "off",
+      "space-infix-ops": "warn"
+  }
+}

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+.idea
+.DS_STORE
+package-lock.json
+node_modules
+scripts/app-bundle.js
+scripts/vendor-bundle.js

+ 82 - 0
README.md

@@ -0,0 +1,82 @@
+# Editierbare Faktenbox (verdichtet)
+
+Diese WebApp dient der Erstellung von Faktenboxen inklusive der Visualisierung von *Icon Arrays*.  
+Aus den eingegebenen Daten wird durch den Einsatz eines *Web Workers* (in nahezu Echtzeit) mit Hilfe von [pdfkit] ein PDF generiert, das unterhalb der Forularelemente in einem `iframe` dargestellt wird. Als zusätzliche Exportoption wird JSON angeboten, um Eingaben sichern zu könnnen. Exportierte Daten können über die Inport-Funktion wieder geladen und weiter bearbeitet werden.
+
+## Entwicklungsumgebung
+
+Diese WebApp wurde auf Basis von [aurelia] entwickelt. Für die Weiterentwicklung des Projekts werden [`nodejs`][nodejs] und `npm` benötigt. Die Abhängigkeiten werden wie üblich per `npm install` installiert.
+
+Für die Entwicklung steht der *Development Build Prozess* zur Verfügung, in dem Javascript Dateien transpiliert, und `css` aus den `scss` Dateien generiert werden. Zudem wird ein Entwicklungsserver gestartet, der bei Änderungen an Quelldateien nach einem erneuten Ausführen des Build Prozesses die Dateien neu lädt. Gestartet wird dieser Prozess mit folgendem Befehl:
+
+    au run --watch
+
+Ein *Production Build* kann folgendermaßen erstellt werden:
+
+    au build --env production
+
+Die so erzeugten Dateien sind unter `scripts` zu finden.
+
+## Verzeichnisstruktur
+
+```
+.
+├── README.md
+├── assets                                                  // Javascript-Bibliotheken zur Generierung von PDFs und zusätzliche Resourcen für umgebendes HTML
+│   ├── blob-stream.min.js
+│   ├── pdfkit.min.js
+│   └── shared
+│       ├── css
+│       │   └── main.css
+│       └── fonts
+│           ├── lora
+│           │   ├── Lora-Regular.ttf
+│           │   └── SIL Open Font License.txt
+│           └── montserrat
+│               ├── Montserrat-SemiBold.otf
+│               └── SIL Open Font License.txt
+├── aurelia_project                                         // Aurelia Projektkonfiguration
+│   └── …
+├── fact-box-editor.html                                    // Index HTML Datei der WebApp
+├── favicon.ico
+├── fonts -> src/scss/font-awesome/fonts
+├── index.html                                              // Umgebendes HTML mit Kontextinformationen
+├── jsconfig.json
+├── karma.conf.js
+├── package.json
+├── scripts                                                 // Build Verzeichnis
+│   └── …
+└── src
+    ├── app.html
+    ├── app.js                                              // Controller der WebApp
+    ├── configuration.js
+    ├── d3custom.js                                         // Definition der verwendeten d3 Module
+    ├── environment.js                                      // aurelia Environment Konfiguration
+    ├── main.js                                             // aurelia Bootstrapper
+    ├── messages.js                                         // Definition von Messages zur Kommunikation zwischen Anwendungsteilen
+    ├── number-format.js                                    // Zahlenformatierung
+    ├── pdfWorker.js                                        // PDF Generator
+    ├── resources
+    │   ├── Verdana\ Bold.ttf
+    │   ├── Verdana.ttf
+    │   └── index.js
+    ├── sampler-visual.html
+    ├── sampler-visual.js                                   // Implementierung der Visualisierung mit d3
+    ├── sampler.js                                          // Logik des Samplings
+    └── scss                                                // Stylesheets der WebApp
+        ├── _custom.scss
+        ├── _globals.scss
+        ├── font-awesome -> ../../node_modules/font-awesome
+        ├── partials
+        │   ├── _colours.scss
+        │   └── _form_elements.scss
+        └── styles.scss
+```
+
+## Wie ändere ich Labels und Texte der WebApp? ##
+
+In der Datei `src/configuration.js` sind alle Labels und Texte definiert. Außerdem sind dort die Farben und zugrunde liegenden geometrischen Daten für das Layout der Icon Arrays definiert.
+
+[aurelia]: https://aurelia.io
+[nodejs]: https://nodejs.org
+[pdfkit]: https://pdfkit.org

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
assets/blob-stream.min.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
assets/pdfkit.min.js


+ 36 - 0
assets/shared/css/main.css

@@ -0,0 +1,36 @@
+@font-face {
+    font-family: "Montserrat";
+    src: url("../fonts/montserrat/Montserrat-SemiBold.otf");
+}
+@font-face {
+    font-family: "Lora";
+    src: url("../fonts/lora/Lora-Regular.ttf");
+}
+body {
+    font-family: "Lora", "Helvetica Neue", Helvetica, Arial, sans-serif;
+    line-height: 1.5;
+}
+
+main {
+    margin: 0 auto;
+    max-width: 64em;
+}
+
+h1, h2 {
+    color: #262525;
+    font-family: "Montserrat", "Helvetica Neue", Helvetica, Arial, sans-serif;
+    font-weight: 700;
+    margin: 35px 0 35px;
+}
+
+iframe {
+    display: block;
+    margin: 0 auto;
+}
+
+.logos {
+    align-items: center;
+    display: flex;
+    justify-content: space-between;
+    margin: 2rem 0;
+}

BIN
assets/shared/fonts/lora/Lora-Regular.ttf


+ 44 - 0
assets/shared/fonts/lora/SIL Open Font License.txt

@@ -0,0 +1,44 @@
+Copyright (c) 2011-2013, Cyreal (www.cyreal.org a@cyreal.org), with
+Reserved Font Name 'Lora'
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
+
+"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
+
+5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.

BIN
assets/shared/fonts/montserrat/Montserrat-SemiBold.otf


+ 43 - 0
assets/shared/fonts/montserrat/SIL Open Font License.txt

@@ -0,0 +1,43 @@
+Copyright 2011 The Montserrat Project Authors (https://github.com/JulietaUla/Montserrat)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
+
+"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
+
+5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.

+ 172 - 0
aurelia_project/aurelia.json

@@ -0,0 +1,172 @@
+{
+  "name": "factbox-generator",
+  "type": "project:application",
+  "platform": {
+    "id": "web",
+    "displayName": "Web",
+    "output": "scripts",
+    "index": "index.html"
+  },
+  "transpiler": {
+    "id": "babel",
+    "displayName": "Babel",
+    "fileExtension": ".js",
+    "options": {
+      "plugins": [
+        "transform-es2015-modules-amd"
+      ]
+    },
+    "source": "src/**/*.js"
+  },
+  "markupProcessor": {
+    "id": "none",
+    "displayName": "None",
+    "fileExtension": ".html",
+    "source": "src/**/*.html"
+  },
+  "cssProcessor": {
+    "id": "sass",
+    "displayName": "Sass",
+    "fileExtension": ".scss",
+    "source": "src/**/*.scss"
+  },
+  "editor": {
+    "id": "vim",
+    "displayName": "Vim"
+  },
+  "unitTestRunner": {
+    "id": "karma",
+    "displayName": "Karma",
+    "source": "test/unit/**/*.js"
+  },
+  "paths": {
+    "root": "src",
+    "resources": "resources",
+    "elements": "resources/elements",
+    "attributes": "resources/attributes",
+    "valueConverters": "resources/value-converters",
+    "bindingBehaviors": "resources/binding-behaviors"
+  },
+  "testFramework": {
+    "id": "jasmine",
+    "displayName": "Jasmine"
+  },
+  "build": {
+    "targets": [
+      {
+        "id": "web",
+        "displayName": "Web",
+        "output": "scripts",
+        "index": "index.html"
+      }
+    ],
+    "loader": {
+      "type": "require",
+      "configTarget": "vendor-bundle.js",
+      "includeBundleMetadataInConfig": "auto",
+      "plugins": [
+        {
+          "name": "text",
+          "extensions": [
+            ".html",
+            ".css"
+          ],
+          "stub": true
+        }
+      ]
+    },
+    "options": {
+      "minify": "stage & prod",
+      "sourcemaps": "dev & stage"
+    },
+    "bundles": [
+      {
+        "name": "app-bundle.js",
+        "source": [
+          "[**/*.js]",
+          "**/*.{css,html}"
+        ]
+      },
+      {
+        "name": "vendor-bundle.js",
+        "prepend": [
+          "node_modules/bluebird/js/browser/bluebird.core.js",
+          "scripts/require.js"
+        ],
+        "dependencies": [
+          "aurelia-binding",
+          "aurelia-bootstrapper",
+          "aurelia-dependency-injection",
+          "aurelia-event-aggregator",
+          "aurelia-framework",
+          "aurelia-history",
+          "aurelia-history-browser",
+          "aurelia-loader",
+          "aurelia-loader-default",
+          "aurelia-logging",
+          "aurelia-logging-console",
+          "aurelia-metadata",
+          "aurelia-pal",
+          "aurelia-pal-browser",
+          "aurelia-path",
+          "aurelia-polyfills",
+          "aurelia-route-recognizer",
+          "aurelia-router",
+          "aurelia-task-queue",
+          "aurelia-templating",
+          "aurelia-templating-binding",
+          {
+            "name": "text",
+            "path": "../scripts/text"
+          },
+          {
+            "name": "aurelia-templating-resources",
+            "path": "../node_modules/aurelia-templating-resources/dist/amd",
+            "main": "aurelia-templating-resources"
+          },
+          {
+            "name": "aurelia-templating-router",
+            "path": "../node_modules/aurelia-templating-router/dist/amd",
+            "main": "aurelia-templating-router"
+          },
+          {
+            "name": "aurelia-testing",
+            "path": "../node_modules/aurelia-testing/dist/amd",
+            "main": "aurelia-testing",
+            "env": "dev"
+          },
+          {
+            "name": "aurelia-fetch-client",
+            "path": "../node_modules/aurelia-fetch-client/dist/amd",
+            "main": "aurelia-fetch-client"
+          },
+          {
+              "name": "d3-selection",
+              "path": "../node_modules/d3-selection/dist",
+              "main": "d3-selection"
+          },
+          {
+              "name": "d3-array",
+              "path": "../node_modules/d3-array/dist",
+              "main": "d3-array"
+          },
+          {
+              "name": "d3-collection",
+              "path": "../node_modules/d3-collection/dist",
+              "main": "d3-collection"
+          },
+          {
+              "name": "d3-color",
+              "path": "../node_modules/d3-color/dist",
+              "main": "d3-color"
+          },
+          {
+              "name": "fetch",
+              "path": "../node_modules/whatwg-fetch",
+              "main": "fetch"
+          }
+        ]
+      }
+    ]
+  }
+}

+ 4 - 0
aurelia_project/environments/dev.js

@@ -0,0 +1,4 @@
+export default {
+  debug: true,
+  testing: true
+};

+ 4 - 0
aurelia_project/environments/prod.js

@@ -0,0 +1,4 @@
+export default {
+  debug: false,
+  testing: false
+};

+ 4 - 0
aurelia_project/environments/stage.js

@@ -0,0 +1,4 @@
+export default {
+  debug: true,
+  testing: false
+};

+ 44 - 0
aurelia_project/generators/attribute.js

@@ -0,0 +1,44 @@
+import {inject} from 'aurelia-dependency-injection';
+import {Project, ProjectItem, CLIOptions, UI} from 'aurelia-cli';
+
+@inject(Project, CLIOptions, UI)
+export default class AttributeGenerator {
+  constructor(project, options, ui) {
+    this.project = project;
+    this.options = options;
+    this.ui = ui;
+  }
+
+  execute() {
+    return this.ui
+      .ensureAnswer(this.options.args[0], 'What would you like to call the custom attribute?')
+      .then(name => {
+        let fileName = this.project.makeFileName(name);
+        let className = this.project.makeClassName(name);
+
+        this.project.attributes.add(
+          ProjectItem.text(`${fileName}.js`, this.generateSource(className))
+        );
+
+        return this.project.commitChanges()
+          .then(() => this.ui.log(`Created ${fileName}.`));
+      });
+  }
+
+  generateSource(className) {
+    return `import {inject} from 'aurelia-framework';
+
+@inject(Element)
+export class ${className}CustomAttribute {
+  constructor(element) {
+    this.element = element;
+  }
+
+  valueChanged(newValue, oldValue) {
+
+  }
+}
+
+`;
+  }
+}

+ 4 - 0
aurelia_project/generators/attribute.json

@@ -0,0 +1,4 @@
+{
+  "name": "attribute",
+  "description": "Creates a custom attribute class and places it in the project resources."
+}

+ 41 - 0
aurelia_project/generators/binding-behavior.js

@@ -0,0 +1,41 @@
+import {inject} from 'aurelia-dependency-injection';
+import {Project, ProjectItem, CLIOptions, UI} from 'aurelia-cli';
+
+@inject(Project, CLIOptions, UI)
+export default class BindingBehaviorGenerator {
+  constructor(project, options, ui) {
+    this.project = project;
+    this.options = options;
+    this.ui = ui;
+  }
+
+  execute() {
+    return this.ui
+      .ensureAnswer(this.options.args[0], 'What would you like to call the binding behavior?')
+      .then(name => {
+        let fileName = this.project.makeFileName(name);
+        let className = this.project.makeClassName(name);
+
+        this.project.bindingBehaviors.add(
+          ProjectItem.text(`${fileName}.js`, this.generateSource(className))
+        );
+
+        return this.project.commitChanges()
+          .then(() => this.ui.log(`Created ${fileName}.`));
+      });
+  }
+
+  generateSource(className) {
+    return `export class ${className}BindingBehavior {
+  bind(binding, source) {
+
+  }
+
+  unbind(binding, source) {
+
+  }
+}
+
+`
+  }
+}

+ 4 - 0
aurelia_project/generators/binding-behavior.json

@@ -0,0 +1,4 @@
+{
+  "name": "binding-behavior",
+  "description": "Creates a binding behavior class and places it in the project resources."
+}

+ 48 - 0
aurelia_project/generators/element.js

@@ -0,0 +1,48 @@
+import {inject} from 'aurelia-dependency-injection';
+import {Project, ProjectItem, CLIOptions, UI} from 'aurelia-cli';
+
+@inject(Project, CLIOptions, UI)
+export default class ElementGenerator {
+  constructor(project, options, ui) {
+    this.project = project;
+    this.options = options;
+    this.ui = ui;
+  }
+
+  execute() {
+    return this.ui
+      .ensureAnswer(this.options.args[0], 'What would you like to call the custom element?')
+      .then(name => {
+        let fileName = this.project.makeFileName(name);
+        let className = this.project.makeClassName(name);
+
+        this.project.elements.add(
+          ProjectItem.text(`${fileName}.js`, this.generateJSSource(className)),
+          ProjectItem.text(`${fileName}.html`, this.generateHTMLSource(className))
+        );
+
+        return this.project.commitChanges()
+          .then(() => this.ui.log(`Created ${fileName}.`));
+      });
+  }
+
+  generateJSSource(className) {
+    return `import {bindable} from 'aurelia-framework';
+
+export class ${className} {
+  @bindable value;
+
+  valueChanged(newValue, oldValue) {
+
+  }
+}
+
+`;
+  }
+
+  generateHTMLSource(className) {
+    return `<template>
+  <h1>\${value}</h1>
+</template>`;
+  }
+}

+ 4 - 0
aurelia_project/generators/element.json

@@ -0,0 +1,4 @@
+{
+  "name": "element",
+  "description": "Creates a custom element class and template, placing them in the project resources."
+}

+ 73 - 0
aurelia_project/generators/generator.js

@@ -0,0 +1,73 @@
+import {inject} from 'aurelia-dependency-injection';
+import {Project, ProjectItem, CLIOptions, UI} from 'aurelia-cli';
+
+@inject(Project, CLIOptions, UI)
+export default class GeneratorGenerator {
+  constructor(project, options, ui) {
+    this.project = project;
+    this.options = options;
+    this.ui = ui;
+  }
+
+  execute() {
+    return this.ui
+      .ensureAnswer(this.options.args[0], 'What would you like to call the generator?')
+      .then(name => {
+        let fileName = this.project.makeFileName(name);
+        let className = this.project.makeClassName(name);
+
+        this.project.generators.add(
+          ProjectItem.text(`${fileName}.js`, this.generateSource(className))
+        );
+
+        return this.project.commitChanges()
+          .then(() => this.ui.log(`Created ${fileName}.`));
+      });
+  }
+
+  generateSource(className) {
+    return `import {inject} from 'aurelia-dependency-injection';
+import {Project, ProjectItem, CLIOptions, UI} from 'aurelia-cli';
+
+@inject(Project, CLIOptions, UI)
+export default class ${className}Generator {
+  constructor(project, options, ui) {
+    this.project = project;
+    this.options = options;
+    this.ui = ui;
+  }
+
+  execute() {
+    return this.ui
+      .ensureAnswer(this.options.args[0], 'What would you like to call the new item?')
+      .then(name => {
+        let fileName = this.project.makeFileName(name);
+        let className = this.project.makeClassName(name);
+
+        this.project.elements.add(
+          ProjectItem.text(\`\${fileName}.js\`, this.generateSource(className))
+        );
+
+        return this.project.commitChanges()
+          .then(() => this.ui.log(\`Created \${fileName}.\`));
+      });
+  }
+
+  generateSource(className) {
+return \`import {bindable} from 'aurelia-framework';
+
+export class \${className} {
+  @bindable value;
+
+  valueChanged(newValue, oldValue) {
+
+  }
+}
+
+\`
+  }
+}
+
+`;
+  }
+}

+ 4 - 0
aurelia_project/generators/generator.json

@@ -0,0 +1,4 @@
+{
+  "name": "generator",
+  "description": "Creates a generator class and places it in the project generators folder."
+}

+ 41 - 0
aurelia_project/generators/task.js

@@ -0,0 +1,41 @@
+import {inject} from 'aurelia-dependency-injection';
+import {Project, ProjectItem, CLIOptions, UI} from 'aurelia-cli';
+
+@inject(Project, CLIOptions, UI)
+export default class TaskGenerator {
+  constructor(project, options, ui) {
+    this.project = project;
+    this.options = options;
+    this.ui = ui;
+  }
+
+  execute() {
+    return this.ui
+      .ensureAnswer(this.options.args[0], 'What would you like to call the task?')
+      .then(name => {
+        let fileName = this.project.makeFileName(name);
+        let functionName = this.project.makeFunctionName(name);
+
+        this.project.tasks.add(
+          ProjectItem.text(`${fileName}.js`, this.generateSource(functionName))
+        );
+
+        return this.project.commitChanges()
+          .then(() => this.ui.log(`Created ${fileName}.`));
+      });
+  }
+
+  generateSource(functionName) {
+    return `import gulp from 'gulp';
+import changed from 'gulp-changed';
+import project from '../aurelia.json';
+
+export default function ${functionName}() {
+  return gulp.src(project.paths.???)
+    .pipe(changed(project.paths.output, {extension: '.???'}))
+    .pipe(gulp.dest(project.paths.output));
+}
+
+`;
+  }
+}

+ 4 - 0
aurelia_project/generators/task.json

@@ -0,0 +1,4 @@
+{
+  "name": "task",
+  "description": "Creates a task and places it in the project tasks folder."
+}

+ 41 - 0
aurelia_project/generators/value-converter.js

@@ -0,0 +1,41 @@
+import {inject} from 'aurelia-dependency-injection';
+import {Project, ProjectItem, CLIOptions, UI} from 'aurelia-cli';
+
+@inject(Project, CLIOptions, UI)
+export default class ValueConverterGenerator {
+  constructor(project, options, ui) {
+    this.project = project;
+    this.options = options;
+    this.ui = ui;
+  }
+
+  execute() {
+    return this.ui
+      .ensureAnswer(this.options.args[0], 'What would you like to call the value converter?')
+      .then(name => {
+        let fileName = this.project.makeFileName(name);
+        let className = this.project.makeClassName(name);
+
+        this.project.valueConverters.add(
+          ProjectItem.text(`${fileName}.js`, this.generateSource(className))
+        );
+
+        return this.project.commitChanges()
+          .then(() => this.ui.log(`Created ${fileName}.`));
+      });
+  }
+
+  generateSource(className) {
+    return `export class ${className}ValueConverter {
+  toView(value) {
+
+  }
+
+  fromView(value) {
+
+  }
+}
+
+`;
+  }
+}

+ 4 - 0
aurelia_project/generators/value-converter.json

@@ -0,0 +1,4 @@
+{
+  "name": "value-converter",
+  "description": "Creates a value converter class and places it in the project resources."
+}

+ 24 - 0
aurelia_project/tasks/build.js

@@ -0,0 +1,24 @@
+import gulp from 'gulp';
+import transpile from './transpile';
+import processMarkup from './process-markup';
+import processCSS from './process-css';
+import {build} from 'aurelia-cli';
+import project from '../aurelia.json';
+
+export default gulp.series(
+  readProjectConfiguration,
+  gulp.parallel(
+    transpile,
+    processMarkup,
+    processCSS
+  ),
+  writeBundles
+);
+
+function readProjectConfiguration() {
+  return build.src(project);
+}
+
+function writeBundles() {
+  return build.dest();
+}

+ 11 - 0
aurelia_project/tasks/build.json

@@ -0,0 +1,11 @@
+{
+  "name": "build",
+  "description": "Builds and processes all application assets.",
+  "flags": [
+    {
+      "name": "env",
+      "description": "Sets the build environment.",
+      "type": "string"
+    }
+  ]
+}

+ 12 - 0
aurelia_project/tasks/process-css.js

@@ -0,0 +1,12 @@
+import gulp from 'gulp';
+import sourcemaps from 'gulp-sourcemaps';
+import sass from 'gulp-sass';
+import project from '../aurelia.json';
+import {build} from 'aurelia-cli';
+
+export default function processCSS() {
+  return gulp.src(project.cssProcessor.source)
+    .pipe(sourcemaps.init())
+    .pipe(sass().on('error', sass.logError))
+    .pipe(build.bundle());
+}

+ 10 - 0
aurelia_project/tasks/process-markup.js

@@ -0,0 +1,10 @@
+import gulp from 'gulp';
+import changedInPlace from 'gulp-changed-in-place';
+import project from '../aurelia.json';
+import {build} from 'aurelia-cli';
+
+export default function processMarkup() {
+  return gulp.src(project.markupProcessor.source)
+    .pipe(changedInPlace({firstPass: true}))
+    .pipe(build.bundle());
+}

+ 67 - 0
aurelia_project/tasks/run.js

@@ -0,0 +1,67 @@
+import gulp from 'gulp';
+import browserSync from 'browser-sync';
+import historyApiFallback from 'connect-history-api-fallback/lib';
+import project from '../aurelia.json';
+import build from './build';
+import {CLIOptions} from 'aurelia-cli';
+
+function log(message) {
+  console.log(message); //eslint-disable-line no-console
+}
+
+function onChange(path) {
+  log(`File Changed: ${path}`);
+}
+
+function reload(done) {
+  browserSync.reload();
+  done();
+}
+
+let serve = gulp.series(
+  build,
+  done => {
+    browserSync({
+      online: false,
+      open: false,
+      port: 9000,
+      logLevel: 'silent',
+      server: {
+        baseDir: ['.'],
+        middleware: [historyApiFallback(), function(req, res, next) {
+          res.setHeader('Access-Control-Allow-Origin', '*');
+          next();
+        }]
+      }
+    }, function(err, bs) {
+      let urls = bs.options.get('urls').toJS();
+      log(`Application Available At: ${urls.local}`);
+      log(`BrowserSync Available At: ${urls.ui}`);
+      done();
+    });
+  }
+);
+
+let refresh = gulp.series(
+  build,
+  reload
+);
+
+let watch = function() {
+  gulp.watch(project.transpiler.source, refresh).on('change', onChange);
+  gulp.watch(project.markupProcessor.source, refresh).on('change', onChange);
+  gulp.watch(project.cssProcessor.source, refresh).on('change', onChange);
+};
+
+let run;
+
+if (CLIOptions.hasFlag('watch')) {
+  run = gulp.series(
+    serve,
+    watch
+  );
+} else {
+  run = serve;
+}
+
+export default run;

+ 16 - 0
aurelia_project/tasks/run.json

@@ -0,0 +1,16 @@
+{
+  "name": "run",
+  "description": "Builds the application and serves up the assets via a local web server, watching files for changes as you work.",
+  "flags": [
+    {
+      "name": "env",
+      "description": "Sets the build environment.",
+      "type": "string"
+    },
+    {
+      "name": "watch",
+      "description": "Watches source files for changes and refreshes the app automatically.",
+      "type": "boolean"
+    }
+  ]
+}

+ 11 - 0
aurelia_project/tasks/test.js

@@ -0,0 +1,11 @@
+import {Server as Karma} from 'karma';
+import {CLIOptions} from 'aurelia-cli';
+
+export function unit(done) {
+  new Karma({
+    configFile: __dirname + '/../../karma.conf.js',
+    singleRun: !CLIOptions.hasFlag('watch')
+  }, done).start();
+}
+
+export default unit;

+ 16 - 0
aurelia_project/tasks/test.json

@@ -0,0 +1,16 @@
+{
+  "name": "test",
+  "description": "Runs all unit tests and reports the results.",
+  "flags": [
+    {
+      "name": "env",
+      "description": "Sets the build environment.",
+      "type": "string"
+    },
+    {
+      "name": "watch",
+      "description": "Watches test files for changes and re-runs the tests automatically.",
+      "type": "boolean"
+    }
+  ]
+}

+ 32 - 0
aurelia_project/tasks/transpile.js

@@ -0,0 +1,32 @@
+import gulp from 'gulp';
+import changedInPlace from 'gulp-changed-in-place';
+import plumber from 'gulp-plumber';
+import babel from 'gulp-babel';
+import sourcemaps from 'gulp-sourcemaps';
+import notify from 'gulp-notify';
+import rename from 'gulp-rename';
+import project from '../aurelia.json';
+import {CLIOptions, build} from 'aurelia-cli';
+
+function configureEnvironment() {
+  let env = CLIOptions.getEnvironment();
+
+  return gulp.src(`aurelia_project/environments/${env}.js`)
+    .pipe(changedInPlace({firstPass: true}))
+    .pipe(rename('environment.js'))
+    .pipe(gulp.dest(project.paths.root));
+}
+
+function buildJavaScript() {
+  return gulp.src(project.transpiler.source)
+    .pipe(plumber({errorHandler: notify.onError('Error: <%= error.message %>')}))
+    .pipe(changedInPlace({firstPass: true}))
+    .pipe(sourcemaps.init())
+    .pipe(babel(project.transpiler.options))
+    .pipe(build.bundle());
+}
+
+export default gulp.series(
+  configureEnvironment,
+  buildJavaScript
+);

+ 11 - 0
fact-box-editor.html

@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Fact Box Editor</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+  </head>
+
+  <body aurelia-app="main">
+    <script src="scripts/vendor-bundle.js" data-main="aurelia-bootstrapper"></script>
+  </body>
+</html>

BIN
favicon.ico


+ 1 - 0
fonts

@@ -0,0 +1 @@
+src/scss/font-awesome/fonts

+ 49 - 0
index.html

@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="utf-8">
+        <title>Editierbare Verbraucherfaktenbox</title>
+        <meta name="viewport" content="width=device-width, initial-scale=1">
+        <link href="assets/shared/css/main.css" rel="stylesheet">
+    </head>
+    <body>
+        <main>
+            <div>
+                <h2 id="fragestellungen-zur-risikokommunikation-für-verbraucher">Fragestellungen zur Risikokommunikation für Verbraucher</h2>
+                <p>Liegt ein Problem des Risikos vor, d.h. sind belastbare numerische Schätzer vom Eintreffen spezifischer Ereignisse oder Entscheidungenkonsequenzen verfügbar, ist eine Hauptfragestellung, wie Risikokommunikation hier transparent, ausgewogen und verständlich für die Verbraucher sein kann? Drei Herausforderungen sind hierbei</p>
+                <ol type="1">
+                    <li>Wie können Organisationen Einzelfallwahrscheinlichkeiten trotz aleatorischer Unsicherheit über den konkreten Ausgang im Einzelfall vermitteln?</li>
+                    <li>Wie können Handlungsoptionen mit verschiedenen Konsequenzen (potenzielle Nutzen und Schäden) unter Berücksichtigung der Wahrscheinlichkeiten des Eintretens vergleichend und vor allem standardisiert kommuniziert werden?</li>
+                    <li>Wie können Organisationen, die nicht auf Risikokommunikation spezialisiert sind, für eigene Fragestellungen auf direktem Wege evidenzbasierte Risikokommunikation machen.</li>
+                </ol>
+                <h2 id="lösungsansatz">Lösungsansatz</h2>
+                <p>Editierbare Verbraucherfaktenbox</p>
+                <ol type="1">
+                    <li>Einzelfallwahrscheinlichkeiten lassen sich entweder mit einfachen Häufigkeiten (sofern nur Wahrscheinlichkeitswerte &gt; 1% auch als Prozentformat) in tabellarischer Form (Schwartz et al., 2009) oder, zur Unterstützung von Verbrauchern mit niedriger Numeracy (Cokely et al., 2012), durch den Einsatz von empirisch validierten Grafikformaten (Garcia-Retamero &amp; Galesic, 2010), Barcharts oder IconArrays kommunizieren.</li>
+                    <li>Statische Faktenboxen sind evidenzbasierte Präsentationsformate, die mögliche Nutzen und Schäden von Optionen transparent und ausgewogen zusammenfassen. Faktenboxen als evaluiertes Format zeigen, wie Risikokommunikation an Entscheidungsoptionen geknüpft, zugleich transparent, ausgewogen und verständlich ist: Sie haben sich in mehreren randomisierten kontrollierten Studien als effektives Hilfsmittel erwiesen, um das Verständnis von statistischen Daten – auch mittels IconArray wie in der Verbraucherfaktenbox – zu erhöhen (McDowell et al., 2019) sowie die Extraktion von Informationen und den Wissenserwerb nach einmaligem Lesen zu erleichtern (Schwartz et al., 2009).</li>
+                    <li>Das nach einer Standardisierung (McDowell et al., 2016) weiterentwickelte Verbraucherfaktenbox-Format, eingebunden in einen rahmenden Kontext für diese Evidenz, kann auf verschiedenste Entscheidungsprobleme des Risikos angewendet werden. Als solches ist eine standardisierte Vorlage für Risikokommunikatoren geschaffen.</li>
+                </ol>
+                <h2 id="wie-wird-das-werkzeug-verwendet">Wie wird das Werkzeug verwendet?</h2>
+                <p>Die Felder werden direkt im Browser gefüllt und ein PDF entsprechend generiert, welches abgespeichert und verschickt werden kann. Auch die Datendateien lassen sich exportieren bzw. entsprechend importieren, um Wichtig für die Konstruktion sind: Titel, um das Thema des Entscheidungsproblems präzise zu beschreiben; Referenzgruppe, um zu erklären, auf wen sich das Entscheidungsproblem bezieht. Hierbei ist zwingend die Gruppe zu beschreiben, mit welcher die Daten gewonnen wurden; Zusammenfassung der Evidenz in Form eines einzelnen Satzes zum Verhältnis der potenziellen Nutzen und Schäden. Hierbei muss auf eine Bewertung verzichtet werden, da die Faktenbox nicht für die Darstellung von Interpretationen konstruiert ist; Einfache Gruppenlabel, welche die beiden Entscheidungsoptionen unterscheidbar machen; Endpunkte, welche die Konsequenzen, also die potenziellen Nutzen und Schäden des Verfolgens von Entscheidungsoptionen benennen; Die Zahleneingaben sind im einfachen Häufigkeitsformat formuliert (auszuwählen entweder je 100 oder je 1000); Quellenangaben für die zu visualisierenden Zahlen; Das Datum der letzten Aktualisierung; Der Link, unter dem eine Verbraucherfaktenbox hinterlegt wird.</p>
+            </div>
+
+            <iframe src="fact-box-editor.html" width="1024px" height="1519px" frameborder="0"></iframe>
+
+            <div>
+                <h2 id="quellen.">Quellen.</h2>
+                <p>Cokely, E. T., Galesic, M., Schulz, E., Ghazal, S., &amp; Garcia-Retamero, R. (2012). Measuring Risk Literacy: The Berlin Numeracy Test. Judgment and Decision Making, 7(1), 25-47.<br />
+                  Garcia-Retamero, R., &amp; Galesic, M. (2010). Who proficts from visual aids: Overcoming challenges in people’s understanding of risks. Social Science &amp; Medicine, 70(7), 1019-1025.<br />
+                  McDowell, M., Gigerenzer, G., Wegwarth, O., &amp; Rebitschek, F. G. (2019). Effect of Tabular and Icon Fact Box Formats on Comprehension of Benefits and Harms of Prostate Cancer Screening: A Randomized Trial. Medical Decision Making, 39(1), 41-56.<br />
+                  McDowell, M., Rebitschek, F. G., Gigerenzer, G., &amp; Wegwarth, O. (2016). A simple tool for communicating the benefits and harms of health interventions: a guide for creating a fact box. MDM Policy &amp; Practice, 1(1), 2381468316665365.<br />
+                  Schwartz, L. M., Woloshin, S., &amp; Welch, H. G. (2009). Using a drug facts box to communicate drug benefits and harms: two randomized trials. Annals of Internal Medicine, 150(8), 516-527.</p>
+
+                <h2 id="letztes-update">Letztes Update</h2>
+                <ol start="7" type="1">
+                    <li>Februar 2019</li>
+                </ol>
+                <h2 id="erstellung">Erstellung</h2>
+                <p>Felix G. Rebitschek und Michael Zitzmann</p>
+            </div>
+        </main>
+    </body>
+</html>

+ 7 - 0
jsconfig.json

@@ -0,0 +1,7 @@
+{
+  "compilerOptions": {
+    "target": "ES6",
+    "module": "amd",
+    "experimentalDecorators": true
+  }
+}

+ 40 - 0
karma.conf.js

@@ -0,0 +1,40 @@
+"use strict";
+const path = require('path');
+const project = require('./aurelia_project/aurelia.json');
+
+let testSrc = [
+  { pattern: project.unitTestRunner.source, included: false },
+  'test/aurelia-karma.js'
+];
+
+let output = project.platform.output;
+let appSrc = project.build.bundles.map(x => path.join(output, x.name));
+let entryIndex = appSrc.indexOf(path.join(output, project.build.loader.configTarget));
+let entryBundle = appSrc.splice(entryIndex, 1)[0];
+let files = [entryBundle].concat(testSrc).concat(appSrc);
+
+module.exports = function(config) {
+  config.set({
+    basePath: '',
+    frameworks: [project.testFramework.id],
+    files: files,
+    exclude: [],
+    preprocessors: {
+      [project.unitTestRunner.source]: [project.transpiler.id]
+    },
+    'babelPreprocessor': { options: project.transpiler.options },
+    reporters: ['progress'],
+    port: 9876,
+    colors: true,
+    logLevel: config.LOG_INFO,
+    autoWatch: true,
+    browsers: ['Chrome'],
+    singleRun: false,
+    // client.args must be a array of string.
+    // Leave 'aurelia-root', project.paths.root in this order so we can find
+    // the root of the aurelia project.
+    client: {
+      args: ['aurelia-root', project.paths.root]
+    }
+  });
+};

+ 83 - 0
package.json

@@ -0,0 +1,83 @@
+{
+  "name": "fact-box-editor-compact",
+  "version": "1.0.0",
+  "license": "MIT",
+  "dependencies": {
+    "aurelia-animator-css": "^1.0.4",
+    "aurelia-binding": "^2.3.1",
+    "aurelia-bootstrapper": "^2.3.3",
+    "aurelia-fetch-client": "^1.8.2",
+    "aurelia-templating-router": "^1.4.0",
+    "blob-stream": "^0.1.3",
+    "bluebird": "^3.5.5",
+    "bulma": "^0.7.5",
+    "concise.css": "^4.1.2",
+    "d3-array": "^2.2.0",
+    "d3-collection": "^1.0.7",
+    "d3-color": "^1.2.8",
+    "d3-scale": "^3.0.0",
+    "d3-selection": "^1.4.0",
+    "emitter": "0.0.5",
+    "font-awesome": "^4.7.0",
+    "fs": "0.0.2",
+    "inherits": "^2.0.4",
+    "path": "^0.12.7",
+    "pdfkit": "^0.10.0",
+    "purecss-sass": "^1.0.0",
+    "stream": "0.0.2",
+    "susy": "^3.0.5",
+    "util": "^0.12.1",
+    "whatwg-fetch": "^3.0.0"
+  },
+  "peerDependencies": {},
+  "devDependencies": {
+    "@babel/core": "^7.5.4",
+    "@babel/preset-env": "^7.5.4",
+    "aurelia-cli": "^1.0.2",
+    "aurelia-dependency-injection": "^1.4.2",
+    "aurelia-event-aggregator": "^1.0.3",
+    "aurelia-framework": "^1.3.1",
+    "aurelia-history": "^1.2.1",
+    "aurelia-history-browser": "^1.4.0",
+    "aurelia-loader-default": "^1.2.1",
+    "aurelia-logging-console": "^1.1.1",
+    "aurelia-pal-browser": "^1.8.1",
+    "aurelia-polyfills": "^1.3.4",
+    "aurelia-route-recognizer": "^1.3.2",
+    "aurelia-router": "^1.7.1",
+    "aurelia-templating-binding": "^1.5.3",
+    "aurelia-templating-resources": "^1.11.0",
+    "aurelia-testing": "^1.0.0",
+    "aurelia-tools": "^2.0.0",
+    "babel-eslint": "^10.0.2",
+    "babel-plugin-syntax-flow": "^6.18.0",
+    "babel-plugin-transform-decorators-legacy": "^1.3.5",
+    "babel-plugin-transform-es2015-modules-amd": "^6.24.1",
+    "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
+    "babel-plugin-transform-flow-strip-types": "^6.22.0",
+    "babel-polyfill": "^6.26.0",
+    "babel-preset-env": "^1.7.0",
+    "babel-register": "^6.26.0",
+    "browser-sync": "^2.26.7",
+    "connect-history-api-fallback": "^1.6.0",
+    "gulp": "^4.0.2",
+    "gulp-babel": "^8.0.0",
+    "gulp-changed-in-place": "^2.3.0",
+    "gulp-eslint": "^6.0.0",
+    "gulp-notify": "^3.2.0",
+    "gulp-plumber": "^1.2.1",
+    "gulp-rename": "^1.4.0",
+    "gulp-sass": "^4.0.2",
+    "gulp-sourcemaps": "^2.6.5",
+    "jasmine-core": "^3.4.0",
+    "karma": "^4.1.0",
+    "karma-babel-preprocessor": "^8.0.0",
+    "karma-chrome-launcher": "^2.2.0",
+    "karma-jasmine": "^2.0.1",
+    "minimatch": "^3.0.4",
+    "node-sass": "^4.13.1",
+    "through2": "^3.0.1",
+    "uglify-js": "^3.6.0",
+    "vinyl-fs": "^3.0.3"
+  }
+}

+ 36 - 0
scripts/require.js

@@ -0,0 +1,36 @@
+/*
+ RequireJS 2.2.0 Copyright jQuery Foundation and other contributors.
+ Released under MIT license, http://github.com/requirejs/requirejs/LICENSE
+*/
+var requirejs,require,define;
+(function(ga){function ka(b,c,d,g){return g||""}function K(b){return"[object Function]"===Q.call(b)}function L(b){return"[object Array]"===Q.call(b)}function y(b,c){if(b){var d;for(d=0;d<b.length&&(!b[d]||!c(b[d],d,b));d+=1);}}function X(b,c){if(b){var d;for(d=b.length-1;-1<d&&(!b[d]||!c(b[d],d,b));--d);}}function x(b,c){return la.call(b,c)}function e(b,c){return x(b,c)&&b[c]}function D(b,c){for(var d in b)if(x(b,d)&&c(b[d],d))break}function Y(b,c,d,g){c&&D(c,function(c,e){if(d||!x(b,e))!g||"object"!==
+typeof c||!c||L(c)||K(c)||c instanceof RegExp?b[e]=c:(b[e]||(b[e]={}),Y(b[e],c,d,g))});return b}function z(b,c){return function(){return c.apply(b,arguments)}}function ha(b){throw b;}function ia(b){if(!b)return b;var c=ga;y(b.split("."),function(b){c=c[b]});return c}function F(b,c,d,g){c=Error(c+"\nhttp://requirejs.org/docs/errors.html#"+b);c.requireType=b;c.requireModules=g;d&&(c.originalError=d);return c}function ma(b){function c(a,n,b){var h,k,f,c,d,l,g,r;n=n&&n.split("/");var q=p.map,m=q&&q["*"];
+if(a){a=a.split("/");k=a.length-1;p.nodeIdCompat&&U.test(a[k])&&(a[k]=a[k].replace(U,""));"."===a[0].charAt(0)&&n&&(k=n.slice(0,n.length-1),a=k.concat(a));k=a;for(f=0;f<k.length;f++)c=k[f],"."===c?(k.splice(f,1),--f):".."===c&&0!==f&&(1!==f||".."!==k[2])&&".."!==k[f-1]&&0<f&&(k.splice(f-1,2),f-=2);a=a.join("/")}if(b&&q&&(n||m)){k=a.split("/");f=k.length;a:for(;0<f;--f){d=k.slice(0,f).join("/");if(n)for(c=n.length;0<c;--c)if(b=e(q,n.slice(0,c).join("/")))if(b=e(b,d)){h=b;l=f;break a}!g&&m&&e(m,d)&&
+(g=e(m,d),r=f)}!h&&g&&(h=g,l=r);h&&(k.splice(0,l,h),a=k.join("/"))}return(h=e(p.pkgs,a))?h:a}function d(a){E&&y(document.getElementsByTagName("script"),function(n){if(n.getAttribute("data-requiremodule")===a&&n.getAttribute("data-requirecontext")===l.contextName)return n.parentNode.removeChild(n),!0})}function m(a){var n=e(p.paths,a);if(n&&L(n)&&1<n.length)return n.shift(),l.require.undef(a),l.makeRequire(null,{skipMap:!0})([a]),!0}function r(a){var n,b=a?a.indexOf("!"):-1;-1<b&&(n=a.substring(0,
+b),a=a.substring(b+1,a.length));return[n,a]}function q(a,n,b,h){var k,f,d=null,g=n?n.name:null,p=a,q=!0,m="";a||(q=!1,a="_@r"+(Q+=1));a=r(a);d=a[0];a=a[1];d&&(d=c(d,g,h),f=e(v,d));a&&(d?m=f&&f.normalize?f.normalize(a,function(a){return c(a,g,h)}):-1===a.indexOf("!")?c(a,g,h):a:(m=c(a,g,h),a=r(m),d=a[0],m=a[1],b=!0,k=l.nameToUrl(m)));b=!d||f||b?"":"_unnormalized"+(T+=1);return{prefix:d,name:m,parentMap:n,unnormalized:!!b,url:k,originalName:p,isDefine:q,id:(d?d+"!"+m:m)+b}}function u(a){var b=a.id,
+c=e(t,b);c||(c=t[b]=new l.Module(a));return c}function w(a,b,c){var h=a.id,k=e(t,h);if(!x(v,h)||k&&!k.defineEmitComplete)if(k=u(a),k.error&&"error"===b)c(k.error);else k.on(b,c);else"defined"===b&&c(v[h])}function A(a,b){var c=a.requireModules,h=!1;if(b)b(a);else if(y(c,function(b){if(b=e(t,b))b.error=a,b.events.error&&(h=!0,b.emit("error",a))}),!h)g.onError(a)}function B(){V.length&&(y(V,function(a){var b=a[0];"string"===typeof b&&(l.defQueueMap[b]=!0);G.push(a)}),V=[])}function C(a){delete t[a];
+delete Z[a]}function J(a,b,c){var h=a.map.id;a.error?a.emit("error",a.error):(b[h]=!0,y(a.depMaps,function(h,f){var d=h.id,g=e(t,d);!g||a.depMatched[f]||c[d]||(e(b,d)?(a.defineDep(f,v[d]),a.check()):J(g,b,c))}),c[h]=!0)}function H(){var a,b,c=(a=1E3*p.waitSeconds)&&l.startTime+a<(new Date).getTime(),h=[],k=[],f=!1,g=!0;if(!aa){aa=!0;D(Z,function(a){var l=a.map,e=l.id;if(a.enabled&&(l.isDefine||k.push(a),!a.error))if(!a.inited&&c)m(e)?f=b=!0:(h.push(e),d(e));else if(!a.inited&&a.fetched&&l.isDefine&&
+(f=!0,!l.prefix))return g=!1});if(c&&h.length)return a=F("timeout","Load timeout for modules: "+h,null,h),a.contextName=l.contextName,A(a);g&&y(k,function(a){J(a,{},{})});c&&!b||!f||!E&&!ja||ba||(ba=setTimeout(function(){ba=0;H()},50));aa=!1}}function I(a){x(v,a[0])||u(q(a[0],null,!0)).init(a[1],a[2])}function O(a){a=a.currentTarget||a.srcElement;var b=l.onScriptLoad;a.detachEvent&&!ca?a.detachEvent("onreadystatechange",b):a.removeEventListener("load",b,!1);b=l.onScriptError;a.detachEvent&&!ca||a.removeEventListener("error",
+b,!1);return{node:a,id:a&&a.getAttribute("data-requiremodule")}}function P(){var a;for(B();G.length;){a=G.shift();if(null===a[0])return A(F("mismatch","Mismatched anonymous define() module: "+a[a.length-1]));I(a)}l.defQueueMap={}}var aa,da,l,R,ba,p={waitSeconds:7,baseUrl:"./",paths:{},bundles:{},pkgs:{},shim:{},config:{}},t={},Z={},ea={},G=[],v={},W={},fa={},Q=1,T=1;R={require:function(a){return a.require?a.require:a.require=l.makeRequire(a.map)},exports:function(a){a.usingExports=!0;if(a.map.isDefine)return a.exports?
+v[a.map.id]=a.exports:a.exports=v[a.map.id]={}},module:function(a){return a.module?a.module:a.module={id:a.map.id,uri:a.map.url,config:function(){return e(p.config,a.map.id)||{}},exports:a.exports||(a.exports={})}}};da=function(a){this.events=e(ea,a.id)||{};this.map=a;this.shim=e(p.shim,a.id);this.depExports=[];this.depMaps=[];this.depMatched=[];this.pluginMaps={};this.depCount=0};da.prototype={init:function(a,b,c,h){h=h||{};if(!this.inited){this.factory=b;if(c)this.on("error",c);else this.events.error&&
+(c=z(this,function(a){this.emit("error",a)}));this.depMaps=a&&a.slice(0);this.errback=c;this.inited=!0;this.ignore=h.ignore;h.enabled||this.enabled?this.enable():this.check()}},defineDep:function(a,b){this.depMatched[a]||(this.depMatched[a]=!0,--this.depCount,this.depExports[a]=b)},fetch:function(){if(!this.fetched){this.fetched=!0;l.startTime=(new Date).getTime();var a=this.map;if(this.shim)l.makeRequire(this.map,{enableBuildCallback:!0})(this.shim.deps||[],z(this,function(){return a.prefix?this.callPlugin():
+this.load()}));else return a.prefix?this.callPlugin():this.load()}},load:function(){var a=this.map.url;W[a]||(W[a]=!0,l.load(this.map.id,a))},check:function(){if(this.enabled&&!this.enabling){var a,b,c=this.map.id;b=this.depExports;var h=this.exports,k=this.factory;if(!this.inited)x(l.defQueueMap,c)||this.fetch();else if(this.error)this.emit("error",this.error);else if(!this.defining){this.defining=!0;if(1>this.depCount&&!this.defined){if(K(k)){if(this.events.error&&this.map.isDefine||g.onError!==
+ha)try{h=l.execCb(c,k,b,h)}catch(d){a=d}else h=l.execCb(c,k,b,h);this.map.isDefine&&void 0===h&&((b=this.module)?h=b.exports:this.usingExports&&(h=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",A(this.error=a)}else h=k;this.exports=h;if(this.map.isDefine&&!this.ignore&&(v[c]=h,g.onResourceLoad)){var f=[];y(this.depMaps,function(a){f.push(a.normalizedMap||a)});g.onResourceLoad(l,this.map,f)}C(c);
+this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}},callPlugin:function(){var a=this.map,b=a.id,d=q(a.prefix);this.depMaps.push(d);w(d,"defined",z(this,function(h){var k,f,d=e(fa,this.map.id),M=this.map.name,r=this.map.parentMap?this.map.parentMap.name:null,m=l.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(h.normalize&&(M=h.normalize(M,function(a){return c(a,r,!0)})||
+""),f=q(a.prefix+"!"+M,this.map.parentMap),w(f,"defined",z(this,function(a){this.map.normalizedMap=f;this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),h=e(t,f.id)){this.depMaps.push(f);if(this.events.error)h.on("error",z(this,function(a){this.emit("error",a)}));h.enable()}}else d?(this.map.url=l.nameToUrl(d),this.load()):(k=z(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),k.error=z(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];D(t,function(a){0===
+a.map.id.indexOf(b+"_unnormalized")&&C(a.map.id)});A(a)}),k.fromText=z(this,function(h,c){var d=a.name,f=q(d),M=S;c&&(h=c);M&&(S=!1);u(f);x(p.config,b)&&(p.config[d]=p.config[b]);try{g.exec(h)}catch(e){return A(F("fromtexteval","fromText eval for "+b+" failed: "+e,e,[b]))}M&&(S=!0);this.depMaps.push(f);l.completeLoad(d);m([d],k)}),h.load(a.name,m,k,p))}));l.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){Z[this.map.id]=this;this.enabling=this.enabled=!0;y(this.depMaps,z(this,function(a,
+b){var c,h;if("string"===typeof a){a=q(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=e(R,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;w(a,"defined",z(this,function(a){this.undefed||(this.defineDep(b,a),this.check())}));this.errback?w(a,"error",z(this,this.errback)):this.events.error&&w(a,"error",z(this,function(a){this.emit("error",a)}))}c=a.id;h=t[c];x(R,c)||!h||h.enabled||l.enable(a,this)}));D(this.pluginMaps,z(this,function(a){var b=e(t,a.id);
+b&&!b.enabled&&l.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){y(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};l={config:p,contextName:b,registry:t,defined:v,urlFetched:W,defQueue:G,defQueueMap:{},Module:da,makeModuleMap:q,nextTick:g.nextTick,onError:A,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");if("string"===typeof a.urlArgs){var b=
+a.urlArgs;a.urlArgs=function(a,c){return(-1===c.indexOf("?")?"?":"&")+b}}var c=p.shim,h={paths:!0,bundles:!0,config:!0,map:!0};D(a,function(a,b){h[b]?(p[b]||(p[b]={}),Y(p[b],a,!0,!0)):p[b]=a});a.bundles&&D(a.bundles,function(a,b){y(a,function(a){a!==b&&(fa[a]=b)})});a.shim&&(D(a.shim,function(a,b){L(a)&&(a={deps:a});!a.exports&&!a.init||a.exportsFn||(a.exportsFn=l.makeShimExports(a));c[b]=a}),p.shim=c);a.packages&&y(a.packages,function(a){var b;a="string"===typeof a?{name:a}:a;b=a.name;a.location&&
+(p.paths[b]=a.location);p.pkgs[b]=a.name+"/"+(a.main||"main").replace(na,"").replace(U,"")});D(t,function(a,b){a.inited||a.map.unnormalized||(a.map=q(b,null,!0))});(a.deps||a.callback)&&l.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ga,arguments));return b||a.exports&&ia(a.exports)}},makeRequire:function(a,n){function m(c,d,f){var e,r;n.enableBuildCallback&&d&&K(d)&&(d.__requireJsBuild=!0);if("string"===typeof c){if(K(d))return A(F("requireargs",
+"Invalid require call"),f);if(a&&x(R,c))return R[c](t[a.id]);if(g.get)return g.get(l,c,a,m);e=q(c,a,!1,!0);e=e.id;return x(v,e)?v[e]:A(F("notloaded",'Module name "'+e+'" has not been loaded yet for context: '+b+(a?"":". Use require([])")))}P();l.nextTick(function(){P();r=u(q(null,a));r.skipMap=n.skipMap;r.init(c,d,f,{enabled:!0});H()});return m}n=n||{};Y(m,{isBrowser:E,toUrl:function(b){var d,f=b.lastIndexOf("."),g=b.split("/")[0];-1!==f&&("."!==g&&".."!==g||1<f)&&(d=b.substring(f,b.length),b=b.substring(0,
+f));return l.nameToUrl(c(b,a&&a.id,!0),d,!0)},defined:function(b){return x(v,q(b,a,!1,!0).id)},specified:function(b){b=q(b,a,!1,!0).id;return x(v,b)||x(t,b)}});a||(m.undef=function(b){B();var c=q(b,a,!0),f=e(t,b);f.undefed=!0;d(b);delete v[b];delete W[c.url];delete ea[b];X(G,function(a,c){a[0]===b&&G.splice(c,1)});delete l.defQueueMap[b];f&&(f.events.defined&&(ea[b]=f.events),C(b))});return m},enable:function(a){e(t,a.id)&&u(a).enable()},completeLoad:function(a){var b,c,d=e(p.shim,a)||{},g=d.exports;
+for(B();G.length;){c=G.shift();if(null===c[0]){c[0]=a;if(b)break;b=!0}else c[0]===a&&(b=!0);I(c)}l.defQueueMap={};c=e(t,a);if(!b&&!x(v,a)&&c&&!c.inited)if(!p.enforceDefine||g&&ia(g))I([a,d.deps||[],d.exportsFn]);else return m(a)?void 0:A(F("nodefine","No define call for "+a,null,[a]));H()},nameToUrl:function(a,b,c){var d,k,f,m;(d=e(p.pkgs,a))&&(a=d);if(d=e(fa,a))return l.nameToUrl(d,b,c);if(g.jsExtRegExp.test(a))d=a+(b||"");else{d=p.paths;k=a.split("/");for(f=k.length;0<f;--f)if(m=k.slice(0,f).join("/"),
+m=e(d,m)){L(m)&&(m=m[0]);k.splice(0,f,m);break}d=k.join("/");d+=b||(/^data\:|^blob\:|\?/.test(d)||c?"":".js");d=("/"===d.charAt(0)||d.match(/^[\w\+\.\-]+:/)?"":p.baseUrl)+d}return p.urlArgs&&!/^blob\:/.test(d)?d+p.urlArgs(a,d):d},load:function(a,b){g.load(l,a,b)},execCb:function(a,b,c,d){return b.apply(d,c)},onScriptLoad:function(a){if("load"===a.type||oa.test((a.currentTarget||a.srcElement).readyState))N=null,a=O(a),l.completeLoad(a.id)},onScriptError:function(a){var b=O(a);if(!m(b.id)){var c=[];
+D(t,function(a,d){0!==d.indexOf("_@r")&&y(a.depMaps,function(a){if(a.id===b.id)return c.push(d),!0})});return A(F("scripterror",'Script error for "'+b.id+(c.length?'", needed by: '+c.join(", "):'"'),a,[b.id]))}}};l.require=l.makeRequire();return l}function pa(){if(N&&"interactive"===N.readyState)return N;X(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return N=b});return N}var g,B,C,H,O,I,N,P,u,T,qa=/(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,ra=/[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,
+U=/\.js$/,na=/^\.\//;B=Object.prototype;var Q=B.toString,la=B.hasOwnProperty,E=!("undefined"===typeof window||"undefined"===typeof navigator||!window.document),ja=!E&&"undefined"!==typeof importScripts,oa=E&&"PLAYSTATION 3"===navigator.platform?/^complete$/:/^(complete|loaded)$/,ca="undefined"!==typeof opera&&"[object Opera]"===opera.toString(),J={},w={},V=[],S=!1;if("undefined"===typeof define){if("undefined"!==typeof requirejs){if(K(requirejs))return;w=requirejs;requirejs=void 0}"undefined"===typeof require||
+K(require)||(w=require,require=void 0);g=requirejs=function(b,c,d,m){var r,q="_";L(b)||"string"===typeof b||(r=b,L(c)?(b=c,c=d,d=m):b=[]);r&&r.context&&(q=r.context);(m=e(J,q))||(m=J[q]=g.s.newContext(q));r&&m.configure(r);return m.require(b,c,d)};g.config=function(b){return g(b)};g.nextTick="undefined"!==typeof setTimeout?function(b){setTimeout(b,4)}:function(b){b()};require||(require=g);g.version="2.2.0";g.jsExtRegExp=/^\/|:|\?|\.js$/;g.isBrowser=E;B=g.s={contexts:J,newContext:ma};g({});y(["toUrl",
+"undef","defined","specified"],function(b){g[b]=function(){var c=J._;return c.require[b].apply(c,arguments)}});E&&(C=B.head=document.getElementsByTagName("head")[0],H=document.getElementsByTagName("base")[0])&&(C=B.head=H.parentNode);g.onError=ha;g.createNode=function(b,c,d){c=b.xhtml?document.createElementNS("http://www.w3.org/1999/xhtml","html:script"):document.createElement("script");c.type=b.scriptType||"text/javascript";c.charset="utf-8";c.async=!0;return c};g.load=function(b,c,d){var m=b&&b.config||
+{},e;if(E){e=g.createNode(m,c,d);e.setAttribute("data-requirecontext",b.contextName);e.setAttribute("data-requiremodule",c);!e.attachEvent||e.attachEvent.toString&&0>e.attachEvent.toString().indexOf("[native code")||ca?(e.addEventListener("load",b.onScriptLoad,!1),e.addEventListener("error",b.onScriptError,!1)):(S=!0,e.attachEvent("onreadystatechange",b.onScriptLoad));e.src=d;if(m.onNodeCreated)m.onNodeCreated(e,m,c,d);P=e;H?C.insertBefore(e,H):C.appendChild(e);P=null;return e}if(ja)try{setTimeout(function(){},
+0),importScripts(d),b.completeLoad(c)}catch(q){b.onError(F("importscripts","importScripts failed for "+c+" at "+d,q,[c]))}};E&&!w.skipDataMain&&X(document.getElementsByTagName("script"),function(b){C||(C=b.parentNode);if(O=b.getAttribute("data-main"))return u=O,w.baseUrl||-1!==u.indexOf("!")||(I=u.split("/"),u=I.pop(),T=I.length?I.join("/")+"/":"./",w.baseUrl=T),u=u.replace(U,""),g.jsExtRegExp.test(u)&&(u=O),w.deps=w.deps?w.deps.concat(u):[u],!0});define=function(b,c,d){var e,g;"string"!==typeof b&&
+(d=c,c=b,b=null);L(c)||(d=c,c=null);!c&&K(d)&&(c=[],d.length&&(d.toString().replace(qa,ka).replace(ra,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));S&&(e=P||pa())&&(b||(b=e.getAttribute("data-requiremodule")),g=J[e.getAttribute("data-requirecontext")]);g?(g.defQueue.push([b,c,d]),g.defQueueMap[b]=!0):V.push([b,c,d])};define.amd={jQuery:!0};g.exec=function(b){return eval(b)};g(w)}})(this);

+ 391 - 0
scripts/text.js

@@ -0,0 +1,391 @@
+/**
+ * @license RequireJS text 2.0.14 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved.
+ * Available via the MIT or new BSD license.
+ * see: http://github.com/requirejs/text for details
+ */
+/*jslint regexp: true */
+/*global require, XMLHttpRequest, ActiveXObject,
+  define, window, process, Packages,
+  java, location, Components, FileUtils */
+
+define(['module'], function (module) {
+    'use strict';
+
+    var text, fs, Cc, Ci, xpcIsWindows,
+        progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'],
+        xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im,
+        bodyRegExp = /<body[^>]*>\s*([\s\S]+)\s*<\/body>/im,
+        hasLocation = typeof location !== 'undefined' && location.href,
+        defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''),
+        defaultHostName = hasLocation && location.hostname,
+        defaultPort = hasLocation && (location.port || undefined),
+        buildMap = {},
+        masterConfig = (module.config && module.config()) || {};
+
+    text = {
+        version: '2.0.14',
+
+        strip: function (content) {
+            //Strips <?xml ...?> declarations so that external SVG and XML
+            //documents can be added to a document without worry. Also, if the string
+            //is an HTML document, only the part inside the body tag is returned.
+            if (content) {
+                content = content.replace(xmlRegExp, "");
+                var matches = content.match(bodyRegExp);
+                if (matches) {
+                    content = matches[1];
+                }
+            } else {
+                content = "";
+            }
+            return content;
+        },
+
+        jsEscape: function (content) {
+            return content.replace(/(['\\])/g, '\\$1')
+                .replace(/[\f]/g, "\\f")
+                .replace(/[\b]/g, "\\b")
+                .replace(/[\n]/g, "\\n")
+                .replace(/[\t]/g, "\\t")
+                .replace(/[\r]/g, "\\r")
+                .replace(/[\u2028]/g, "\\u2028")
+                .replace(/[\u2029]/g, "\\u2029");
+        },
+
+        createXhr: masterConfig.createXhr || function () {
+            //Would love to dump the ActiveX crap in here. Need IE 6 to die first.
+            var xhr, i, progId;
+            if (typeof XMLHttpRequest !== "undefined") {
+                return new XMLHttpRequest();
+            } else if (typeof ActiveXObject !== "undefined") {
+                for (i = 0; i < 3; i += 1) {
+                    progId = progIds[i];
+                    try {
+                        xhr = new ActiveXObject(progId);
+                    } catch (e) {}
+
+                    if (xhr) {
+                        progIds = [progId];  // so faster next time
+                        break;
+                    }
+                }
+            }
+
+            return xhr;
+        },
+
+        /**
+         * Parses a resource name into its component parts. Resource names
+         * look like: module/name.ext!strip, where the !strip part is
+         * optional.
+         * @param {String} name the resource name
+         * @returns {Object} with properties "moduleName", "ext" and "strip"
+         * where strip is a boolean.
+         */
+        parseName: function (name) {
+            var modName, ext, temp,
+                strip = false,
+                index = name.lastIndexOf("."),
+                isRelative = name.indexOf('./') === 0 ||
+                             name.indexOf('../') === 0;
+
+            if (index !== -1 && (!isRelative || index > 1)) {
+                modName = name.substring(0, index);
+                ext = name.substring(index + 1);
+            } else {
+                modName = name;
+            }
+
+            temp = ext || modName;
+            index = temp.indexOf("!");
+            if (index !== -1) {
+                //Pull off the strip arg.
+                strip = temp.substring(index + 1) === "strip";
+                temp = temp.substring(0, index);
+                if (ext) {
+                    ext = temp;
+                } else {
+                    modName = temp;
+                }
+            }
+
+            return {
+                moduleName: modName,
+                ext: ext,
+                strip: strip
+            };
+        },
+
+        xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/,
+
+        /**
+         * Is an URL on another domain. Only works for browser use, returns
+         * false in non-browser environments. Only used to know if an
+         * optimized .js version of a text resource should be loaded
+         * instead.
+         * @param {String} url
+         * @returns Boolean
+         */
+        useXhr: function (url, protocol, hostname, port) {
+            var uProtocol, uHostName, uPort,
+                match = text.xdRegExp.exec(url);
+            if (!match) {
+                return true;
+            }
+            uProtocol = match[2];
+            uHostName = match[3];
+
+            uHostName = uHostName.split(':');
+            uPort = uHostName[1];
+            uHostName = uHostName[0];
+
+            return (!uProtocol || uProtocol === protocol) &&
+                   (!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) &&
+                   ((!uPort && !uHostName) || uPort === port);
+        },
+
+        finishLoad: function (name, strip, content, onLoad) {
+            content = strip ? text.strip(content) : content;
+            if (masterConfig.isBuild) {
+                buildMap[name] = content;
+            }
+            onLoad(content);
+        },
+
+        load: function (name, req, onLoad, config) {
+            //Name has format: some.module.filext!strip
+            //The strip part is optional.
+            //if strip is present, then that means only get the string contents
+            //inside a body tag in an HTML string. For XML/SVG content it means
+            //removing the <?xml ...?> declarations so the content can be inserted
+            //into the current doc without problems.
+
+            // Do not bother with the work if a build and text will
+            // not be inlined.
+            if (config && config.isBuild && !config.inlineText) {
+                onLoad();
+                return;
+            }
+
+            masterConfig.isBuild = config && config.isBuild;
+
+            var parsed = text.parseName(name),
+                nonStripName = parsed.moduleName +
+                    (parsed.ext ? '.' + parsed.ext : ''),
+                url = req.toUrl(nonStripName),
+                useXhr = (masterConfig.useXhr) ||
+                         text.useXhr;
+
+            // Do not load if it is an empty: url
+            if (url.indexOf('empty:') === 0) {
+                onLoad();
+                return;
+            }
+
+            //Load the text. Use XHR if possible and in a browser.
+            if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) {
+                text.get(url, function (content) {
+                    text.finishLoad(name, parsed.strip, content, onLoad);
+                }, function (err) {
+                    if (onLoad.error) {
+                        onLoad.error(err);
+                    }
+                });
+            } else {
+                //Need to fetch the resource across domains. Assume
+                //the resource has been optimized into a JS module. Fetch
+                //by the module name + extension, but do not include the
+                //!strip part to avoid file system issues.
+                req([nonStripName], function (content) {
+                    text.finishLoad(parsed.moduleName + '.' + parsed.ext,
+                                    parsed.strip, content, onLoad);
+                });
+            }
+        },
+
+        write: function (pluginName, moduleName, write, config) {
+            if (buildMap.hasOwnProperty(moduleName)) {
+                var content = text.jsEscape(buildMap[moduleName]);
+                write.asModule(pluginName + "!" + moduleName,
+                               "define(function () { return '" +
+                                   content +
+                               "';});\n");
+            }
+        },
+
+        writeFile: function (pluginName, moduleName, req, write, config) {
+            var parsed = text.parseName(moduleName),
+                extPart = parsed.ext ? '.' + parsed.ext : '',
+                nonStripName = parsed.moduleName + extPart,
+                //Use a '.js' file name so that it indicates it is a
+                //script that can be loaded across domains.
+                fileName = req.toUrl(parsed.moduleName + extPart) + '.js';
+
+            //Leverage own load() method to load plugin value, but only
+            //write out values that do not have the strip argument,
+            //to avoid any potential issues with ! in file names.
+            text.load(nonStripName, req, function (value) {
+                //Use own write() method to construct full module value.
+                //But need to create shell that translates writeFile's
+                //write() to the right interface.
+                var textWrite = function (contents) {
+                    return write(fileName, contents);
+                };
+                textWrite.asModule = function (moduleName, contents) {
+                    return write.asModule(moduleName, fileName, contents);
+                };
+
+                text.write(pluginName, nonStripName, textWrite, config);
+            }, config);
+        }
+    };
+
+    if (masterConfig.env === 'node' || (!masterConfig.env &&
+            typeof process !== "undefined" &&
+            process.versions &&
+            !!process.versions.node &&
+            !process.versions['node-webkit'] &&
+            !process.versions['atom-shell'])) {
+        //Using special require.nodeRequire, something added by r.js.
+        fs = require.nodeRequire('fs');
+
+        text.get = function (url, callback, errback) {
+            try {
+                var file = fs.readFileSync(url, 'utf8');
+                //Remove BOM (Byte Mark Order) from utf8 files if it is there.
+                if (file[0] === '\uFEFF') {
+                    file = file.substring(1);
+                }
+                callback(file);
+            } catch (e) {
+                if (errback) {
+                    errback(e);
+                }
+            }
+        };
+    } else if (masterConfig.env === 'xhr' || (!masterConfig.env &&
+            text.createXhr())) {
+        text.get = function (url, callback, errback, headers) {
+            var xhr = text.createXhr(), header;
+            xhr.open('GET', url, true);
+
+            //Allow plugins direct access to xhr headers
+            if (headers) {
+                for (header in headers) {
+                    if (headers.hasOwnProperty(header)) {
+                        xhr.setRequestHeader(header.toLowerCase(), headers[header]);
+                    }
+                }
+            }
+
+            //Allow overrides specified in config
+            if (masterConfig.onXhr) {
+                masterConfig.onXhr(xhr, url);
+            }
+
+            xhr.onreadystatechange = function (evt) {
+                var status, err;
+                //Do not explicitly handle errors, those should be
+                //visible via console output in the browser.
+                if (xhr.readyState === 4) {
+                    status = xhr.status || 0;
+                    if (status > 399 && status < 600) {
+                        //An http 4xx or 5xx error. Signal an error.
+                        err = new Error(url + ' HTTP status: ' + status);
+                        err.xhr = xhr;
+                        if (errback) {
+                            errback(err);
+                        }
+                    } else {
+                        callback(xhr.responseText);
+                    }
+
+                    if (masterConfig.onXhrComplete) {
+                        masterConfig.onXhrComplete(xhr, url);
+                    }
+                }
+            };
+            xhr.send(null);
+        };
+    } else if (masterConfig.env === 'rhino' || (!masterConfig.env &&
+            typeof Packages !== 'undefined' && typeof java !== 'undefined')) {
+        //Why Java, why is this so awkward?
+        text.get = function (url, callback) {
+            var stringBuffer, line,
+                encoding = "utf-8",
+                file = new java.io.File(url),
+                lineSeparator = java.lang.System.getProperty("line.separator"),
+                input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)),
+                content = '';
+            try {
+                stringBuffer = new java.lang.StringBuffer();
+                line = input.readLine();
+
+                // Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324
+                // http://www.unicode.org/faq/utf_bom.html
+
+                // Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK:
+                // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058
+                if (line && line.length() && line.charAt(0) === 0xfeff) {
+                    // Eat the BOM, since we've already found the encoding on this file,
+                    // and we plan to concatenating this buffer with others; the BOM should
+                    // only appear at the top of a file.
+                    line = line.substring(1);
+                }
+
+                if (line !== null) {
+                    stringBuffer.append(line);
+                }
+
+                while ((line = input.readLine()) !== null) {
+                    stringBuffer.append(lineSeparator);
+                    stringBuffer.append(line);
+                }
+                //Make sure we return a JavaScript string and not a Java string.
+                content = String(stringBuffer.toString()); //String
+            } finally {
+                input.close();
+            }
+            callback(content);
+        };
+    } else if (masterConfig.env === 'xpconnect' || (!masterConfig.env &&
+            typeof Components !== 'undefined' && Components.classes &&
+            Components.interfaces)) {
+        //Avert your gaze!
+        Cc = Components.classes;
+        Ci = Components.interfaces;
+        Components.utils['import']('resource://gre/modules/FileUtils.jsm');
+        xpcIsWindows = ('@mozilla.org/windows-registry-key;1' in Cc);
+
+        text.get = function (url, callback) {
+            var inStream, convertStream, fileObj,
+                readData = {};
+
+            if (xpcIsWindows) {
+                url = url.replace(/\//g, '\\');
+            }
+
+            fileObj = new FileUtils.File(url);
+
+            //XPCOM, you so crazy
+            try {
+                inStream = Cc['@mozilla.org/network/file-input-stream;1']
+                           .createInstance(Ci.nsIFileInputStream);
+                inStream.init(fileObj, 1, 0, false);
+
+                convertStream = Cc['@mozilla.org/intl/converter-input-stream;1']
+                                .createInstance(Ci.nsIConverterInputStream);
+                convertStream.init(inStream, "utf-8", inStream.available(),
+                Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
+
+                convertStream.readString(inStream.available(), readData);
+                convertStream.close();
+                inStream.close();
+                callback(readData.value);
+            } catch (e) {
+                throw new Error((fileObj && fileObj.path || '') + ': ' + e);
+            }
+        };
+    }
+    return text;
+});

+ 190 - 0
src/app.html

@@ -0,0 +1,190 @@
+<template>
+    <require from="./scss/styles.css"></require>    
+    <require from="./sampler-visual"></require>
+    <require from="./number-format"></require>
+    <main id="factbox">
+        <header>
+            <div class="title-row">
+                <textarea id="factbox-title"
+                          type="text"
+                          name="factbox-title"
+                          class="form-item textarea factbox-title"
+                          rows="1"
+                          placeholder="${labels.title[selectedLanguage]}"
+                          value.bind="title[selectedLanguage]"
+                          change.delegate="updateExportData($event.target)"
+                          focus.trigger="textareaFocused($event)"
+                          keyup.delegate="textareaAutoResize($event.target)">
+                </textarea>
+            </div>
+            <textarea id="factbox-description"
+                name="factbox-description"
+                class="form-item textarea factbox-description"
+                placeholder="${labels.description[selectedLanguage]}"
+                value.bind="description[selectedLanguage]"
+                rows="1"
+                change.delegate="updateExportData()"
+                focus.trigger="textareaFocused($event)"
+                keyup.delegate="textareaAutoResize($event.target)">
+            </textarea>
+            <textarea id="factbox-summary"
+                name="factbox-summary"
+                class="form-item textarea factbox-summary"
+                rows="1"
+                placeholder="${labels.summary[selectedLanguage]}"
+                value.bind="summary[selectedLanguage]"
+                focus.trigger="textareaFocused($event)"
+                change.delegate="updateExportData()"
+                keyup.delegate="textareaAutoResize($event.target)">
+            </textarea>
+        </header>
+
+        <section class="samplers">
+                <div repeat.for="sampler of samplers"
+                    class="sampler sampler-wrapper-${samplers.indexOf(sampler)}"
+                    id="sampler-wrapper-${samplers.indexOf(sampler)}">
+
+                    <div class="reference-class">
+                        <label innerHTML.bind="selectedSize"
+                            class="form-item referenceClass"></label>
+                        <input
+                            type="text"
+                            name="stitle"
+                            class="form-item"
+                            id="stitle"
+                            placeholder="${labels.referenceClasses[selectedLanguage][samplers.indexOf(sampler)]}"
+                            value.bind="referenceClasses[selectedLanguage][samplers.indexOf(sampler)]"
+                            change.delegate="updateExportData()"
+                        />
+                    </div>
+                    <section as-element="sampler-visual" class="iconarray"
+                        sampler.bind="sampler"
+                        blocks.bind="iconArraySizes[selectedSize].blocks"
+                        blocksize.bind="iconArraySizes[selectedSize].blocksize"
+                        blockspacing-ratio.bind="iconArraySizes[selectedSize].blockspacingRatio"
+                        radius.bind="iconArraySizes[selectedSize].radius"
+                        dotspacing-ratio.bind="iconArraySizes[selectedSize].dotspacingRatio"
+                        icon.bind="icons[selectedIcon]"
+                        soffsets.bind="soffsets"
+                        voffsets.bind="voffsets[samplers.indexOf(sampler)]"
+                        id.bind="samplers.indexOf(sampler)"
+                        classoffset.bind="iconArraySizes[selectedSize].classOffset"
+                        max-samples.bind="selectedSize">
+                    </section>
+                </div>
+        </section>
+
+        <aside>
+            <div class="legend-references">
+                <section class="legend">
+                    <div repeat.for="cix of classes.length" class="legendgroup legend-${cix}">
+                        <form class="form-inline">
+                            <input
+                                id="colour-${cix}"
+                                change.delegate="colourChanged($event)"
+                                value.bind="classes[cix].colour"
+                                style="background-color:${classes[cix].colour}"
+                                class="form-item input-colour"
+                                type="color" />
+                            <!-- <label for="colour-${cix}" innerHTML.bind="classes[cix].colour"></label> -->
+                        </form>
+                        <textarea
+                            change.delegate="updateExportData()"
+                            focus.trigger="textareaFocused($event)"
+                            keyup.delegate="textareaAutoResize($event.target)"
+                            placeholder="${labels.classes[cix].name[selectedLanguage]}"
+                            value.bind="classes[cix].name[selectedLanguage]"
+                            rows="1"
+                            class="form-item textarea class-description">
+                        </textarea>
+                        <label if.bind="cix === 0" repeat.for="sampler of samplers"
+                               innerHTML.bind="sampler.remainingSamples"
+                               class="form-item align-right group-${samplers.indexOf(sampler) + 1}">
+                        </label>
+                        <!-- screen -->
+                        <input if.bind="cix !== 0" repeat.for="sampler of samplers"
+                               change.delegate="sampler.changeClassSize(cix, $event)"
+                               value.bind="sampler.samplesPerClass[cix] | numberFormat"
+                               type="number"
+                               class="form-item align-right group-${samplers.indexOf(sampler) + 1}"
+                               min="0" />
+                        <!-- print -->
+                        <label if.bind="cix !== 0" repeat.for="sampler of samplers"
+                               innerHTML.bind="sampler.samplesPerClass[cix]"
+                               class="form-item group-${samplers.indexOf(sampler) + 1}">
+                        </label>
+                        <button class="form-item remove-class b-${cix}" click.delegate="removeClass(cix, $event)">
+                            <i class="fa fa-minus-square-o" aria-hidden="true"></i>
+                        </button>
+                        <button class="form-item add-class" click.delegate="addNewClass(cix, $event)" if.bind="cix === classes.length - 1 && classes.length < maxClasses">
+                            <i class="fa fa-plus-square-o" aria-hidden="true"></i>
+                        </button>
+                    </div>
+                </section>
+                <section class="references">
+                    <label class="">${labels.source[selectedLanguage]}:</label>
+                    <textarea name="reference-all"
+                              class="textarea references-text form-item"
+                              rows="1"
+                              placeholder="${labels.reference[selectedLanguage]}"
+                              value.bind="reference[selectedLanguage]"
+                              change.delegate="updateExportData()"
+                              focus.trigger="textareaFocused($event)"
+                              keyup.delegate="textareaAutoResize($event.target)">
+                    </textarea>
+                </section>
+            </div>
+            <section class="meta-data">
+                <input class="url form-item"
+                    placeholder="${labels.url[selectedLanguage]}"
+                    value.bind="url[selectedLanguage]"
+                    change.delegate="updateExportData()" />
+                <fieldset>
+                    <label class="form-item last-update" for="lastUpdate">${labels.update.label[selectedLanguage]}:</label>
+                    <input class="last-update form-item"
+                        type="text"
+                        name="lastUpdate"
+                        id="lastUpdate"
+                        value.bind="lastUpdate[selectedLanguage]"
+                        placeholder="${labels.update.value[selectedLanguage]}"
+                        change.delegate="updateExportData()" />
+                </fieldset>
+            </section>
+        </aside>
+
+        <footer class="controls">
+            <section class="settings">
+                <form class="size form-inline">
+                    <label for="sizeSelection" class="form-item" innerHTML.bind="labels.controls.iconArraySize[selectedLanguage]"></label>
+                    <select id="sizeSelection" class="form-item" change.delegate="iconArraySizeChanged($event)">
+                        <option value="1000">1000</option>
+                        <option value="100">100</option> 
+                    </select>
+                </form>
+                <form class="language form-inline">
+                    <label class="form-item" for="languageSelection" innerHTML.bind="labels.controls.language[selectedLanguage]"></label>
+                    <select id="languageSelection" value.bind="selectedLanguage" class="languageSelection form-item" change.delegate="languageChanged()">
+                        <option repeat.for="language of languages" model.bind="language.id">${language.name}</option> 
+                    </select>
+                </form>
+            </section>
+            <section class="importexport">
+                <form class="export-json form-inline">
+                    <a href="#" id="exportJSON" class="export form-item" aria-role="button" download.bind="jsonFilename">
+                        Data Export&nbsp;<i class="fa fa-download" aria-hidden="true"></i>
+                    </a>
+                </form>
+                <form class="export-pdf form-inline">
+                    <a class="form-item export" href.bind="pdfURL" id="exportPDF" aria-role="button" download.bind="pdfFilename">
+                        PDF Export&nbsp;<i class="fa fa-download" aria-hidden="true"></i>
+                    </a>
+                </form>
+                <form class="import form-inline">
+                    <input class="import input-file" type="file" id="upload" change.delegate="importJSON()" accept=".json">
+                    <label class="import form-item" for="upload">Import Data&nbsp;<i class="fa fa-upload" aria-hidden="true"></i></label>
+                </form>
+            </section>
+            <iframe id="preview" src.bind="pdfURL" frameborder="0"></iframe>
+        </footer>
+    </main>
+</template>

+ 364 - 0
src/app.js

@@ -0,0 +1,364 @@
+import { HttpClient } from 'aurelia-fetch-client';
+
+import * as d3 from './d3custom';
+
+import { inject } from 'aurelia-framework';
+import { EventAggregator } from 'aurelia-event-aggregator';
+
+import {
+    NumberOfSamplesChanged,     // icon array size selected: fbe ->...???
+    ClassSizeChanged,           // new samples drawn: sampler -> ...
+    OffsetUpdated,              // new offset calculated for sampler vis: sampler-visual -> fbe
+    OffsetsSynced,              // new synchronised offset calculated: fbe -> sampler-visual*
+    RedrawIconArray             // colour changed / icon selected (draw from scratch): fbe -> sampler-visual*
+} from './messages';
+
+import { Sampler } from './sampler';
+import { FactBoxConfiguration } from './configuration';
+
+
+@inject(FactBoxConfiguration, EventAggregator)
+
+export class App {
+
+    // user's choices
+    pdfURL = '';
+    jsonURL = '';
+
+    pdfWorker = undefined;
+
+    selectedLanguage = 'de';
+    selectedIcon = 0;
+    selectedSize = 1000;
+    selectedSizeId = 1;
+
+    maxClasses = 5;
+    iaGroupSpacing = 0.0;
+    iaClassSpacing = 0;
+
+    fontsloaded = 0;
+    fontData = {
+        'roman': { 'name': 'Verdana', 'url': 'src/resources/Verdana.ttf', 'data': undefined },
+        'bold': { 'name': 'Verdana Bold', 'url': 'src/resources/Verdana Bold.ttf', 'data': undefined }
+    }
+
+    baseFontSize = 8;
+
+    constructor(configuration, eventaggregator) {
+
+        this.ea = eventaggregator;
+
+        for (let key in configuration) {
+            this[key] = configuration[key];
+        }
+
+        let option = window.location.search.substr(1, 4);
+        if (option === 'lang') {
+            this.selectedLanguage = window.location.search.substr(6, window.location.search.length - 1);
+        } else {
+            this.languageChanged();
+        }
+
+        this.initialise();
+
+        // event listeners
+        this.ea.subscribe(OffsetUpdated, msg => {
+            let cidx = msg.cidx - 1;
+            this.soffsets[cidx] = Math.max(this.voffsets[0][cidx], this.voffsets[1][cidx]);
+            this.ea.publish(new OffsetsSynced(cidx));
+        });
+
+        this.ea.subscribe(ClassSizeChanged, msg => {
+            this.updateExportData();
+        });
+
+        this.httpClient = new HttpClient();
+        this.httpClient.configure(config => {
+            config.useStandardConfiguration();
+        });
+    }
+
+    attached() {
+        for (let md in this.fontData) {
+            let cfd = this.fontData[md];
+
+            this.httpClient.fetch(cfd.url)
+                .then(response => response.arrayBuffer())
+                .then(data => {
+                    cfd.data = data;
+                    this.fontsloaded += 1;
+                    this.updateExportData();
+
+                    if (this.fontsloaded === Object.keys(this.fontData).length) {
+                        this.createWorker();
+                    }
+                });
+        }
+    }
+
+    createWorker() {
+        // employ a proper pdf worker after all fonts are loaded
+        if (window.Worker) {
+
+            this.pdfWorker = new Worker('src/pdfWorker.js');
+
+            this.pdfWorker.onmessage = (e) => {  // I need 'this'
+                this.pdfURL = e.data;
+            };
+
+            this.updatePDF();
+
+        } else {
+            // console.log('window.Worker is not supported by this browser');
+        }
+    }
+
+    languageChanged() {
+        window.history.replaceState(
+            {},
+            this.labels.appTitle[this.selectedLanguage],
+            window.location.origin + window.location.pathname + '?lang=' + this.selectedLanguage
+        );
+        document.title = this.labels.appTitle[this.selectedLanguage];
+        if (this.pdfWorker) {
+            this.updatePDF();
+        }
+    }
+
+    updatePDF() {
+
+        if (this.pdfURL) {
+            window.URL.revokeObjectURL(this.pdfURL);
+        }
+
+        let ccsamples = [];
+        let spc = [];
+        for (let i = 0; i < 2; ++i) {
+            ccsamples.push(this.concatenateSamplesArrays(i));
+            spc.push(this.samplers[i].samplesPerClass);
+        }
+
+        this.pdfWorker.postMessage(
+            {
+                'appTitle': this.labels.appTitle,
+                'baseFontSize': this.baseFontSize,
+                'ccsamples': ccsamples,
+                'classes': this.classes,
+                'description': this.description,
+                'fontData': this.fontData,
+                'iconArraySizes': this.iconArraySizes,
+                'labels': this.labels,
+                'lastUpdate': this.lastUpdate,
+                'reference': this.reference,
+                'referenceClasses': this.referenceClasses,
+                'samplesPerClass': spc,
+                'summary': this.summary,
+                'selectedSize': this.selectedSize,
+                'selectedLanguage': this.selectedLanguage,
+                'title': this.title,
+                'url': this.url
+            }
+        );
+    }
+
+    createFileNames() {
+        let d = new Date();
+        let filename = d.getFullYear().toString() +
+            (d.getMonth() + 1).toString() +
+            d.getDate().toString() +
+            this.title[this.selectedLanguage] +
+            '_' + this.selectedLanguage;
+        this.jsonFilename = filename + '.json';
+        this.svgFilename = filename + '.svg';
+        this.pdfFilename = filename + '.pdf';
+    }
+
+    // inititialise samplers, offsets for visualisation
+    initialise() {
+        this.samples = [];
+        this.samplers = [];
+        this.voffsets = [];
+        this.soffsets = [];
+
+        for (let i = 0; i < 2; ++i) {
+            let samplespc = [];
+            samplespc[0] = this.selectedSize;
+            for (let cix = 1; cix < this.classes.length; cix++) {
+                samplespc[cix] = 0;
+            }
+            this.samples.push(samplespc);
+            this.samplers.push(new Sampler(this.ea, samplespc, this.classes, i));
+
+            //initialise visual offsets
+            let vo = [];
+            for (let c = 1; c < this.classes.length; ++c) {  // no offset for the default event
+                vo[c - 1] = 0;
+            }
+            this.voffsets[i] = vo;
+        }
+
+        // initialise synchronised offsets
+        for (let i = 1; i < this.classes.length; ++i) {
+            this.soffsets[i - 1] = this.iconArraySizes[this.selectedSize].classOffset;
+        }
+    }
+
+    // QUICK HACK for pdf generation. TODO: create pdf creator component
+    concatenateSamplesArrays(sidx) {
+        let samplesIsolated = this.samplers[sidx].sortedSamples();
+        let samplesConcat = [];
+
+        let iaconf = this.iconArraySizes[this.selectedSize];
+
+        // create linear array of samples to meet the FACT BOX LAYOUT criteria, use complementary event class as filler class
+        for (let i = 1; i < samplesIsolated.length; ++i) {
+
+            for (let j = 0; j < samplesIsolated[i].length; ++j) {  // fill array with all samples of current class
+                samplesConcat.push(i);
+            }
+            // fill up block with complementary class
+            for (let k = 0; k < this.soffsets[i - 1] * iaconf.blocks * iaconf.blocksize.width * iaconf.blocksize.height - samplesIsolated[i].length; ++k) {
+                samplesConcat.push(samplesIsolated[0].shift());
+            }
+        }
+
+        let sclength = samplesConcat.length;
+        for (let i = 0; i < this.selectedSize - sclength; ++i) {  // fill up the array with either base class id or undefined (placeholder)
+            samplesConcat.push(samplesIsolated[0].shift());
+        }
+        return samplesConcat;
+    }
+    // <<< QUICK HACK for pdf generation
+
+    textareaFocused(event) {
+        event.target.focus();
+        setTimeout(function() { event.target.select(); }, 1);
+    }
+
+    textareaAutoResize(element) {
+        element.style.height = 'auto';
+        element.style.height = (element.scrollHeight) + 'px';
+    }
+
+    redrawIconArrays(fromScratch, e) {
+        this.ea.publish(new RedrawIconArray(fromScratch));
+        this.updateExportData();
+    }
+
+    // number of total samples changed
+    iconArraySizeChanged(e) {
+        this.selectedSize = parseInt(e.target.value);
+        this.initialise();
+        this.ea.publish(new NumberOfSamplesChanged(this.iconArraySizes[this.selectedSize]));
+        this.updateExportData();
+    }
+
+    addNewClass(idx, e) {
+        if (this.classes.length < this.maxClasses) {  // limit number of classes
+
+            let newClass = {
+                id: this.classes[this.classes.length - 1].id + 1,  // new id is id of last element + 1
+                colour: this.fcolours[this.classes.length],
+                name: { 'de': '', 'en': '' }
+            };
+            this.classes.push(newClass);
+
+            this.samples.forEach(function(el) {
+                el.push(0);
+            });
+
+            this.updateExportData();
+        }
+    }
+
+    removeClass(cidx, e) {
+        this.classes.splice(cidx, 1);
+        this.updateExportData();
+    }
+
+    // TODO: delete, also from html
+    colourChanged(e) {
+        e.target.style.backgroundColor = e.target.value;
+        this.ea.publish(new RedrawIconArray(false));
+        this.updateExportData();
+    }
+
+    // import / export
+    importJSON() {
+        let up = document.querySelector('#upload');
+
+        let reader = new FileReader();
+        reader.readAsText(up.files[0]);
+        // reader.onload = function(e){
+        reader.onload = (e) => {  // I need 'this'
+            let data = JSON.parse(e.target.result);
+            for (let key in data) {
+                this[key] = data[key];
+            }
+            for (let i = 0; i < 2; ++i) {
+
+                let surv = this.samplers[i];
+                surv.classes = this.classes;
+                surv.samplesPerClass = this.samples[i];  // when assigned inside following loop, inputs / labels don't get updated?!?$%^&
+                for (let j = 0; j < surv.classes.length; ++j) {
+                //     surv.samplesPerClass[j] = this.samples[i][j];
+                    surv.changeClassSize(j, null);  // just declare every class as dirty...
+                }
+                // this.remainingSamples[i] = surv.samplesPerClass[0];
+            }
+        };
+        if (this.pdfWorker) {
+            this.updatePDF();
+        }
+    }
+
+    updateJSONExport() {
+        this.createFileNames();
+
+        if (this.jsonURL) {
+            window.URL.revokeObjectURL(this.jsonURL);
+        }
+
+        let exportData = {
+            title: this.title,
+            description: this.description,
+            summary: this.summary,
+            selectedSize: this.selectedSize,
+            samples: this.samplers.map(function(s) {
+                return s.samplesPerClass;
+            }),
+            classes: this.classes.map(function(i) {
+                return { colour: i.colour, name: i.name, id: i.id };
+            }),
+            reference: this.reference,
+            referenceClasses: this.referenceClasses,
+            selectedLanguage: this.selectedLanguage,
+            lastUpdate: this.lastUpdate,
+            url: this.url
+        };
+        return new Blob([JSON.stringify(exportData)], {type: 'application/json'});
+    }
+
+    updateSVGExport() {
+        let svgString = '';
+        let exportData = d3.selectAll('svg');
+        let serial = new XMLSerializer();
+
+        for (let svgnode of exportData.nodes()) {
+            svgString += serial.serializeToString(svgnode);
+        }
+        return new Blob([svgString], {type: 'application/xml'});
+    }
+
+    updateExportData(element) {
+
+        let blob = this.updateJSONExport();
+        this.jsonURL = URL.createObjectURL(blob);
+        let link = document.querySelector('#exportJSON');
+        link.href = this.jsonURL;
+
+        if (this.pdfWorker) {
+            this.updatePDF();
+        }
+    }
+}

+ 230 - 0
src/configuration.js

@@ -0,0 +1,230 @@
+export class FactBoxConfiguration {
+
+    hcVersion = false;
+
+    colours = {
+        bright: [
+            '#847d71',
+            '#0d47dd',
+            '#0dafdd',
+            '#0ddda3',
+            '#0ddd3b',
+            '#afdd0d',
+            '#dda30d',
+            '#dd3b0d',
+            '#dd0d47',
+            '#dd0daf'
+        ],
+        medium: [
+            // '#5b564e',
+            // '#a8a399',
+            '#cbc8c2',
+            '#0a36a5',
+            '#0a83a5',
+            '#0aa57a',
+            '#0aa52c',
+            '#83a50a',
+            '#a57a0a',
+            '#a52c0a',
+            '#a50a36',
+            '#a50a83'
+        ],
+        dark: [
+            '#322f2b',
+            '#07246d',
+            '#07576d',
+            '#076d50',
+            '#076d1d',
+            '#576d07',
+            '#6d5007',
+            '#6d1d07',
+            '#6d0724',
+            '#6d0757'
+        ]
+    };
+
+    languagesrcs = {
+        'de': 'src/resources/HardingCenter_Logo_de.svg',
+        'en': 'src/resources/HardingCenter_Logo_en.svg'
+    };
+
+    languages = [
+        { id: 'de', name: 'deutsch', logourl: './src/resources/HardingCenter_Logo_de.svg' },
+        { id: 'en', name: 'english', logourl: './src/resources/HardingCenter_Logo_en.svg' }
+    ];
+
+    labels = {
+        appTitle: {
+            'de': 'Faktenbox Editor',
+            'en': 'Fact Box Editor'
+        },
+        title: {
+            'de': 'Faktenboxtitel',
+            'en': 'Fact Box Title'
+        },
+        classes: [
+            {
+                name: {
+                    'de': 'Sonstige',
+                    'en': 'Remaining'
+                }
+            }, {
+                name: {
+                    'de': 'Klasse',
+                    'en': 'Class'
+                }
+            }, {
+                name: {
+                    'de': 'Klasse',
+                    'en': 'Class'
+                }
+            }, {
+                name: {
+                    'de': 'Klasse',
+                    'en': 'Class'
+                }
+            }, {
+                name: {
+                    'de': 'Klasse',
+                    'en': 'Class'
+                }
+            }
+        ],
+        description: {
+            'de': 'Faktenboxbescheibung',
+            'en': 'Explanation of the Fact Box'
+        },
+        referenceClasses: {
+            'de': ['Referenzgruppe 1', 'Referenzgruppe 2'],
+            'en': ['Reference Group 1', 'Reference Group 2']
+        },
+        reference: {
+            'de': 'Cochrane Datenbank of systematic reviews',
+            'en': 'Cochrane database of systematic reviews'
+        },
+        summary: {
+            'de': 'Kurze Zusammenfassung der Faktenbox',
+            'en': 'Short Summary of the Fact Box'
+        },
+        source: {
+            'de': 'Quelle',
+            'en': 'Source'
+        },
+        update: {
+            label: {
+                'de': 'Letztes Update',
+                'en': 'Last update'
+            },
+            value: {
+                'de': '27. März 2018',
+                'en': 'March 27 2018'
+            }
+        },
+        url: {
+            'de': 'http://harding-center.mpg.de/de/faktenboxen',
+            'en': 'http://harding-center.mpg.de/en/factboxes'
+        },
+        controls: {
+            'rawExport': {
+                'de': 'Daten Export',
+                'en': 'Data Export'
+            },
+            'pdfExport': {
+                'de': 'PDF Export',
+                'en': 'PDF export'
+            },
+            'fileImport': {
+                'de': 'Daten Import',
+                'en': 'Data Import'
+            },
+            'iconArraySize': {
+                'de': 'Größe Icon Array',
+                'en': 'Icon Array Size'
+            },
+            'language': {
+                'de': 'Sprache',
+                'en': 'Language'
+            }
+        }
+    };
+
+    title = {
+        'de': '',
+        'en': ''
+    };
+
+    referenceClasses = {
+        'de': ['', ''],
+        'en': ['', '']
+    };
+
+    description = {
+        'de': '',
+        'en': ''
+    };
+
+    summary = {
+        'de': '',
+        'en': ''
+    };
+
+    reference = {
+        'de': '',
+        'en': ''
+    };
+    lastUpdate = {
+        'de': '',
+        'en': ''
+    };
+    url = {
+        'de': '',
+        'en': ''
+    };
+
+    classes = [
+        { id: 0, name: { 'de': '', 'en': '' }, colour: '' },
+        { id: 1, name: { 'de': '', 'en': '' }, colour: '' },
+        { id: 2, name: { 'de': '', 'en': '' }, colour: '' }
+    ];
+
+    fcolours = this.colours.medium.filter(function(e, i) {
+        if (i % 2 === 0) {
+            return e;
+        }
+    });
+    classes = this.classes.map(function(it, idx) {
+        it.colour = this.fcolours[idx];
+        return it;
+    }, this);
+
+    icons = [
+        { id: 0, name: 'dot' },
+        { id: 1, name: 'pregnant' },
+        { id: 2, name: 'gerd' }
+    ];
+
+    iconArraySizes = {
+        100: {
+            blocks: 1,
+            blocksize: {
+                width: 10,
+                height: 10
+            },
+            blockspacingRatio: 0,
+            radius: 12,
+            dotspacingRatio: 0.2,
+            classOffset: 0
+        },
+        1000: {
+            blocks: 2,
+            blocksize: {
+                width: 20,
+                height: 5
+            },
+            blockspacingRatio: 0.3,
+            radius: 4.5,
+            dotspacingRatio: 0.5,
+            classOffset: 0
+        }
+    }
+}

+ 2 - 0
src/d3custom.js

@@ -0,0 +1,2 @@
+export { select, selectAll } from 'd3-selection';
+export { rgb, color } from 'd3-color';

+ 4 - 0
src/environment.js

@@ -0,0 +1,4 @@
+export default {
+  debug: true,
+  testing: true
+};

+ 26 - 0
src/main.js

@@ -0,0 +1,26 @@
+import 'fetch';
+import environment from './environment';
+
+//Configure Bluebird Promises.
+//Note: You may want to use environment-specific configuration.
+Promise.config({
+    warnings: {
+        wForgottenReturn: false
+    }
+});
+
+export function configure(aurelia) {
+    aurelia.use
+        .standardConfiguration()
+        .feature('resources');
+
+    if (environment.debug) {
+        aurelia.use.developmentLogging();
+    }
+
+    if (environment.testing) {
+        aurelia.use.plugin('aurelia-testing');
+    }
+
+    aurelia.start().then(() => aurelia.setRoot());
+}

+ 36 - 0
src/messages.js

@@ -0,0 +1,36 @@
+export class ClassSizeChanged {
+    constructor(surv, cidx) {
+        this.surv = surv;
+        this.cidx = cidx;
+    }
+}
+
+export class NumberOfSamplesChanged {
+    constructor(iconArrayConfig) {
+        this.iconArrayConfig = iconArrayConfig;
+    }
+}
+
+export class SamplesUpdated {
+    constructor(samples) {
+        this.samples = samples;
+    }
+}
+
+export class OffsetsSynced {
+    constructor(offsets) {
+        this.offsets = offsets;
+    }
+}
+
+export class OffsetUpdated {
+    constructor(cidx) {
+        this.cidx = cidx;
+    }
+}
+
+export class RedrawIconArray {
+    constructor(fromScratch) {
+        this.fromScratch = fromScratch;
+    }
+}

+ 5 - 0
src/number-format.js

@@ -0,0 +1,5 @@
+export class NumberFormatValueConverter {
+    fromView(value) {
+        return parseInt(value);
+    }
+}

+ 168 - 0
src/pdfWorker.js

@@ -0,0 +1,168 @@
+importScripts(
+    '../assets/pdfkit.min.js',
+    '../assets/blob-stream.min.js'
+
+);
+
+onmessage = function(e) {
+
+    let doc = new PDFDocument(
+        {
+            size: 'A5',
+            layout: 'landscape',
+            margin: 0
+        }
+    );
+
+    let stream = doc.pipe(blobStream());
+
+    for (let fd in e.data.fontData) {
+        let cfd = e.data.fontData[fd];
+        doc.registerFont(cfd.name, cfd.data);
+    }
+
+    let romanFont = e.data.fontData.roman.name;
+    let boldFont = e.data.fontData.bold.name;
+
+    let iaconf = e.data.iconArraySizes[e.data.selectedSize];
+
+    // meta data
+    doc.info.Title = e.data.title[e.data.selectedLanguage];
+    doc.info.Author = e.data.appTitle[e.data.selectedLanguage];
+    doc.info.Subject = e.data.description[e.data.selectedLanguage];
+
+    // title
+    doc.font(boldFont)  // bold font
+        .fontSize(e.data.baseFontSize * 1.6)
+        .text(
+            e.data.title[e.data.selectedLanguage],
+            3 * e.data.baseFontSize,
+            1.6 * e.data.baseFontSize,
+            { width: 410 }
+        );
+
+    // description
+    doc.font(boldFont)  // roman font
+        .fontSize(e.data.baseFontSize * 1);
+    doc.text(e.data.description[e.data.selectedLanguage]);
+
+    // summary
+    doc.font(romanFont)
+        .fontSize(e.data.baseFontSize * 1);
+    doc.text(e.data.summary[e.data.selectedLanguage], { width: 560, align: 'left' });
+
+    doc.save();
+
+    // here starts hand-adjusted positioning
+    let iaxoffset = 285;
+    let iayoffset = 90;
+    let afteriay = 170;
+
+    // reference classes
+    doc.fontSize(e.data.baseFontSize * 1.6)
+        .text(
+            e.data.selectedSize + ' ' + e.data.referenceClasses[e.data.selectedLanguage][0],
+            3 * e.data.baseFontSize,
+            iayoffset
+        )
+        .text(
+            e.data.selectedSize + ' ' + e.data.referenceClasses[e.data.selectedLanguage][1],
+            3 * e.data.baseFontSize + iaxoffset,
+            iayoffset
+        );
+
+    // icon arrays
+    // ***MAGIC NUMBERS*** for conversion from svg web layout to pdf print
+    let magicSpacingRatio = 0.425;
+    let magicRadiusRatio = 0.49;
+    if (e.data.ccsamples[0].length === 100) {
+        magicSpacingRatio = 0;
+        magicRadiusRatio = 0.4;
+    }
+
+    let diameter = parseFloat(iaconf.radius) * 1.18;
+    let dsp = diameter * iaconf.dotspacingRatio * magicSpacingRatio;
+    let blockspacing = diameter * iaconf.blockspacingRatio;
+    for (let j = 0; j < 2; ++j) {
+        let csamples = e.data.ccsamples[j];
+        let iaxfirst = csamples.length === 100 ? 120 : 0;
+
+        if (e.data.selectedSize === 1000) {
+            iayoffset = 110;
+        } else {
+            iayoffset = 120;
+        }
+        for (let i = 0; i < csamples.length; ++i) {
+            doc.ellipse(
+                j * iaxoffset + (1 - j) * iaxfirst + e.data.baseFontSize * 3 + diameter * magicRadiusRatio +     // x-offset per icon array
+
+                (i % (iaconf.blocks * iaconf.blocksize.width)) * (dsp + diameter) +                            // x-offset per icon
+                blockspacing * (Math.floor(i / iaconf.blocksize.width) % iaconf.blocks),                            // blockspacing x
+                iayoffset +                                                                                         // y-offset
+
+                Math.floor(i / (iaconf.blocks * iaconf.blocksize.width)) * (dsp + diameter) +                  // y-position
+                blockspacing * (Math.floor(i / iaconf.blocksize.width / iaconf.blocksize.height / iaconf.blocks)),  // blockspacing y
+                diameter * magicRadiusRatio                                                                    // radius
+            ) .fill(e.data.classes[csamples[i]].colour);
+        }
+    }
+
+    // legend
+    for (let i = 0; i < e.data.classes.length; ++i) {
+        doc
+            .ellipse(
+                e.data.baseFontSize * 3 + 4,
+                iayoffset + afteriay + 5.6 + i * e.data.baseFontSize * 2.56,
+                2.8
+            )
+            .fill(e.data.classes[i].colour);
+        doc
+            .font(e.data.fontData.roman.name)
+            .fillColor('#000')
+            .fontSize(e.data.baseFontSize)
+            .text(
+                e.data.classes[i].name[e.data.selectedLanguage],
+                e.data.baseFontSize * 4 + iaconf.radius * magicRadiusRatio,
+                iayoffset + afteriay + i * e.data.baseFontSize * 2.56, { width: 220 }
+            );
+
+        for (let j = 0; j < 2; ++j) {
+            doc.text(
+                e.data.samplesPerClass[j][i],
+                e.data.baseFontSize * 4 + 200 + j * 45,
+                iayoffset + afteriay + i * e.data.baseFontSize * 2.56,
+                { width: 50, align: 'right' }
+            );
+        }
+    }
+
+    // source, last update
+    doc
+        .font(boldFont)
+        .text(e.data.labels.source[e.data.selectedLanguage], e.data.baseFontSize * 4 + 330, iayoffset + afteriay)
+        .font(romanFont)
+        .text(e.data.reference[e.data.selectedLanguage], e.data.baseFontSize * 4 + 330, iayoffset + afteriay + e.data.baseFontSize * 1.6)
+        .text(
+            e.data.labels.update.label[e.data.selectedLanguage] + ': ' + e.data.lastUpdate[e.data.selectedLanguage],
+            e.data.baseFontSize * 4 + 330,
+            iayoffset + afteriay + e.data.baseFontSize * 1.6 * 9
+        );
+
+    // url
+    doc
+        .font(romanFont)
+        .text(
+            e.data.url[e.data.selectedLanguage],
+            e.data.baseFontSize * 3,
+            iayoffset + afteriay + e.data.baseFontSize * 1.6 * 9
+        );
+
+    doc.end();
+
+    stream.on('finish', function() {
+
+        let pdfURL = this.toBlobURL('application/pdf');
+
+        postMessage(pdfURL);
+    });
+};

BIN
src/resources/Verdana Bold.ttf


BIN
src/resources/Verdana.ttf


+ 3 - 0
src/resources/index.js

@@ -0,0 +1,3 @@
+export function configure(config) {
+  //config.globalResources([]);
+}

+ 3 - 0
src/sampler-visual.html

@@ -0,0 +1,3 @@
+<template>
+    <svg class="${sampler.svgclass}"></svg>
+</template>

+ 209 - 0
src/sampler-visual.js

@@ -0,0 +1,209 @@
+import { customElement, bindable, inject } from 'aurelia-framework';
+import { EventAggregator } from 'aurelia-event-aggregator';
+
+import * as d3 from './d3custom';
+
+import {
+    NumberOfSamplesChanged,
+    OffsetUpdated,
+    OffsetsSynced,
+    ClassSizeChanged,
+    RedrawIconArray
+} from './messages';
+
+@customElement('sampler-visual')
+@inject(EventAggregator)
+
+export class SamplerVisual {
+
+    @bindable sampler;
+    @bindable maxSamples;
+    @bindable sortMethodId;
+    @bindable voffsets;
+    @bindable soffsets;
+    @bindable icon;
+    @bindable blocks;
+    @bindable blocksize;
+    @bindable blockspacingRatio;
+    @bindable radius;
+    @bindable dotspacingRatio;
+    @bindable classoffset;
+
+    constructor(ea) {
+
+        this.ea = ea;
+        this.svgWidth = 496;
+        this.svgHeight = 310;
+
+        // TODO: simplify event messages: changes that need complete redraw and those which don't require that
+        // this.updateVisualisation(dirty)  ... if(dirty) ...
+        this.ea.subscribe(OffsetsSynced, msg => {
+            this.updateVisualisation();
+        });
+
+        // total number of samples changed
+        this.ea.subscribe(NumberOfSamplesChanged, msg => {
+            this.blocks = parseInt(msg.iconArrayConfig.blocks);
+            this.blocksize = msg.iconArrayConfig.blocksize;
+            this.blockspacingRatio = parseFloat(msg.iconArrayConfig.blockspacingRatio);
+            this.radius = parseFloat(msg.iconArrayConfig.radius);
+            this.dotspacingRatio = parseFloat(msg.iconArrayConfig.dotspacingRatio);
+            this.updateVisualisation();
+        });
+
+        // Redraw icon array from scratch (icons changed, total number of samples changed. d'oh)
+        this.ea.subscribe(RedrawIconArray, msg => {
+            if (msg.fromScratch === true) {
+                this.dirty = true;
+            }
+            this.updateVisualisation();
+        });
+
+        // calculate new class offsets
+        this.ea.subscribe(ClassSizeChanged, msg => {
+
+            if (msg.surv === this.sampler) {
+
+                let cidx = msg.cidx;
+                // let iconblocksize = this.blocks * this.blocksize * this.blocksize;
+                let iconblocksize = this.blocks * this.blocksize.width * this.blocksize.height;
+                this.voffsets[cidx - 1] = Math.ceil(this.sampler.samplesPerClass[cidx] / iconblocksize);  // calculate number of lines occupied by class
+
+                this.ea.publish(new OffsetUpdated(cidx));
+            }
+        });
+    }
+
+    attached() {  // set up d3 view when dom ready
+
+        let radius = parseFloat(this.radius);  // completely absurd hack against #$%^ javascript string coercion. d**n javascript
+
+        this.margin = {
+            top: radius,
+            right: radius,
+            bottom: radius,
+            left: radius
+        };
+        this.width = this.svgWidth - this.margin.left - this.margin.right;
+        this.height = this.svgHeight - this.margin.top - this.margin.bottom;
+
+        this.svg = d3.select('.' + this.sampler.svgclass)
+            .attr('width', this.width + this.margin.left + this.margin.right)
+            .attr('height', this.height + this.margin.top + this.margin.bottom);
+
+        this.iconarray = this.svg.append('g')
+            .attr('class', 'iconarray')
+            .attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')');
+
+        this.updateVisualisation();
+    }
+
+    concatenateSamplesArrays(blockwise = false) {
+        let samplesIsolated = this.sampler.sortedSamples();
+        let samplesConcat = [];
+
+        if (blockwise) {
+            samplesConcat.fill(0, 0, 1000);
+        } else {
+            // create linear array of samples to meet the FACT BOX LAYOUT criteria, use complementary event class as filler class
+            for (let i = 1; i < samplesIsolated.length; ++i) {
+
+                // fill array with all samples of current class
+                for (let j = 0; j < samplesIsolated[i].length; ++j) {
+                    samplesConcat.push(i);
+                }
+                // fill up block with complementary class
+                for (let k = 0; k < this.soffsets[i - 1] * this.blocks * this.blocksize.width * this.blocksize.height - samplesIsolated[i].length; ++k) {
+                    samplesConcat.push(samplesIsolated[0].shift());
+                }
+            }
+        }
+
+        // fill up the array with either base class id or undefined (placeholder)
+        let sclength = samplesConcat.length;
+        for (let i = 0; i < this.maxSamples - sclength; ++i) {
+            samplesConcat.push(samplesIsolated[0].shift());
+        }
+        return samplesConcat;
+    }
+
+    updateVisualisation() {
+
+        if (this.dirty) {
+            this.iconarray.remove();
+            this.iconarray = this.svg.append('g')
+                .attr('class', 'iconarray')
+                .attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')');
+            this.dirty = false;
+        }
+
+        let radius = this.radius;
+        let diameter = 2 * radius;
+        let dotspacing = radius * this.dotspacingRatio;
+
+        let hblocks = this.blocks;
+        let vblocks = this.maxSamples / hblocks / this.blocksize.width / this.blocksize.height;
+        let blocksize = this.blocksize;
+        let hsamples = hblocks * blocksize.width;
+        let vsamples = this.maxSamples / hsamples;
+        let blockspacing = diameter * this.blockspacingRatio;
+
+        let xoffset = (
+            this.svgWidth - (                   // svg width -
+                hsamples * diameter +           // number of horizontal samples
+                dotspacing * (hsamples - 1) +   // spacing between horizontal samples
+                blockspacing * (hblocks - 1)    // spacing between horizontal blocks
+            )) / 2;
+
+        let yoffset = (
+            this.svgHeight - (                  // svg height -
+                vsamples * diameter +           // number of vertical samples +
+                dotspacing * (vsamples - 1) +   // spacing between vertical samples +
+                blockspacing * (vblocks - 1)    // spacing between vertical blocks
+            )) / 2;
+
+        let colours = this.sampler.colours();
+
+        let samplesConcat = this.concatenateSamplesArrays();
+
+        let circles = this.iconarray.selectAll('circle')
+            .data(samplesConcat);
+
+        function getX(i) {
+            return xoffset +
+                (dotspacing + diameter) * (i % (hblocks * blocksize.width)) +                    // x position
+                blockspacing * (Math.floor(i / blocksize.width) % hblocks);                      // additional horizontal block spacing
+        }
+
+        function getY(i) {
+            return yoffset +
+                (dotspacing + diameter) * Math.floor(i / (hblocks * blocksize.width)) +          // x position
+                blockspacing * (Math.floor(i / blocksize.width / blocksize.height / hblocks));   // vertical blockspacing
+        }
+
+        // EXIT
+        circles.exit().remove();
+
+        // UPDATE
+        circles
+            .attr('r', radius)
+            .attr('transform', function(d, i) {
+                return 'translate(' + getX(i) + ',' + getY(i) + ')';
+            })
+            .attr('fill', function(d) {
+                return colours[d];
+            });
+
+        // ENTER
+        circles.enter()
+            .append('circle')
+            .attr('transform', function(d, i) {  // x positioning with block grouping
+                return 'translate(' + getX(i) + ',' + getY(i) + ')';
+            })
+            .attr('r', radius)
+            .attr('fill', function(d) {
+                return colours[d];
+            })
+            .attr('stroke', 'none');
+    }
+}

+ 58 - 0
src/sampler.js

@@ -0,0 +1,58 @@
+import { ClassSizeChanged } from './messages';
+
+export class Sampler {
+
+    constructor(ea, samples, classes, id) {
+        this.classes = classes;
+        this.id = id;
+        this.svgclass = 'sampler-' + this.id;
+        this.samplesPerClass = samples;
+        this.remainingSamples = this.samplesPerClass[0];
+
+        this.nrSamples = this.samplesPerClass.reduce(function(s, v) {
+            return s + v;
+        }, 0);
+
+        this.ea = ea;
+    }
+
+    // calculate remaining samples, send class size changed notification with respective class index
+    changeClassSize(cidx, e) {
+        this.samplesPerClass[0] = this.remaining();
+        this.remainingSamples = this.samplesPerClass[0];
+        this.ea.publish(new ClassSizeChanged(this, cidx));
+    }
+
+    // calculate number of remaining samples ('base class', 'complementary event',...)
+    remaining() {
+
+        let sum = 0;
+        for (let i = 1; i < this.classes.length; ++i) {  // exclude base class
+            sum += this.samplesPerClass[i];
+        }
+        return this.nrSamples - sum;
+    }
+
+    colours() {
+        return this.classes.map(function(cls) {
+            return cls.colour;
+        });
+    }
+
+    // create array of arrays of samples identified by class id for d3 visualisation convenience
+    sortedSamples() {
+
+        let samplesIsolated = [];
+
+        // fill array with ordered samples
+        for (let cls of this.samplesPerClass) {
+            let arr = [];
+            for (let i = 0; i < cls; ++i) {
+                arr.push(this.samplesPerClass.indexOf(cls)); // !!!!
+            }
+            samplesIsolated.push(arr);
+        }
+
+        return samplesIsolated;
+    }
+}

+ 17 - 0
src/scss/_custom.scss

@@ -0,0 +1,17 @@
+//
+// <Project name>
+// =============================================================================
+
+h1, h3, h4 {
+    font-weight: normal;
+}
+
+// *, ::after, ::before {
+//     box-sizing: inherit;
+//     color: inherit;
+//     font-family: initial;
+//     font-size: initial;
+//     line-height: inherit;
+//     text-decoration: inherit;
+//     vertical-align: inherit;
+// }

+ 152 - 0
src/scss/_globals.scss

@@ -0,0 +1,152 @@
+//
+// Custom Global Variables
+// =============================================================================
+
+
+//
+// Base
+// ----
+
+// Non-responsive website
+// $non-responsive: true !default;
+
+// Transition duration
+// $transition-duration: 150ms;
+
+// Breakpoints
+// $bp-extra-small: 30em;
+// $bp-small:       48em;
+// $bp-medium:      60em;
+// $bp-large:       70em;
+// $bp-extra-large: 80em;
+
+// Base Size (used in unitSize() for proportions)
+// $base-unit: 8;
+
+// Spacing
+// $spacing-xs: unitSize(1);
+// $spacing-s:  unitSize(2);
+// $spacing-m:  unitSize(3);
+// $spacing-l:  unitSize(4);
+// $spacing-xl: unitSize(5);
+
+
+//
+// Grid
+// ----
+
+// Prefix for the attributes, you can use 'data-' to make your markup valid
+// $prefix: "";
+
+// Max width for container
+// $container-width: 1200px;
+
+// Gutter size in pixels (without the unit we can do math easily)
+// $gutter: 30;
+
+// Number of columns in a row
+// $num-columns: 12;
+
+// If you only want to use the mixins for "semantic grids" set this to true
+// $only-semantic: false;
+
+
+//
+// Typography
+// ----------
+
+$font-family:          "Avenir Next", "Helvetica", "Arial", sans-serif;
+$font-family-headings: "Avenir Next", "Helvetica", "Arial", sans-serif;
+// $font-family-print:    "Georgia", "Times New Roman", "Times", serif;
+// $font-family-mono:     "Consolas", monospace;
+// $font-base-size:       16;
+
+
+//
+// Heading sizes
+// ----------
+
+// $h1-size: 48;
+// $h2-size: 36;
+// $h3-size: 28;
+// $h4-size: 18;
+// $h5-size: 16;
+// $h6-size: 14;
+// $giga-size: 80;
+// $mega-size: 64;
+// $kilo-size: 52;
+
+
+//
+// Color Palette
+// -------------
+
+// Colors
+// $colors: (
+//   base: (
+//     "primary":   #4591aa,
+//     "selection": #d6d6d6,
+//     "lines":     #e0e0e0
+//   ),
+//
+//   text: (
+//     "primary":   #666,
+//     "secondary": #aaa,
+//     "heading":   #222,
+//     "ui":        white
+//   ),
+//
+//   background: (
+//     "dark":  #282E31,
+//     "light": #f5f5f5,
+//     "body":  white
+//   ),
+//
+//   state: (
+//     "muted":   #aaa,
+//     "primary": #4591aa,
+//     "success": #45ca69,
+//     "warning": #ffb800,
+//     "error":   #ca4829
+//   ),
+//
+//   blue: (
+//     "darker":  #495b61,
+//     "dark":    #447281,
+//     "base":    #4591aa,
+//     "light":   #5ab0cc,
+//     "lighter": #74cbe8
+//   ),
+//
+//   green: (
+//     "darker":  #3b6e6e,
+//     "dark":    #3b8686,
+//     "base":    #37a1a1,
+//     "light":   #2dbaba,
+//     "lighter": #69d1d1
+//   ),
+//
+//   cream: (
+//     "darker":  #c47858,
+//     "dark":    #e29372,
+//     "base":    #ecac91,
+//     "light":   #f9c2ab,
+//     "lighter": #fdd5c3
+//   ),
+//
+//   red: (
+//     "darker":  #653131,
+//     "dark":    #b73333,
+//     "base":    #da3c3c,
+//     "light":   #f25a5a,
+//     "lighter": #fa8181
+//   ),
+//
+//   gray: (
+//     "darker":  #333333,
+//     "dark":    #4d4d4d,
+//     "base":    #666666,
+//     "light":   #808080,
+//     "lighter": #999999
+//   )
+// );

+ 1 - 0
src/scss/font-awesome

@@ -0,0 +1 @@
+../../node_modules/font-awesome

+ 116 - 0
src/scss/partials/_colours.scss

@@ -0,0 +1,116 @@
+// Color Palette
+// -------------
+
+$colors: (
+    dark: (
+        #322f2b,
+        #07246d,
+        #07576d,
+        #076d50,
+        #076d1d,
+        #576d07,
+        #6d5007,
+        #6d1d07,
+        #6d0724,
+        #6d0757
+    ),
+    medium: (
+        #5b564e,
+        #0a36a5,
+        #0a83a5,
+        #0aa57a,
+        #0aa52c,
+        #83a50a,
+        #a57a0a,
+        #a52c0a,
+        #a50a36,
+        #a50a83
+  ),
+  light: (
+      #847d71,
+      #0d47dd,
+      #0dafdd,
+      #0ddda3,
+      #0ddd3b,
+      #afdd0d,
+      #dda30d,
+      #dd3b0d,
+      #dd0d47,
+      #dd0daf
+  ),
+  base: (
+    "primary":   #4591aa,
+    "selection": #d6d6d6,
+    "lines":     #e0e0e0
+  ),
+
+  text: (
+    "primary":   #666,
+    "secondary": #aaa,
+    "heading":   #222,
+    "ui":        white
+  ),
+
+  background: (
+    "dark":  #282e31,
+    "light": #f5f5f5,
+    "body":  white
+  ),
+
+  dsst: (
+    "yellow": #f2bf1a,
+    "background": #5b564e,
+    "text": #7c7872,
+    "light": #f7f7f6,
+    "lesslight": #e5e5e1,
+    "evenlesslight": #d2d2cc
+  ),
+
+  state: (
+    "muted":   #aaa,
+    "primary": #4591aa,
+    "success": #45ca69,
+    "warning": #ffb800,
+    "error":   #ca4829
+  ),
+
+  blue: (
+    "darker":  #495b61,
+    "dark":    #447281,
+    "base":    #4591aa,
+    "light":   #5ab0cc,
+    "lighter": #74cbe8
+  ),
+
+  green: (
+    "darker":  #3b6e6e,
+    "dark":    #3b8686,
+    "base":    #37a1a1,
+    "light":   #2dbaba,
+    "lighter": #69d1d1
+  ),
+
+  cream: (
+    "darker":  #c47858,
+    "dark":    #e29372,
+    "base":    #ecac91,
+    "light":   #f9c2ab,
+    "lighter": #fdd5c3
+  ),
+
+  red: (
+    "darker":  #653131,
+    "dark":    #b73333,
+    "base":    #da3c3c,
+    "light":   #f25a5a,
+    "lighter": #fa8181
+  ),
+
+  gray: (
+    "darker":  #333333,
+    "dark":    #4d4d4d,
+    "base":    #666666,
+    "light":   #808080,
+    "lighter": #999999
+  )
+)

+ 145 - 0
src/scss/partials/_form_elements.scss

@@ -0,0 +1,145 @@
+@import 'colours';
+
+$tcol: map-get(map-get($colors, dsst), text);
+$lcol: map-get(map-get($colors, dsst), light);
+$llcol: map-get(map-get($colors, dsst), lesslight);
+$hcol: map-get(map-get($colors, dsst), yellow);
+$text: nth(map-get($colors, dark), 1);
+
+$bs: 1px;
+
+a[aria-role=button],
+button {
+
+    -webkit-appearance: none;
+    background-color: $lcol;
+
+    text-decoration: none;
+    color: $text;
+}
+
+a[aria-role=button]:hover,
+button:hover {
+    background-color: $tcol;
+    border: $bs solid $lcol;
+    cursor: pointer;
+
+    color: $lcol;
+}
+
+a[aria-role=button]:avtive,
+button:active {
+    background-color: $hcol;
+}
+
+input::not([type=range]),
+label,
+a[aria-role=button],
+textarea,
+button {
+    -webkit-appearance: none;
+    color: $tcol;
+    font-size: 1rem;
+    padding: 0.4em 0.8em;
+}
+
+input[type=number] {
+    -moz-appearance: textfield;
+}
+
+input::-webkit-outer-spin-button,
+input::-webkit-inner-spin-button {
+    -webkit-appearance: none;
+}
+
+.input-radio {
+    height: 0;
+    width: 0;
+    opacity: 0;
+}
+
+.input-radio:hover ~ label {
+    border: $bs solid $lcol;
+    opacity: 1;
+}
+
+.input-radio:active ~ label {
+    border: $bs solid $hcol;
+    color: $hcol;
+    opacity: 1;
+}
+
+.input-radio:checked ~ label {
+    border: $bs solid $lcol;
+    opacity: 1;
+}
+
+.input-file {
+    width: 0.1px;
+    height: 0.1px;
+    opacity: 0;
+    overflow: hidden;
+    position: absolute;
+    z-index: -1;
+}
+
+.input-radio ~ label {
+    border: $bs solid $lcol;
+    box-sizing: border-box;
+    cursor: pointer;
+    display: block;
+    height: 100%;
+    opacity: 0.6;
+}
+
+.input-file + label,
+.input-colour + label {
+    background-color: transparent;
+    font-size: 1em;
+    cursor: pointer;
+    display: inline-block;
+}
+
+.input-file:focus + label,
+.input-file + label:hover {
+    border: $bs solid $lcol;
+    background-color: $tcol;
+    color: $lcol;
+}
+
+.input-file:focus + label {
+    outline: 1px dotted #000;
+    outline: -webkit-focus-ring-color auto 5px;
+}
+
+.input-file + label:active {
+    // border: $bs solid $lcol;
+    background-color: $hcol;
+    // color: $lcol;
+}
+
+.form-inline .form-control {
+    display: inline-block;
+    height: 2.2em;
+    vertical-align: middle;
+    width: auto;
+}
+
+.form-item {
+    float: left;
+    font-size: 1rem;
+    margin: 0.2rem;
+    padding: 0.4rem 0.8rem;
+}
+
+.form-item:not(label) {
+    border: $bs solid $tcol;
+}
+
+label {
+    background-color: $llcol;
+}
+
+fieldset {
+    border-color: transparent;
+}

+ 260 - 0
src/scss/styles.scss

@@ -0,0 +1,260 @@
+@import 'font-awesome/scss/font-awesome';
+@import 'partials/colours';
+@import 'partials/form_elements';
+
+$ibg: map-get(map-get($colors, dsst), light);
+$dbg: nth(map-get($colors, dark), 1);
+$text: $dbg;
+
+* {
+    box-sizing: border-box;
+    margin: 0;
+    padding: 0;
+}
+
+body {
+    font-family: Verdana, sans-serif;
+    font-size: 16px;
+    font-weight: normal;
+    background-color: map-get(map-get($colors, background), dark);
+    color: $text;
+}
+
+.iconset {
+    display: none;
+}
+
+main {
+    background-color: $ibg;
+    display: flex;
+    flex-direction: column;
+    justify-content: flex-start;
+    margin: auto;
+    padding: 1rem;
+    width: 64em;
+}
+
+input, textarea, select {
+    font-family: Verdana, sans-serif;
+}
+
+textarea {
+    resize: none;
+    overflow: hidden;
+    min-height: 1em;
+    max-height: 3em;
+}
+
+input:not([type=text]) {
+    max-width: 4rem;
+}
+
+header, aside, footer, main > section {
+    margin: 0.5rem 0;
+}
+
+header {
+    display: flex;
+    flex-direction: column;
+
+    .title-row {
+        display: flex;
+    }
+
+    .factbox-title {
+        flex: 2;
+        font-size: 2.25rem;
+        font-weight: bold;
+    }
+
+    .factbox-description {
+        font-size: 1.5rem;
+    }
+
+    .factbox-summary {
+        font-size: 1rem;
+    }
+
+    .logo {
+        flex: initial;
+    }
+}
+
+label {
+    border: 1px solid transparent;
+}
+
+.samplers {
+    display: flex;
+    justify-content: space-between;
+    background-color: $ibg;
+}
+
+.sampler {
+
+    .reference-class {
+        display: flex;
+        font-weight: bold;
+        width: 100%;
+
+        label {
+            flex: initial;
+        }
+
+        input {
+            flex: auto;
+            font-weight: bold;
+        }
+    }
+}
+
+.align-right {
+    text-align: right;
+}
+
+aside {
+
+    display: flex;
+    flex-direction: column;
+
+    &, input, label, textarea {
+        font-size: 0.667rem;
+    }
+
+    .legend {
+
+        flex: 2;
+
+        input[type=number],
+        input.number {
+            text-align: right;
+        }
+
+        .b-0 {
+            visibility: hidden;
+        }
+    }
+
+    .legend-references {
+        display: flex;
+        margin: 0.2rem 0 1rem;
+    }
+
+    .legendgroup {
+        display: flex;
+        padding: 0.1rem 0;
+
+        input[type=color] {
+            flex: 1;
+        }
+
+        textarea {
+            flex: 10;
+            // min-width: 21em;  // class <table>
+            max-width: 21.5em;  // flexbox layout
+        }
+
+        button {
+            flex: initial;
+            max-width: 3em;
+        }
+    }
+
+    .legendgroup:not(.legend-0) {
+        label.group-1, label.group-2 {
+            display: none;
+        }
+    }
+
+    .legend-0 {
+        label{
+            flex: 1;
+            max-width: 4em;
+        }
+
+        .b-0 {
+            flex: 2;
+        }
+
+        textarea {
+            flex: 9;
+        }
+    }
+
+    .references {
+        flex: 1;
+        margin-top: -1rem;
+
+        .references-text {
+            min-width: 100%;
+            min-height: calc(100% - 1em);
+        }
+    }
+
+    .meta-data {
+        display: flex;
+
+        .url {
+            align-self: flex-end;
+            flex: 1;
+            max-width: initial;
+        }
+
+        .minerva {
+            align-self: flex-end;
+            flex: initial;
+            width: 10em;
+        }
+
+        fieldset {
+            align-self: flex-end;
+            flex: initial;
+            border: 0;
+        }
+    }
+}
+
+footer {
+
+    @media print {
+        display: none;
+    }
+
+    display: flex;
+    flex-direction: column;
+    padding-top: 1rem;
+    border-top: 1px dashed;
+
+    label {
+        font-size: 0.667rem;
+    }
+
+    .settings, .importexport {
+        display: flex;
+
+        input {
+            max-width: none;
+        }
+    }
+
+    .importexport {
+        #upload {
+            max-width: none;
+        }
+        label {
+            border: $bs solid $tcol;
+        }
+    }
+
+    .settings {
+        justify-content: flex-end;
+        flex-direction: column;
+    }
+
+    iframe {
+        &#preview {
+            width: 100%;
+            height: 600px; // firefox
+        }
+    }
+}

+ 81 - 0
test/aurelia-karma.js

@@ -0,0 +1,81 @@
+(function(global) {
+  var karma = global.__karma__;
+  var requirejs = global.requirejs
+  var locationPathname = global.location.pathname;
+  var root = 'src';
+  karma.config.args.forEach(function(value, index) {
+    if (value === 'aurelia-root') {
+      root = karma.config.args[index + 1];
+    }
+  });
+
+  if (!karma || !requirejs) {
+    return;
+  }
+
+  function normalizePath(path) {
+    var normalized = []
+    var parts = path
+      .split('?')[0] // cut off GET params, used by noext requirejs plugin
+      .split('/')
+
+    for (var i = 0; i < parts.length; i++) {
+      if (parts[i] === '.') {
+        continue
+      }
+
+      if (parts[i] === '..' && normalized.length && normalized[normalized.length - 1] !== '..') {
+        normalized.pop()
+        continue
+      }
+
+      normalized.push(parts[i])
+    }
+
+    return normalized.join('/')
+  }
+
+  function patchRequireJS(files, originalLoadFn, locationPathname) {
+    var IS_DEBUG = /debug\.html$/.test(locationPathname)
+
+    requirejs.load = function (context, moduleName, url) {
+      url = normalizePath(url)
+
+      if (files.hasOwnProperty(url) && !IS_DEBUG) {
+        url = url + '?' + files[url]
+      }
+
+      if (url.indexOf('/base') !== 0) {
+        url = '/base/' + url;
+      }
+
+      return originalLoadFn.call(this, context, moduleName, url)
+    }
+
+    var originalDefine = global.define;
+    global.define = function(name, deps, m) {
+      if (typeof name === 'string') {
+        originalDefine('/base/' + root + '/' + name, [name], function (result) { return result; });
+      }
+
+      return originalDefine(name, deps, m);
+    }
+  }
+
+  function requireTests() {
+    var TEST_REGEXP = /(spec)\.js$/i;
+    var allTestFiles = ['/base/test/unit/setup.js'];
+
+    Object.keys(window.__karma__.files).forEach(function(file) {
+      if (TEST_REGEXP.test(file)) {
+        allTestFiles.push(file);
+      }
+    });
+
+    require(allTestFiles, window.__karma__.start);
+  }
+
+  karma.loaded = function() {}; // make it async
+  patchRequireJS(karma.files, requirejs.load, locationPathname);
+  requireTests();
+})(window);

+ 7 - 0
test/unit/app.spec.js

@@ -0,0 +1,7 @@
+import {App} from '../../src/app';
+
+describe('the app', () => {
+  it('says hello', () => {
+    expect(new App().message).toBe('Hello World!');
+  });
+});

+ 3 - 0
test/unit/setup.js

@@ -0,0 +1,3 @@
+import 'aurelia-polyfills';
+import {initialize} from 'aurelia-pal-browser';
+initialize();

Vissa filer visades inte eftersom för många filer har ändrats