Michael Zitzmann vor 5 Jahren
Commit
5351e7db5d
100 geänderte Dateien mit 7952 neuen und 0 gelöschten Zeilen
  1. 13 0
      .babelrc
  2. 26 0
      .editorconfig
  3. 212 0
      .eslintrc.yml
  4. 2 0
      .gitignore
  5. 25 0
      .htmlhintrc
  6. 40 0
      .sass-lint.yml
  7. 104 0
      config.js
  8. 38 0
      doc/01_introduction.md
  9. 31 0
      doc/02_structure.md
  10. 197 0
      doc/03_module.md
  11. 101 0
      doc/04_development.md
  12. 32 0
      doc/05_javascript.md
  13. 30 0
      doc/06_styles.md
  14. 15 0
      doc/07_images.md
  15. 26 0
      doc/08_history.md
  16. 2 0
      doc/html/css/github-markdown.css
  17. 121 0
      doc/html/css/github-syntax-highlight.css
  18. 233 0
      doc/html/index.html
  19. 127 0
      gulpfile.babel.js
  20. 78 0
      package.json
  21. 9 0
      public/browserconfig.xml
  22. BIN
      public/favicon.ico
  23. 5 0
      public/robots.txt
  24. 666 0
      readme.md
  25. BIN
      src/fonts/roboto-light.woff
  26. BIN
      src/fonts/roboto-light.woff2
  27. BIN
      src/fonts/roboto-medium.woff
  28. BIN
      src/fonts/roboto-medium.woff2
  29. BIN
      src/fonts/roboto-regular.woff
  30. BIN
      src/fonts/roboto-regular.woff2
  31. BIN
      src/fonts/roboto-thin.woff
  32. BIN
      src/fonts/roboto-thin.woff2
  33. BIN
      src/fonts/robotomono-light.woff
  34. BIN
      src/fonts/robotomono-light.woff2
  35. 31 0
      src/html/index.html
  36. BIN
      src/img/android-chrome-192x192.png
  37. BIN
      src/img/android-chrome-512x512.png
  38. BIN
      src/img/apple-touch-icon.png
  39. BIN
      src/img/favicon-16x16.png
  40. BIN
      src/img/favicon-32x32.png
  41. BIN
      src/img/mstile-150x150.png
  42. 3 0
      src/img/safari-pinned-tab.svg
  43. 1 0
      src/img/sprites.svg
  44. 3 0
      src/img/sprites/correct.svg
  45. 3 0
      src/img/sprites/incorrect.svg
  46. 15 0
      src/img/sprites/sprites.yaml
  47. 1 0
      src/img/sprites/triangle.svg
  48. 19 0
      src/js/components/FinalScreen.jsx
  49. 321 0
      src/js/components/Index.jsx
  50. 82 0
      src/js/components/QuestionScreen.jsx
  51. 19 0
      src/js/components/TitleScreen.jsx
  52. 73 0
      src/js/components/partials/GraphItem.jsx
  53. 15 0
      src/js/components/partials/HeaderLightItem.jsx
  54. 12 0
      src/js/components/partials/IntroItem.jsx
  55. 36 0
      src/js/components/partials/QuestionItem.jsx
  56. 16 0
      src/js/components/partials/UserVoteItem.jsx
  57. 55 0
      src/js/components/partials/VoteItem.jsx
  58. 105 0
      src/js/config.js
  59. 106 0
      src/js/content/Gruppe-1_item-a.json
  60. 286 0
      src/js/content/Gruppe-1_item-b.json
  61. 286 0
      src/js/content/Gruppe-1_item-c.json
  62. 281 0
      src/js/content/Gruppe-2_item-a.json
  63. 286 0
      src/js/content/Gruppe-2_item-b.json
  64. 281 0
      src/js/content/Gruppe-2_item-c.json
  65. 281 0
      src/js/content/Gruppe-2_item-d.json
  66. 272 0
      src/js/content/Gruppe-3_item-a.json
  67. 272 0
      src/js/content/Gruppe-3_item-b.json
  68. 272 0
      src/js/content/Gruppe-3_item-c.json
  69. 272 0
      src/js/content/Gruppe-3_item-d.json
  70. 11 0
      src/js/content/module.json
  71. 17 0
      src/js/content/offline.js
  72. 96 0
      src/js/d3/axes.js
  73. 85 0
      src/js/d3/bar.js
  74. 62 0
      src/js/d3/defs.js
  75. 150 0
      src/js/d3/gradient.js
  76. 57 0
      src/js/d3/grid.js
  77. 180 0
      src/js/d3/handle.js
  78. 44 0
      src/js/d3/handleConnector.js
  79. 103 0
      src/js/d3/handleSymbols.js
  80. 41 0
      src/js/d3/legend.js
  81. 46 0
      src/js/d3/lineGraphs.js
  82. 107 0
      src/js/d3/main.js
  83. 47 0
      src/js/d3/pointMarker.js
  84. 140 0
      src/js/d3/range.js
  85. 461 0
      src/js/d3/rangeController.js
  86. 23 0
      src/js/d3/scales.js
  87. 13 0
      src/js/main-offline.jsx
  88. 13 0
      src/js/main.jsx
  89. 98 0
      src/js/utilities/api.js
  90. 6 0
      src/js/utilities/enableTouch.js
  91. 32 0
      src/js/utilities/fonts.js
  92. 76 0
      src/js/utilities/formatter.js
  93. 6 0
      src/js/utilities/math.js
  94. 15 0
      src/js/utilities/randomizer.js
  95. 19 0
      src/scss/base/_fonts.scss
  96. 52 0
      src/scss/base/_forms.scss
  97. 48 0
      src/scss/base/_headings.scss
  98. 12 0
      src/scss/base/_links.scss
  99. 24 0
      src/scss/base/_rhythm.scss
  100. 30 0
      src/scss/base/_root.scss

+ 13 - 0
.babelrc

@@ -0,0 +1,13 @@
+{
+  // babelHelpers: 'bundled',  // doesn't work, even though complies to documentation. Moved to rollup configuration instead in `tasks/scripts.js
+  // see https://github.com/storybookjs/storybook/issues/1320#issuecomment-310777396
+  presets: [
+    '@babel/preset-react',
+    '@babel/preset-env'
+  ],
+  exclude: [ 'node_modules/**', '**/*.json' ],
+  plugins: [
+    ['@babel/plugin-transform-react-jsx', { pragma: 'h' }],
+    '@babel/plugin-proposal-object-rest-spread'
+  ]
+}

+ 26 - 0
.editorconfig

@@ -0,0 +1,26 @@
+# editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.{css,scss,sass,js,json,jsx}]
+indent_size = 2
+indent_style = space
+
+[*.{html,php,twig,tmpl}]
+indent_size = 2
+indent_style = space
+
+[*.{yml,yaml,ini}]
+indent_size = 2
+indent_style = space
+
+[*.{md,markdown,rst,txt}]
+indent_size = 2
+indent_style = space
+trim_trailing_whitespace = false

+ 212 - 0
.eslintrc.yml

@@ -0,0 +1,212 @@
+---
+  extends:
+    - 'eslint:recommended'
+
+  parser: 'babel-eslint'
+  parserOptions:
+    ecmaVersion: 6
+    sourceType: module
+    ecmaFeatures:
+      jsx: true
+      experimentalObjectRestSpread: true
+
+  env:
+    browser: true
+    node: true
+    es6: true
+
+  plugins:
+    - react
+
+  rules:
+    # best practices section
+    array-callback-return: error
+    block-scoped-var: error
+    consistent-return: error
+    curly: [ error, 'multi-line' ]
+    default-case: error
+    dot-location:
+      - error
+      - property
+    dot-notation: error
+    eqeqeq: error
+    no-alert: error
+    no-div-regex: error
+    no-else-return: error
+    no-empty-function: error
+    no-eq-null: error
+    no-eval: error
+    no-extend-native: error
+    no-extra-bind: error
+    no-extra-label: error
+    no-floating-decimal: error
+    no-global-assign: error
+    no-implicit-coercion: error
+    no-implicit-globals: error
+    no-implied-eval: error
+    no-iterator: error
+    no-labels: error
+    no-lone-blocks: error
+    no-loop-func: error
+    # no-multi-spaces: error
+    no-multi-str: error
+    no-new-func: error
+    no-new-wrappers: error
+    no-new: error
+    no-octal-escape: error
+    no-param-reassign: error
+    no-proto: error
+    no-return-assign: error
+    no-script-url: error
+    no-self-compare: error
+    no-sequences: error
+    no-throw-literal: error
+    no-unmodified-loop-condition: error
+    no-unused-expressions: error
+    no-useless-call: error
+    no-useless-concat: error
+    no-useless-escape: error
+    no-void: error
+    no-with: error
+    vars-on-top: error
+    wrap-iife: error
+    yoda: error
+
+    # Variables section
+    no-label-var: error
+    no-shadow-restricted-names: error
+    no-shadow: error
+    no-undef-init: error
+    # no-undefined: error
+    no-use-before-define: error
+
+    # Stylistic Issues section
+    # array-bracket-spacing:
+    #   - error
+    #   - always
+    block-spacing: error
+    brace-style: error
+    camelcase:
+      - error
+      - properties: never
+    comma-dangle: error
+    comma-spacing: error
+    comma-style: error
+    computed-property-spacing: error
+    consistent-this: error
+    eol-last: error
+    func-call-spacing: error
+    func-names: warn
+    func-style:
+      - warn
+      - expression
+    indent:
+      - error
+      - 2
+      - SwitchCase: 1
+    key-spacing: error
+    keyword-spacing: error
+    linebreak-style: error
+    lines-around-comment: warn
+    max-depth: warn
+    max-lines: ["warn", {
+      "max": 500,
+      "skipComments": true
+    }]
+    max-len:
+      - warn
+      - 180
+    max-nested-callbacks: warn
+    # max-params: warn
+    max-params: ["warn", 5]
+    max-statements-per-line: warn
+    max-statements:
+      - warn
+      - 50
+    new-cap: warn
+    new-parens: error
+    # newline-after-var: warn
+    no-array-constructor: error
+    no-bitwise: error
+    no-lonely-if: warn
+    no-multiple-empty-lines: warn
+    no-nested-ternary: error
+    no-new-object: error
+    no-plusplus: error
+    no-tabs: error
+    no-trailing-spaces: error
+    # no-underscore-dangle: warn
+    no-unneeded-ternary: warn
+    no-whitespace-before-property: error
+    object-curly-newline: error
+    object-curly-spacing:
+      - error
+      - always
+    one-var:
+      - error
+      - never
+    operator-assignment: error
+    operator-linebreak: error
+    quote-props:
+      - error
+      - as-needed
+    quotes:
+      - error
+      - single
+      - avoid-escape
+    semi-spacing: error
+    semi: error
+    space-before-blocks: error
+    space-before-function-paren: error
+    space-in-parens: error
+    space-infix-ops: error
+    space-unary-ops: error
+    spaced-comment: warn
+    unicode-bom: error
+    wrap-regex: error
+
+    # ECMAScript 6 / ES2015 Section
+    arrow-body-style: error
+    # arrow-parens: error
+    arrow-spacing: error
+    generator-star-spacing: warn
+    # no-confusing-arrow: warn
+    no-duplicate-imports: error
+    no-useless-computed-key: warn
+    no-useless-constructor: warn
+    no-useless-rename: error
+    no-var: error
+    object-shorthand: error
+    prefer-arrow-callback: warn
+    # prefer-const: error
+    # prefer-reflect: warn
+    prefer-rest-params: warn
+    prefer-spread: warn
+    prefer-template: error
+    rest-spread-spacing: error
+    template-curly-spacing: error
+    yield-star-spacing: error
+
+    # JSX RULES
+    # jsx-quotes:
+    #   - error
+    #   - prefer-single
+    react/jsx-boolean-value: error
+    # react/jsx-curly-spacing:
+    #   - error
+    #   - never
+    react/jsx-equals-spacing:
+      - error
+      - never
+    react/jsx-indent:
+      - error
+      - 2
+    react/jsx-indent-props:
+      - error
+      - 2
+    react/jsx-no-duplicate-props: error
+    react/jsx-no-undef: error
+    react/jsx-tag-spacing: error
+    react/jsx-uses-react: error
+    react/jsx-uses-vars: error
+    # react/self-closing-comp: error

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+public/assets
+public/index.html

+ 25 - 0
.htmlhintrc

@@ -0,0 +1,25 @@
+{
+  "alt-require": true,
+  "attr-lowercase": true,
+  "attr-no-duplication": true,
+  "attr-unsafe-chars": true,
+  "attr-value-double-quotes": true,
+  "attr-value-not-empty": false,
+  "doctype-first": true,
+  "doctype-html5": true,
+  "head-script-disabled": false,
+  "href-abs-or-rel": false,
+  "id-class-ad-disabled": true,
+  "id-class-value": false,
+  "id-unique": true,
+  "inline-script-disabled": true,
+  "inline-style-disabled": true,
+  "space-tab-mixed-disabled": "space",
+  "spec-char-escape": true,
+  "src-not-empty": true,
+  "style-disabled": false,
+  "tag-pair": true,
+  "tag-self-close": true,
+  "tagname-lowercase": true,
+  "title-require": true
+}

+ 40 - 0
.sass-lint.yml

@@ -0,0 +1,40 @@
+# use the default settings to keep this file small
+options:
+  merge-default-rules: true
+
+rules:
+  # BEM all the things
+  class-name-format:
+    - 1
+    - convention: hyphenatedbem
+  placeholder-name-format:
+    - 1
+    - convention: hyphenatedbem
+
+  # sassception! allow a maximum nesting depth of 4 levels deep
+  nesting-depth:
+    - 1
+    - max-depth: 4
+
+  # those mixins do not have to be used before any other declarations
+  mixins-before-declarations:
+    - 1
+    - exclude: [ 'mediaquery', 'attention' ]
+
+  # allow non-leading zero values like .2rem or .4rem
+  leading-zero: 0
+
+  # do not sort properties by name
+  property-sort-order: 0
+
+  # allow nested attribute and pseudo-selectors, this makes stuff
+  # more readable for humans and allow some edge-case-nesting issues,
+  # but still force element nesting, so the nesting-depth-rule makes sense
+  force-pseudo-nesting: 0
+  force-attribute-nesting: 0
+
+  # allow stuff like 1/2 or 1/4
+  space-around-operator: 0
+
+  # allow warnings for better sass-maps debugging
+  no-warn: 0

+ 104 - 0
config.js

@@ -0,0 +1,104 @@
+import process from 'process';
+import args from 'yargs';
+
+/**
+ * Configuration for the build process
+ *
+ * Distinction of development and production builds is made by user-provided node environment variable
+ * By default, the development target will be built
+ *
+ * development: `gulp ...`
+ * production: `NODE_ENV=production gulp ...`
+ *
+ * Additionally, if the web app has access to an API, the distinction between 'online' and 'offline' modes is made by user-provided command line argument
+ * If no argument is given, the 'online' version will be built
+ *
+ * development / offline: `gulp --api-mode=offline`
+ */
+export default {
+
+  currentTarget: process.env.NODE_ENV || 'development',
+  currentMode: args.argv.apiMode || 'online',
+
+  targets: {
+    development: {
+      delimiter: '',
+      infix: ''
+    },
+    production: {
+      delimiter: '.',
+      infix: 'min'
+    }
+  },
+
+  modes: {
+    offline: {
+      delimiter: '-',
+      infix: 'offline'
+    },
+    online: {
+      delimiter: '',
+      infix: ''
+    }
+  },
+
+  // .eslintcr.yml is not being read?
+  eslint: {
+    development: {
+      rules: {
+        'no-console': 1,
+        'no-warning-comments': 0
+      },
+      plugins: [ 'react' ]
+    },
+    production: {
+      rules: {
+        'no-console': 1,
+        'no-warning-comments': [ 1, { terms: [ 'todo', 'fixme' ], location: 'start' } ]
+      },
+      plugins: [ 'react' ]
+    }
+  },
+
+  // htmlhint configuration
+  htmlhintrc: '.htmlhintrc',
+
+  // gulp-sass configuration
+  sass: {
+    development: {
+      outputStyle: 'nested'
+    },
+    production: {
+      outputStyle: 'nested',
+      sourceMap: false
+    }
+  },
+
+  fileNames: {
+    css: 'main',
+    html: 'index',
+    js: 'main',
+    jsx: 'main'
+  },
+
+  paths: {
+    src: 'src',
+    dest: 'public',
+    assets: 'assets',
+    css: 'css',
+    fonts: 'fonts',
+    html: 'html',
+    js: 'js',
+    images: 'img',
+    sass: 'scss',
+    sprites: 'sprites'
+  },
+
+  // browsersync configuration
+  server: {
+    baseDir: './public/',
+    index: 'index.html'
+  },
+
+  logLevel: 'silent'
+};

+ 38 - 0
doc/01_introduction.md

@@ -0,0 +1,38 @@
+% Dokumentation
+% Tabea David <tabea.david@kf-interactive.com>; zitzmann@mpib-berlin.mpg.de
+
+## Inhalt
+
+- [Einführung](#einführung)
+- [Übersicht der Module](#übersicht-der-module)
+- [Verzeichnisstruktur](#verzeichnisstruktur)
+- [Modul 2: Diagramme verstehen](#modul-2-diagramme-verstehen)
+- [Entwicklungsumgebung](#build-tool-chain)
+- [Javascript](#javascript)
+- [Sass](#sass)
+- [Bilddateien](#bilddateien)
+- [Entwicklungshistorie](#entwicklungshistorie)
+
+## Einführung
+
+Das Projekt [RisikoAtlas] hat die Förderung der Risikokompetenz zum Ziel. Zu diesem Zweck wurden am Harding-Zentrum für Risikokompetenz am Max-Planck-Institut für Bildungsforschung digitale Werkzeuge entwickelt, die auf wissenschaftlichen Erkenntnissen beruhen. Neben interaktiven Visualisierungen evidenzbasierter Risikokommunikation, einer App zur Entscheidungsunterstützung und einer Browser-Erweiterung als Leseassistenz sind Lernvisualisierungen zur Verbesserung der Risikokompetenz Teil des Projektes.
+Das vorliegende Modul ist eines dieser sechs Lernmodule.
+
+Die Lernmodule wurden ursprünglich entwickelt von [kf interactive][kf-interactive].
+
+## Übersicht der Module
+
+Die folgende Liste gibt einen Überblick über die Ziele der Module:
+
+1. Module01 – Risiken vergleichen __\*__
+2. **Module02 – Diagramme verstehen** __\*__
+3. Module03 – Trends schätzen __\*__
+4. Module04 – Stichproben verstehen (original: [Rock 'n poll][rock-n-poll])
+5. Module05 – Relative Risiken verstehen __\*__
+6. Module06 – Wachstumsprozesse verstehen
+
+Alle mit einem __\*__ versehenen Module greifen über eine API auf eine Datenbank zu. Auf diese Weise rufen sie die benötigten Daten ab, und speichern andererseits Antworten der Benutzer, um ihnen den Vergleich zu Anderen zu ermöglichen. Für jedes dieser Module existiert auch eine offline-Version, die ausschließlich auf lokale Daten zugreift.
+
+[RisikoAtlas]: https://risikoatlas.de
+[kf-interactive]: https://www.kf-interactive.com
+[rock-n-poll]: http://rocknpoll.graphics/

+ 31 - 0
doc/02_structure.md

@@ -0,0 +1,31 @@
+## Verzeichnisstruktur
+
+Im Wurzelverzeichnis liegen die für den Build Prozess notwendigen Konfigurationsdateien.
+Das Verzeichnis `doc/` enthält detailliertere Dokumentationen zu einzelnen Aspekten des Projekts.
+Alle für die WebApp benötigten Dateien werden in `public/` erstellt bzw. dorthin kopiert.
+Im `src/` Verzeichnis befinden sich alle Quelldateien, Bilder und Fonts.
+Der `tasks/` Ordner enthält Javascript-Dateien, die die Teilschritte des Build-Prozesses definieren.
+
+Die grobe Struktur sieht folgendermaßen aus:
+
+```
+
+├── .editorconfig        // Konfiguration für Texteditoren
+├── .babelrc             // Konfiguration von babel
+├── .eslintrc.yml        // Konfiguration des Javascript Linters
+├── .htmlhintrc          // Konfiguration des HTML Linters
+├── .sass-lint.yml       // Konfiguration des Sass Linters
+├── config.js            // Konfiguration des Build-Systems
+├── gulpfile.babel.js    // gulp Datei, verwendet Definitionen unter `tasks/`
+├── package.json         // npm Abhängigkeiten, Shortcuts für gulp tasks
+├── doc/                 // Dokumentation in markdown
+├── public/              // Zielverzeichnis für den Build-Prozess
+├── src/                 // Quellverzeichnis
+│   ├── fonts            // Font Dateien
+│   ├── html             // HTML 'Templates'
+│   ├── img              // Bilder und Sprites
+│   ├── js               // Javascript Quelldateien
+│   └── scss             // Sass stylesheets
+└── tasks/               // Definitionen für den gulp Build-Prozess
+
+```

+ 197 - 0
doc/03_module.md

@@ -0,0 +1,197 @@
+## Modul 2: Diagramme verstehen
+
+Dieses Modul stellt ein interaktives Werkzeug als Hilfe zur Verbesserung des Verständnisses von Diagrammen bereit.  
+Dazu werden den Nutzern Multiple-Choice-Fragen zu Charakteristiken des gezeigten Graphen präsentiert. Die Lösung und gegebenenfalls die falsch gewählte Antwort werden grafisch hervorgehoben.
+Ein zweiter Typ von Test ermöglicht es den Nutzern, Teile des Diagramms auszuwählen und zu skalieren, um einen in der Frage vorgegebenen Effekt zu erzielen. Dies soll zeigen, wie einfache Manipulationen die Interpretation von Graphen beeinflussen können. Erst wenn eine korrekte Lösung gefunden wurde, kann zur folgenden Aufgabe weitergegangen werden.
+
+### Javascript Verzeichnisstruktur
+
+Für einen besseren Überblick sind die Quell-Dateien mit kurzen Beschreibungen aufgelistet:
+
+```
+├── main.jsx                  // Einstiegspunkt für App *mit* Verwendung der API ("online mode")
+├── main-offline.jsx          // Einstiegspunkt für App *ohne* Verwendung der API ("offline mode")
+├── config.js                 // Globale Konfiguration der WebApp
+├── components                // (p)react Komponenten
+│   ├── Index.jsx             // Web App Haupt-Komponente
+│   └── partials
+│       ├── GraphItem.jsx     // Komponente für d3 Visualisierungen
+│       ├── IntroItem.jsx     // Komponente für die Einführung zu den Fragen
+│       └── QuestionItem.jsx  // Komponente für Benutzereingaben für Fragen
+├── content
+│   ├── Gruppe-x_item-y.json  // Definitionen der Fragen
+│   ├── module.json           // Definition der (meisten) Labels und Texte des User Interfaces
+│   └── offline.js            // Definition der Reihenfolge der Fragen
+├── d3                        // d3 Module
+│   ├── configuration.js      // Konfiguration für d3-Module
+│   ├── axes.js               // Achsendefinitionen
+│   ├── main.js               // d3 Haupt-Modul
+│   ├── defs.js               // Defintionen für SVG-Element <defs> (clip-path, gradient)
+│   ├── gradient.js           // Darstellung der Steigung innerhalb eines Intervalls (Fragetyp 3)
+│   ├── grid.js               // Skalierendes Hintergrundraster
+│   ├── handleConnector.js    // Definition der Verbindungslinien zwischen Anfassern (Fragetyp 3)
+│   ├── handleSymbols.js      // Definition der Geometrie der Anfasser als SVG symbols (Fragetyp 3)
+│   ├── handle.js             // Konstruktor und Definition des Verhaltens der Anfasser (Fragetyp 3)
+│   ├── legend.js             // Legende der Graphen
+│   ├── bar.js                // Balkendiagramm als Feedback für Benutzer
+│   ├── lineGraphs.js         // Darstellung der Graphen
+│   ├── points.js             // Hervorhebung der Lösung / falschen Antwort (Fragetyp 1)
+│   ├── scales.js             // d3 Skalen für x- und y-Achsen
+│   ├── range.js              // Definition eines Auswahlbereichs (Fragetyp 3)
+│   └── rangeController.js    // Controller für Auswahlbereich; Kommunikation zwischen Visualisierungs-Modulen (untereinander) und Anwendung
+└── utilities
+    ├── api.js                // API für Lese- und Schreibzugriff auf die Datenbank
+    ├── enableTouch.js
+    ├── fonts.js
+    ├── formatter.js
+    ├── math.js
+    ├── randomizer.js
+    └── validator.js          // Funktionen zur Validierung der Benutzerantworten
+```
+
+### Wie ändere ich Bezeichner und Datenbasis?
+
+Um den Benutzern den Vergleich zu anderen zu ermöglichen, wird die Anbindung an eine Datenbank ermöglicht. Diese ist über eine API anzusprechen, die im Abschnitt "Online Version" dokumentiert ist. Es ist aber genauso möglich, auf diese Datenbankanbindung zu verzichten und stattdessen nur "lokale" Daten zu nutzen. Die dafür relevanten Dateien sind unter "Offline Version" dokumentiert.
+
+#### *Offline* Version
+
+Die "lokalen" Inhalte und Daten sind in den `json`-Dateien Dateien unter `src/js/content` definiert.
+
+#### Labels
+
+##### Umgebende Texte
+
+In `module.json` sind die umgebenden Texte und Labels definiert, die nicht direkt Teil der WebApp sind. Dies umfasst die einleitenden (`introtext`) und abschließenden Texte (`outrotext`) und die Labels der Buttons und Vergleichsansichten mit anderen Nutzern.
+
+    {
+      "title": "Wie werden Finanzstatistiken verzerrt?",
+      "introtext": "Um zu lernen, wie Statistiken verzerrt werden, …",
+      "outrotext": "Was haben Sie geübt? …",
+      "success": "Sie haben die Lösung erreicht!",
+      "next": "Weiter",
+      "start": "Start",
+      "restart": "Neustart",
+      "usersVoteHeadline": "So wählten die User",
+      "usersRight": "Andere Nutzer lagen richtig"
+    }
+
+##### Labels der WebApp
+
+Der Titel der Aufgabe, Achsenbezeichner und Legendentext werden aus der `json`-Datei der jeweiligen Aufgabe (Namensschema `Gruppe-<x>_item-<a>.json`) gelesen und können dort geändert werden (siehe anschließenden Abschnitt zu Daten). Die Beschriftung der x-Achse wird direkt aus den Daten übernommen, die y-Achse dagegen automatisch von der Javascript-Bibliothek [ d3js ](https://d3js.org) generiert.
+
+<pre>
+"text": "<b>Welchen Gewinn erzielte Citroniger im 4. Quartal 2016?</b>",
+"antworten": [
+    {
+      "antwort": "<b>15 Millionen</b>",
+      …
+    },
+    …
+],
+"data": [
+    "id": 1,
+    "konkurrent": "<b>Apfelreich</b>",
+    "koordinaten": [
+        { "x": <b>"Q1/2013"</b>, "y": 21.29 },
+        …
+    ],
+    …
+],
+</pre>
+
+#### Daten
+
+In der Datei `offline.js` wird die Reihenfolge der Fragen bzw. Aufgaben definiert. Die Daten der jeweiligen Aufgabe ist in der entsprechenden Datei benannt nach dem Schema `Gruppe-<x>_item-<y>.json` definiert.
+
+Zu jeder Aufgabe sind Metadaten definiert, wie die ID der Aufgabe, Titel (`text`) und `gruppe`, was den Aufgabentyp bezeichnet. Im `data` Array sind die Koordinaten und Metadaten der Graphen enthalten.
+
+    {
+      "id": 1,
+      "konkurrent": "Apfelreich",
+      "koordinaten": [
+        { "x": "Q1/2013", "y": 21.29 },
+        …
+      ]
+    }
+
+**1. Finden eines bestimmten x- oder y-Wertes eines Graphen z**
+
+Die Antworten dieses Typs bestehen aus einem Label (`"antwort"`) und Koordinaten für den entsprechenden Punkt, z.B.:
+
+    {
+      "antwort": "15 Millionen",
+      "korrekt": false,
+      "coordinates": {"x": "Q4/2016", "y": 15.00}
+    }
+
+**2. Beurteilung und Vergleich der Entwicklung der Graphen (negative / positive / unbeständige Entwicklung, Mittelwert)**
+
+Bei diesem Aufgabentyp soll der Graph benannt werden, der die gefragte Eigenschaft besitzt:
+
+    {
+      "antwort": "Apfelreich",
+      "korrekt": true
+    },
+
+**3. Manipulation des Graphen, so dass im betrachteten Teilsegment die grafische Wirkung der Steigung / des Gefälles verstärkt bzw. abgeschwächt wird.**
+
+Bei diesem Aufgabentyp sollen Graphen so manipuliert werden, dass im betrachteten Teilsegment die grafische Wirkung der Steigung / des Gefälles verstärkt bzw. abgeschwächt wird. Dies geschieht durch das Verändern der Grenzen einer Auswahl, das vertikale und horizontale Skalieren der Graphen basierend auf der gewählten Auswahl.
+
+Die Steigung wird grafisch visualisiert, sobald sich beide Punkte gleichzeitig innerhalb des Auswahlbereichs und des sichtbaren Bereichs befinden. Wurde der Graph so transformiert, dass die betrachtete Steigung innerhalb der Grenzwerte liegt, dann wird die Steigungsstrecke grün gefärbt und der Button zum fortfahren zur nächsten Aufgabe eingeblendet. Die Grenzwerte sind mit der Aufgabe definiert in der `json`-Datei als `anstiegUntereGrenze` und `anstiegObereGrenze`. Die Grenzen des betrachteten Intervalls werden mit `untereGrenze` und `obereGrenze` festgelegt.
+
+#### *Online* Version
+
+Die Konfiguration für den Zugriff auf die API ist in `src/js/config.js` definiert.
+Ein Request wird zusammengesetzt aus der URL der API, einem Endpunkt und optionalen Informationen, die je nach HTTP Methode als Parameter der URL angehängt werden, oder im *Message Body* des Requests verschickt werden.
+
+##### Anwendungsdaten
+
+1. Abfrage aller Aufgaben in einer Liste. Die Ordnung (Id) entspricht der Ordnung der in der Excel-Datei abgelegten Liste. Aufgaben sind zusätzlich qualifiziert durch "Gruppe" (int) und "Item" (string), aus denen der Name konstruiert wird (Gruppe=2, Item="c" => Name="Frage 2c")
+
+`GET api/M2_Aufgaben/Liste`
+
+2. Abfrage einer spezifischen Aufgabe
+
+`GET api/M2_Aufgaben/ById/{id}`
+
+3. Abfrage einer Liste von Aufgaben, die zu einer bestimmten Gruppe gehören
+
+`GET api/M2_Aufgaben/Gruppe?gruppe={gruppe}`
+
+Abfrage einer durch eine Gruppe und Item qualifizierte Aufgabe????
+
+`GET api/M2_Aufgaben/Auswahl?gruppe={gruppe}&item={item}`
+
+##### Benutzerdaten
+
+1. Abfrage aller Benutzer-Daten
+
+`GET api/M2_AufgabenUserData/Liste`
+
+2. Abfrage eines spezifischen Benutzer-Datensatzes
+
+`GET api/M2_AufgabenUserData/ById/{id}`
+
+3. Abfrage aller Daten für einen bestimmten Benutzer
+
+`GET api/M2_AufgabenUserData/ByUserId?userId={userId}`
+
+4. Gibt die UserData für eine durch eine Gruppe und Item qualifizierte Aufgabe zurück
+
+`GET api/M2_AufgabenUserData/ByAufgabe?gruppe={gruppe}&item={item}`
+
+5. Gibt die UserData für eine durch die AufgabeId qualifizierte Aufgabe zurück
+
+`GET api/M2_AufgabenUserData/ByAufgabeId?aufgabeId={aufgabeId}`
+
+6. Gibt für alle Fragen die Anteile korrekter und falscher Antworten zurück
+
+`GET api/M2_AufgabenUserData/Proportions`
+
+7. Gibt für eine bestimmte Frage die Anteile korrekter und falscher Antworten zurück
+
+`GET api/M2_AufgabenUserData/ProportionsByAufgabeId?aufgabeId={aufgabeId}`
+
+8. Erzeugt einen neuen Datenrecord in der M2_AufgabeUserData-Tabelle. Dabei sind die Angaben der AufgabeId und der UserId erforderlich, um die Antworten einer Aufgabe/einem User zuordnen zu können. Die Id wird nicht spezifiziert, da diese von der Datenbank automatisch zugewiesen wird
+
+`POST api/M2_AufgabenUserData/Create`

+ 101 - 0
doc/04_development.md

@@ -0,0 +1,101 @@
+## Build Tool Chain
+
+In diesem Abschnitt werden die technischen Voraussetzungen für die Erstellung von im Browser lauffähigem Code und und die Installation und Verwendung der Entwicklungsumgebung erläutert.
+
+### Voraussetzungen
+
+Dieses Projekt wurde entwickelt auf Basis von [nodejs] unter Verwendung von [npm] als Paket-Manager. Mit den folgenden Versionen wurde zuletzt getestet:
+
+```
+nodejs: v14.4.0
+   npm:  6.14.4
+```
+
+Alle Abhängigkeiten sind definiert in der `npm` Konfigurations-Datei `package.json`. Wie üblich werden diese installiert mit dem Befehl `npm install`. Als Task-Manager dieses Projekts wird [gulp] dabei global installiert.
+
+Für das Erstellen der Dokumentation aus den einzelnen *Markdown*-Dateien, die im Verzeichnis `doc/` liegen, wird [pandoc] verwendet. Dieses ist für viele Betriebssysteme und Distributionen verfügbar, muss aber gesondert installiert werden.
+
+### Konfiguration
+
+Die Build Konfiguration ist in `config.js` im Wurzelverzeichnis definiert. Außerdem sind in der Datei `package.json` die zu unterstützenden Browser-Versionen für `autoprefixer` angegeben.
+
+Konfigurationen für Babel, Editoren und *Linter* sind ebenfalls im Wurzelverzeichnis zu finden:
+
+```
+       babel: .babelrc
+editorconfig: .editorconfig
+        html: .htmlhintrc
+  javascript: .eslintrc.yml
+        sass: .sass-lint.yml
+```
+
+### Erstellen von Builds
+
+Alle Schritte zum Erstellen von Builds sind in den Javascript-Dateien unter `tasks/` definiert und werden von der `gulp` Konfigurationsdatei `gulpfile.babel.js` importiert. Dort sind die Teilschritte in *Tasks* zusammengefasst, die man am häufigsten benötigt.
+
+```
+$ gulp        # Default task, Kurzform für 'gulp watch'
+$ gulp build  # Erstellt einen Development Build
+$ gulp watch  # Erstellt einen Development Build und startet den Entwicklungsserver
+```
+#### Build Target
+
+Die Unterscheidung zwischen Development und Production Build wird anhand der `nodejs` Umgebungsvariable `NODE_ENV` vorgenommen. Ohne diese Angabe wird immer ein Development Build erstellt (siehe `config.js`). Für das Development Target werden Javascript und CSS zusätzlich mit *Sourcemaps* versehen, für die Produktiv-Version dagegen werden die Dateien von unnötigem Ballast befreit (`terser` für Javascript, `cssnano` für CSS).
+
+```
+# Erstellen eines Production Build
+$ NODE_ENV=production gulp build
+```
+#### Build Mode
+
+Zusätzlich gibt es für dieses Projekt die Unterscheidung zwischen 'online' und 'offline' Versionen. Im Fall der 'online' Version wird auf eine API zugegriffen, um die benötigten Inhalte zu laden, und um die Antworten der Benutzer zu speichern, um ihnen einen Vergleich mit Anderen zu ermöglichen. Diese Unterscheidung kann beim Aufruf von gulp auf der Kommandozeile mit einem Parameter getroffen werden. Soweit verfügbar, wird standardmäßig der 'online' Modus verwendet, (siehe `config.js`).
+
+```
+# Erstellen eines Development Build für den 'offline' Modus
+$ gulp --api-mode=offline
+```
+
+#### npm Shortcuts
+
+Da die Handhabung mit dem Setzen der Umgebungsvariable und das Übergeben des Parameters etwas umständlich ist, sind in `package.json` `npm` ein paar Shortcuts definiert, z.B.:
+
+```
+# Erstellen eines Production Build für den 'offline' Modus per gulp Script
+$ NODE_ENV=production gulp build --api-mode=offline
+
+# Erstellen eines Production Build für den 'offline' Modus per npm Script
+$ npm run build:prod:offline
+```
+
+### Erstellen der Dokumentation
+
+Die Dokumentation in einzelne *Markdown*-Dateien aufgeteilt, die im Verzeichnis `doc/` liegen. Zum Erstellen einer zusammenhängender Dokumentation sind folgende `npm` Scripts definiert, die auf `pandoc` basieren:
+
+```
+npm build:doc       // Kurzform für das Erstellen der Dokumentation im bevorzugten Ausgabeformat (Standard: Markdown)
+npm build:doc:html  // Erstellt eine HTML Dokumentation als `index.html` in `doc/html`
+npm build:doc:md    // Erstellt eine zusammenhängende Dokumentation als `readme.md` im Wurzelverzeichnis
+```
+
+### Konventionen
+
+Der Javascript Code ist in ES6 (bzw. ES2015) verfasst. Als CSS-Preprocessor wird Sass mit der `scss` Syntax verwendet. Die Code Style Konventionen wurden von den ursprünglichen Entwicklern übernommen und nur an wenigen Stellen leicht angepasst.
+
+Das Projekt verwendet [editorconfig] für die Integration dieser Konventionen in Editoren, die entsprechende Datei heißt `.editorconfig`.
+
+Für die statische Überprüfung des Quellcodes werden folgende *Linter* verwendet:
+
+- Javascript: [eslint]
+- Sass: [sass-lint]
+- HTML: [HTMLHint]
+
+Die zugehörigen Konfigurationsdateien befinden sich im Root-Verzeichnis, wie oben in der Auflistung angegeben.
+
+[editorconfig]: http://editorconfig.org
+[eslint]: https://eslint.org
+[gulp]: https://gulpjs.com/
+[HTMLHint]: https://github.com/htmlhint/HTMLHint
+[nodejs]: https://nodejs.org
+[npm]: https://www.npmjs.com/
+[pandoc]: https://pandoc.org
+[sass-lint]: https://github.com/sasstools/sass-lint

+ 32 - 0
doc/05_javascript.md

@@ -0,0 +1,32 @@
+## Javascript
+
+### Verwendete Bibliotheken
+
+Die grundlegende Architektur der WebApp wurde implementiert auf Basis von [preact], von den Entwicklern beworben mit
+
+> Fast 3kB alternative to React with the same modern API.
+
+Es ist allerdings keine exakte Reimplementierung, weswegen ein eigener Teil der Dokumentation der [Erläuterung der Unterschiede zu React][react-preact] gewidmet ist.
+
+Für Visualisierungen wird die großartige und weit verbreitete Bibliothek [D3.js] verwendet.
+
+### Verzeichnisstruktur
+
+Die folgende Auflistung gibt einen groben Überblick über die Verzeichnisstruktur der Javascript Quelldateien in `src/js/`. Als Einstieg dient `main.jsx` bzw. `main-offline.jsx` für den "offline" Modus.
+`config.js` ist die zentrale Konfigurationsdatei der WebApp. In `components` liegen die Komponenten der *preact*-WebApp. Der Quellcode für die D3-Visualisierungen befindet sich unter `d3`.
+
+```
+├── main.jsx            // Einstiegspunkt für App in "online" Modus
+├── main-offline.jsx    // Einstiegspunkt für App in "offline" Modus
+├── config.js           // Konfiguration der WebApp
+├── components/         // Verzeichnis für (p)react Komponenten
+│   ├── Index.jsx       // Web App Haupt-Komponente
+│   └── partials/       // Vezeichnis für Teilkomponenten
+├── content/            // Verzeichnis für "offline" Inhalte
+├── d3/                 // d3 Module
+└── utilities/          // Verzeichnis für Hilfs-Bibliotheken und Werkzeuge
+```
+
+[D3.js]: https://d3js.org/
+[preact]: https://preactjs.com/
+[react-preact]: https://preactjs.com/guide/v10/differences-to-react

+ 30 - 0
doc/06_styles.md

@@ -0,0 +1,30 @@
+## Sass
+
+Zum Kompilieren von Sass zu CSS wird `gulp-sass` verwendet, das `node-sass` benutzt, welches wiederum auf `libsass` basiert. `node-sass` hat sich beim wiederholten gedankenlosen Aktualisieren von `nodejs` und / oder `npm` als notorischer Nerventöter herausgestellt, daher an dieser Stelle der [Verweis zur *Troubleshooting* Dokumentation][node-sass-trouble] von `node-sass`. Meist reichte im Falle eines Problems aber ein `npm rebuild node-sass`.
+
+### Struktur
+
+In der Datei `main.scss` werden alle Stile eingebunden, die in den *Partials* definiert werden, woraus die endgültige CSS-Datei generiert wird. Die Struktur des `src/scss` Verzeichnisses sieht folgendermaßen aus:
+
+```
+├── base     // Stile für HTML Elemente
+├── config   // Globale Variable
+├── modules  // Stile für Module
+└── tools    // Definierte *mixins* und Funktionen
+```
+
+### Konventionen, Techniken und Tools
+
+Generell wird eine "mobile first" Strategie verfolgt. Als Standard-Einheit wird `rem` verwendet, auf deren Grundlage die Basis-Einheit definiert ist. Da sich alle Größen auf diese Einheit beziehen sollten, wird so das Skalieren des Layouts erleichtert.
+
+Sass wird in diesem Projekt mit der scss-Syntax verwendet. Stilistisch ist es in "oldschool BEM-Style" gehalten, Zitat der ursprünglichen Entwickler. Sie beziehen sich zudem auf bestimmte Guidelines:
+
+> Hugo Giraudel wrote an awesome piece on everything you need to know about Sass, it's called [Sass Guidelines][sass-guidelines] and you should really have a look at it. I agree with this guideline in almost all points, but I try to keep something more simple, and some things more strict, the linter will let you know :)
+
+ʕ̡̢̡ॢ•̫͡ॢ•ʔ̢̡̢
+
+Regeln mit Browser-spezifischen Präfixen (*vendor prefixes*) werden dem CSS automatisch durch [autoprefixer] hinzugefügt. Die Liste der zu unterstützenden Browser ist in `package.json` unter `browserslist` zu finden.
+
+[autoprefixer]: https://github.com/postcss/autoprefixer
+[node-sass-trouble]: https://github.com/sass/node-sass/blob/master/TROUBLESHOOTING.md
+[sass-guidelines]: http://sass-guidelin.es/

+ 15 - 0
doc/07_images.md

@@ -0,0 +1,15 @@
+## Bilddateien
+
+Alle in diesem Projekt verwendeten Bilddateien befinden sich unter `src/img/`.
+
+Icons in Form von SVG-Dateien befinden sich im Unterordner `src/img/sprites` und werden im Build-Prozess mittels `gulp-svg-sprite` zu einem Sprite zusammengefasst. Sie wie folgt in HTML referenziert werden:
+
+```
+<svg class="icon  icon--arrow-left">
+  <use xlink:href="assets/img/sprites.svg#icon--arrow-left"/>
+</svg>
+```
+
+Stil-Definitionen für Icons sind unter `src/scss/modules/_icons.scss` zu finden. Für die Unterstützung von Fragmentbezeichnern (*fragment identifier*) in Internet Explorer wird [svgxuse] verwendet.
+
+[svgxuse]: https://github.com/Keyamoon/svgxuse

+ 26 - 0
doc/08_history.md

@@ -0,0 +1,26 @@
+## Entwicklungshistorie
+
+Ursprünglich wurde dieses Modul als Auftragsarbeit von einer externen Firma entwickelt. Unglücklicherweise wurden während der Entwicklungsphase keine Code-Audits durchgeführt und die Qualität des gelieferten Codes ließ zu wünschen übrig. Teile des Codes mussten komplett neu entwickelt werden, da eine Fehlerbehebung nicht anders möglich erschien. Dies führte zu einer heterogenen Software-Architektur, da bei der Neuentwicklung in erster Linie auf Wartbarkeit und Verständlichkeit Wert gelegt wurde und einer angemessenen Dokumentierung.
+
+### Gründe für eine teilweise Neu-Implementierung des Moduls
+
+Teile des Codes mussten neu entwickelt werden, da insbesondere die originale Implementierung der Visualisierungs-Komponente größtenteils unwartbar war. Einfache Fehler zu beheben war nur schwer möglich, da der Code mit einer unerkennbaren Logik in Module aufgeteilt war und gängige Techniken und von d3 ignoriert wurden.
+
+1. Ein "Modul" mit dem Namen "redraw" war nur dazu gedacht, SVG-Elemente, die in anderen Code-Fragmenten erstellt wurden, zu aktualisieren. ﴾͡๏̯͡๏﴿  
+Um Wartbarkeit als Feature zu integrieren, wurde stattdessen die damals übliche Vorgehensweise implementiert ([General update pattern][d3-update-pattern]). Inzwischen wird diese schon wieder als veraltet bezeichnet, und die Verwendung von [selection.join][d3-selection-join] empfohlen.
+
+2. Multiple-Choice-Fragen wurden ursprünglich gegen die Labels der Antworten validiert. Dabei wurde die Javascript Funktion `parseInt` auf alphanumerische Strings angewendet, bzw. unflexible Reguläre Ausdrücke verwendet, die scheitern würden, sobald sich die Struktur der Labels oder die natürliche Sprache änderte. ( ・\_・)ノ⌒●~\*  
+Um dieses Problem zu beheben, wurde dem *offline*-Datensatz die notwendigen Daten hinzugefügt, um mathematische Validierung über Funktionen zu ermöglichen. Für jede Frage wird nun eine Referenz zu einer *Validator*-Funktion angegeben, die die Daten, die mit der gegebenen Antwort verknüpft sind, mit dem errechneten Ergebnis vergleicht. Aktuell ist die *online*-Version des Moduls nicht funktionsfähig, da die API noch nicht aktualisiert wurde (Stand Juni 2020).
+3. Die aktuelle Implementierung ist nach wie vor sehr unflexibel bezogen auf die Anordnung der verschiedenen Fragetypen. Die Fragen sind in einem zweidimensionalen Array angeordnet, wobei der erste Index den Fragetyp und der zweite die Reihenfolge innerhalb der Gruppe definiert. Verschiedene Fragetypen können daher momentan nicht willkürlich abgewechselt werden. Immerhin wurde der größte Fehler behoben, der beim einfachen Umordnen von Fragen innerhalb einer Gruppe zum "abstürzen" der Web App führte. ٩(̾●̮̮̃̾•̃̾)۶
+
+4. Die Implementierung des Aufgabentyps zum Manipulieren von Graphen war fehlerhaft. Das Ergebnis wurde nur dann richtigerweise als korrekt erkannt, wenn exakt das Intervall wie in der Aufgabenstellung ausgewählt wurde. Mehrere "falsche Lösungen" konnten gefunden werden. Dadurch konnte der Nutzer mit der folgenden Aufgabe fortführen, auch wenn die Steigung des betrachteten Intervalls außerhalb der Toleranzgrenzen lag. Auch waren Konstellationen möglich, bei denen der betrachtete Teil des Graphen außerhalb des sichtbaren Bereichs lag. Transformationen des Auswahlbereichs wurden durch das *Parsen* von Werten aus dem SVG DOM mit Hilfe von wackeligen Regulären Ausdrücken durchgeführt. Fügte man der Transformation eine Rotation hinzu, funktionierte nichts mehr. Das weckt die Vermutung, dass die Geometrie der Anfasser aus diesem Grund zweimal definiert wurde, siehe 5.  
+Dieser Teil wurde nahezu komplett neu implementiert, da der ursprüngliche Code zusätzlich zu den beschriebenen Fehlern unverständlich, verworren und dadurch unwartbar war. Es wurden "Techniken" verwendet, bei denen ein Modul in den SVG-DOM hineinschrieb und ein anderes Modul diesen Wert wieder auslas. ⊂(©෴©)つ  
+Zur Verbesserung der Verständlichkeit wurde eine Controller-zentrische Architektur in Verbindung mit spezifischem Event-Dispatching gewählt. Zur Validierung der Nutzer-gewählten Auswahl werden drei Auswahlbereiche definiert: die `reference range` bezeichnet den ursprünglich ausgewählten Bereich, auf den der benutzerdefinierte Bereich (`selected range`) initialisiert wird. Der Graph wird anhand letzterer transformiert. Die `solution range` definiert die exakte Lösung und wird für die Validierung der Nutzerauswahl verwendet. Sobald der betrachtete Teil des Graphen sichtbar ist und das Validierungskriterium der Frage erfüllt ist, wird die benutzerdefinierte Auswahl als Lösung anerkannt und das Fortfahren zur folgenden Frage wird ermöglicht.
+
+5. Die Geometrie der Anfasser zum Skalieren der Graphen wurde zweimal definiert, einmal für die horizontale und einmal für die vertikale Version, auch wenn sie sich nur durch eine 90 Grad Rotation unterscheiden (ganz der Wahrheit entsprechend ist das nicht, die inneren Linien waren in einem Fall falsch ausgerichtet). Auch dieser Code war so verworren und verständlich wie ein Stück Hirn in Aspik. In einem Aufruf der Funktion bezog sich die Variable "width" tatsächlich auf die Breite, im anderen Fall auf die Höhe (und umgekehrt bei "height"). Dieser Fakt wurde, vermutlich aus Scham, mit keinem Kommentar erwähnt. ℃ↂ\_ↂ  
+Die Geometrie ist nun nur einmal definiert und wird als SVG Symbol jeweils referenziert und durch Matrizen transformiert. Nebenbei wurde ein zeichnerisches Problem behoben.
+
+Dies sind nur Beispiele für die Probleme, die der ursprüngliche Code mitgebracht hat. Die Liste ist sicherlich nicht vollständig und es befinden sich höchstwahrscheinlich nach wie vor Eigenheiten und Fehler im Code, die man hinterfragen kann bzw. sollte.
+
+[d3-update-pattern]: https://observablehq.com/@d3/general-update-pattern?collection=@d3/d3-selection
+[d3-selection-join]: https://observablehq.com/@d3/selection-join

Datei-Diff unterdrückt, da er zu groß ist
+ 2 - 0
doc/html/css/github-markdown.css


+ 121 - 0
doc/html/css/github-syntax-highlight.css

@@ -0,0 +1,121 @@
+/*
+
+github.com style (c) Vasily Polovnyov <vast@whiteants.net>
+
+*/
+
+.hljs {
+  display: block;
+  overflow-x: auto;
+  padding: 0.5em;
+  color: #333;
+  background: #f8f8f8;
+  -webkit-text-size-adjust: none;
+}
+
+.hljs-comment,
+.diff .hljs-header,
+.hljs-javadoc {
+  color: #998;
+  font-style: italic;
+}
+
+.hljs-keyword,
+.css .rule .hljs-keyword,
+.hljs-winutils,
+.nginx .hljs-title,
+.hljs-subst,
+.hljs-request,
+.hljs-status {
+  color: #a71d5d;
+}
+
+.hljs-number,
+.hljs-hexcolor,
+.ruby .hljs-constant {
+  color: #0086b3;
+}
+
+.hljs-string,
+.hljs-tag .hljs-value,
+.hljs-phpdoc,
+.hljs-dartdoc,
+.tex .hljs-formula {
+  color: #df5000;
+}
+
+.hljs-title,
+.hljs-id,
+.scss .hljs-preprocessor {
+  color: #900;
+}
+
+.hljs-list .hljs-keyword,
+.hljs-subst {
+  font-weight: normal;
+}
+
+.hljs-class .hljs-title,
+.hljs-type,
+.vhdl .hljs-literal,
+.tex .hljs-command {
+  color: #795da3;
+}
+
+.hljs-tag,
+.hljs-tag .hljs-title,
+.hljs-rules .hljs-property,
+.django .hljs-tag .hljs-keyword {
+  color: #000080;
+  font-weight: normal;
+}
+
+.hljs-attribute,
+.hljs-variable,
+.lisp .hljs-body {
+  color: #008080;
+}
+
+.hljs-regexp {
+  color: #009926;
+}
+
+.hljs-symbol,
+.ruby .hljs-symbol .hljs-string,
+.lisp .hljs-keyword,
+.clojure .hljs-keyword,
+.scheme .hljs-keyword,
+.tex .hljs-special,
+.hljs-prompt {
+  color: #990073;
+}
+
+.hljs-built_in {
+  color: #795da3;
+}
+
+.hljs-preprocessor,
+.hljs-pragma,
+.hljs-pi,
+.hljs-doctype,
+.hljs-shebang,
+.hljs-cdata {
+  color: #999;
+  font-weight: bold;
+}
+
+.hljs-deletion {
+  background: #fdd;
+}
+
+.hljs-addition {
+  background: #dfd;
+}
+
+.diff .hljs-change {
+  background: #0086b3;
+}
+
+.hljs-chunk {
+  color: #aaa;
+}

+ 233 - 0
doc/html/index.html

@@ -0,0 +1,233 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
+<head>
+  <meta charset="utf-8" />
+  <meta name="generator" content="pandoc" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
+  <meta name="author" content="Tabea David tabea.david@kf-interactive.com" />
+  <meta name="author" content="zitzmann@mpib-berlin.mpg.de" />
+  <title>Dokumentation</title>
+  <style>
+    code{white-space: pre-wrap;}
+    span.smallcaps{font-variant: small-caps;}
+    span.underline{text-decoration: underline;}
+    div.column{display: inline-block; vertical-align: top; width: 50%;}
+    div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
+    ul.task-list{list-style: none;}
+  </style>
+  <link rel="stylesheet" href="css/github-markdown.css" />
+  <link rel="stylesheet" href="css/github-syntax-highlight.css" />
+  <!--[if lt IE 9]>
+    <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
+  <![endif]-->
+</head>
+<body>
+<header id="title-block-header">
+<h1 class="title">Dokumentation</h1>
+<p class="author">Tabea David <a href="mailto:tabea.david@kf-interactive.com" class="email">tabea.david@kf-interactive.com</a></p>
+<p class="author">zitzmann@mpib-berlin.mpg.de</p>
+</header>
+<p><link rel="stylesheet" href="css/cssgithub-markdown.css" /> <link rel="stylesheet" href="css/github-syntax-highlight.css" /></p>
+<h2 id="table-of-contents">Table of contents</h2>
+<ul>
+<li><a href="#einführung">Einführung</a></li>
+<li><a href="#übersicht-der-module">Übersicht der Module</a></li>
+<li><a href="#verzeichnisstruktur">Verzeichnisstruktur</a></li>
+<li><a href="#modul-2-diagramme-verstehen">Modul 2: Diagramme verstehen</a></li>
+<li><a href="#build-tool-chain">Build Tool Chain</a></li>
+<li><a href="#javascript">Javascript</a></li>
+<li><a href="#sass">Sass</a></li>
+<li><a href="#bilddateien">Bilddateien</a></li>
+<li><a href="#entwicklungshistorie">Entwicklungshistorie</a></li>
+</ul>
+<h2 id="einführung">Einführung</h2>
+<p>Das Projekt <a href="https://risikoatlas.de">RisikoAtlas</a> hat die Förderung der Risikokompetenz zum Ziel. Zu diesem Zweck wurden am Harding-Zentrum für Risikokompetenz am Max-Planck-Institut für Bildungsforschung digitale Werkzeuge entwickelt, die auf wissenschaftlichen Erkenntnissen beruhen. Neben interaktiven Visualisierungen evidenzbasierter Risikokommunikation, einer App zur Entscheidungsunterstützung und einer Browser-Erweiterung als Leseassistenz sind Lernvisualisierungen zur Verbesserung der Risikokompetenz Teil des Projektes. Das vorliegende Modul ist eines dieser sechs Lernmodule.</p>
+<p>Die Lernmodule wurden ursprünglich entwickelt von <a href="https://www.kf-interactive.com">kf interactive</a>.</p>
+<h2 id="übersicht-der-module">Übersicht der Module</h2>
+<p>Die folgende Liste gibt einen Überblick über die Ziele der Module:</p>
+<ol type="1">
+<li>Module01 – Risiken vergleichen <strong>*</strong></li>
+<li><strong>Module02 – Diagramme verstehen</strong> <strong>*</strong></li>
+<li>Module03 – Trends schätzen <strong>*</strong></li>
+<li>Module04 – Stichproben verstehen (original: <a href="http://rocknpoll.graphics/">Rock ’n poll</a>)</li>
+<li>Module05 – Relative Risiken verstehen <strong>*</strong></li>
+<li>Module06 – Wachstumsprozesse verstehen</li>
+</ol>
+<p>Alle mit einem <strong>*</strong> versehenen Module greifen über eine API auf eine Datenbank zu. Auf diese Weise rufen sie die benötigten Daten ab, und speichern andererseits Benutzerantworten, um Nutzern den Vergleich zu Anderen zu ermöglichen. Für jedes dieser Module existiert auch eine offline-Version, die ausschließlich auf lokale Daten zugreift.</p>
+<h2 id="verzeichnisstruktur">Verzeichnisstruktur</h2>
+<p>Im Wurzelverzeichnis liegen die für den Build Prozess notwendigen Konfigurationsdateien. Das Verzeichnis <code>doc/</code> enthält detailliertere Dokumentationen zu einzelnen Aspekten des Projekts. Alle für die WebApp benötigten Dateien werden in <code>public/</code> erstellt bzw. dorthin kopiert. Im <code>src/</code> Verzeichnis befinden sich alle Quelldateien, Bilder und Fonts. Der <code>tasks/</code> Ordner enthält Javascript-Dateien, die die Teilschritte des Build-Prozesses definieren.</p>
+<p>Die grobe Struktur sieht folgendermaßen aus:</p>
+<pre><code>
+├── .editorconfig        // Konfiguration für Texteditoren
+├── .babelrc             // Konfiguration von babel
+├── .eslintrc.yml        // Konfiguration des Javascript Linters
+├── .htmlhintrc          // Konfiguration des HTML Linters
+├── .sass-lint.yml       // Konfiguration des Sass Linters
+├── config.js            // Konfiguration des Build-Systems
+├── gulpfile.babel.js    // gulp Datei, verwendet Definitionen unter `tasks/`
+├── package.json         // npm Abhängigkeiten, Shortcuts für gulp tasks
+├── doc/                 // Dokumentation in markdown
+├── public/              // Zielverzeichnis für den Build-Prozess
+├── src/                 // Quellverzeichnis
+│   ├── fonts            // Font Dateien
+│   ├── html             // HTML &#39;Templates&#39;
+│   ├── img              // Bilder und Sprites
+│   ├── js               // Javascript Quelldateien
+│   └── scss             // Sass stylesheets
+└── tasks/               // Definitionen für den gulp Build-Prozess
+</code></pre>
+<h3 id="konventionen">Konventionen</h3>
+<p>Die Code Style Konventionen wurden von den ursprünglichen Entwicklern übernommen und nur an wenigen Stellen angepasst. Der Javascript Code ist in ES6 (bzw. ES2015) verfasst und als CSS-Preprocessor wird Sass mit der <code>scss</code> Syntax verwendet.</p>
+<p>Das Projekt verwendet <a href="http://editorconfig.org">editorconfig</a> für die Integration dieser Konventionen in Editoren, die entsprechende Datei heißt <code>.editorconfig</code>.</p>
+<p>Für die statische Überprüfung des Quellcodes werden folgende <em>Linter</em> verwendet:</p>
+<ul>
+<li>Javascript: <a href="https://eslint.org">eslint</a></li>
+<li>Sass: <a href="https://github.com/sasstools/sass-lint">sass-lint</a></li>
+<li>HTML: <a href="https://github.com/htmlhint/HTMLHint">HTMLHint</a></li>
+</ul>
+<p>Die zugehörigen Konfigurationsdateien befinden sich im Root-Verzeichnis, wie oben in der Auflistung angegeben.</p>
+<h2 id="modul-2-diagramme-verstehen">Modul 2: Diagramme verstehen</h2>
+<p>Dieses Modul stellt ein interaktives Werkzeug als Hilfe zur Verbesserung des Verständnisses von Diagrammen bereit.<br />
+Dazu werden den Nutzern Multiple-Choice-Fragen zu Charakteristiken des gezeigten Graphen präsentiert. Die Lösung und gegebenenfalls die falsch gewählte Antwort werden grafisch hervorgehoben. Ein zweiter Typ von Test ermöglicht es den Nutzern, Teile des Diagramms auszuwählen und zu skalieren, um einen in der Frage vorgegebenen Effekt zu erzielen. Dies soll zeigen, wie einfache Manipulationen die Interpretation von Graphen beeinflussen können. Erst wenn eine korrekte Lösung gefunden wurde, kann zur folgenden Aufgabe weitergegangen werden.</p>
+<h3 id="javascript-verzeichnisstruktur">Javascript Verzeichnisstruktur</h3>
+<p>Für einen besseren Überblick sind die Quell-Dateien mit kurzen Beschreibungen aufgelistet:</p>
+<pre><code>├── main.jsx                  // Einstiegspunkt für App *mit* Verwendung der API (&quot;online mode&quot;)
+├── main-offline.jsx          // Einstiegspunkt für App *ohne* Verwendung der API (&quot;offline mode&quot;)
+├── config.js
+├── components                // (p)react Komponenten
+│   ├── Index.jsx             // Web App Haupt-Komponente
+│   └── partials
+│       ├── GraphItem.jsx     // Komponente für d3 Visualisierungen
+│       ├── IntroItem.jsx     // Komponente für die Einführung zu den Fragen
+│       └── QuestionItem.jsx  // Komponente für Benutzereingaben für Fragen
+├── content
+│   ├── Gruppe-x_item-y.json  // Definitionen der Fragen
+│   ├── module.json           // Definition der (meisten) Labels und Texte des User Interfaces
+│   └── offline.js            // Definition der Reihenfolge der Fragen
+├── modules                   // d3 Module
+│   ├── configuration.js      // Konfiguration für d3-Module
+│   ├── axes.js               // Achsendefinitionen
+│   ├── main.js               // d3 Haupt-Modul
+│   ├── defs.js               // Defintionen für SVG-Element &lt;defs&gt; (clip-path, gradient)
+│   ├── gradient.js           // Darstellung der Steigung innerhalb eines Intervalls (Fragetyp 3)
+│   ├── grid.js               // Skalierendes Hintergrundraster
+│   ├── handleConnector.js    // Definition der Verbindungslinien zwischen Anfassern (Fragetyp 3)
+│   ├── handleSymbols.js      // Definition der Geometrie der Anfasser als SVG symbols (Fragetyp 3)
+│   ├── handle.js             // Konstruktor und Definition des Verhaltens der Anfasser (Fragetyp 3)
+│   ├── legend.js             // Legende der Graphen
+│   ├── bar.js                // Balkendiagramm als Feedback für Benutzer
+│   ├── lineGraphs.js         // Darstellung der Graphen
+│   ├── points.js             // Hervorhebung der Lösung / falschen Antwort (Fragetyp 1)
+│   ├── scales.js             // d3 Skalen für x- und y-Achsen
+│   ├── range.js              // Definition eines Auswahlbereichs (Fragetyp 3)
+│   └── rangeController.js    // Controller für Auswahlbereich; Kommunikation zwischen Visualisierungs-Modulen (untereinander) und Anwendung
+└── utilities
+    ├── api.js                // API für Lesen aus / schreiben in Datenbank
+    ├── touch.js
+    ├── fonts.js
+    ├── formatter.js
+    └── math.js
+    ├── randomizer.js
+    └── validator.js          // Funktionen zur Validierung der Benutzerantworten</code></pre>
+<h2 id="build-tool-chain">Build Tool Chain</h2>
+<p>In diesem Abschnitt werden die technischen Voraussetzungen für die Erstellung von im Browser lauffähigem Code und und die Installation und Verwendung der Entwicklungsumgebung erläutert.</p>
+<h3 id="voraussetzungen">Voraussetzungen</h3>
+<p>Dieses Projekt wurde entwickelt auf Basis von <a href="https://nodejs.org"><code>nodejs</code></a> unter Verwendung von <a href="https://www.npmjs.com/"><code>npm</code></a> als Paket-Manager. Mit den folgenden Versionen wurde zuletzt getestet:</p>
+<pre><code>nodejs: v14.4.0
+   npm:  6.14.4</code></pre>
+<p>Alle Abhängigkeiten sind definiert in der <code>npm</code> Konfigurations-Datei <code>package.json</code>. Wie üblich werden diese installiert mit dem Befehl <code>npm install</code>. <a href="https://gulpjs.com/"><code>gulp</code></a> wird dabei(??? ist das zutreffend?) als Task-Manager dieses Projekts global installiert.</p>
+<p>TODO: Testen mit node LTS 12..</p>
+<h3 id="konfiguration">Konfiguration</h3>
+<p>Die Build Konfiguration ist in <code>config.js</code> im Wurzelverzeichnis definiert. Außerdem sind in der Datei <code>package.json</code> die zu unterstützenden Browser-Versionen für <code>autoprefixer</code> angegeben.</p>
+<p>Konfigurationen für Babel, Editoren und <em>Linter</em> sind ebenfalls im Wurzelverzeichnis zu finden:</p>
+<pre><code>       babel: .babelrc
+editorconfig: .editorconfig
+        html: .htmlhintrc
+  javascript: .eslintrc.yml
+        sass: .sass-lint.yml</code></pre>
+<h3 id="erstellen-von-builds">Erstellen von Builds</h3>
+<p>Alle Schritte zum Erstellen von Builds sind in den Javascript-Dateien unter <code>tasks/</code> definiert und werden von der <code>gulp</code> Konfigurationsdatei <code>gulpfile.babel.js</code> importiert. Dort sind die Teilschritte in <em>Tasks</em> zusammengefasst, die man am häufigsten benötigt.</p>
+<pre><code>$ gulp        # Default task, Kurzform für &#39;gulp watch&#39;
+$ gulp build  # Erstellt einen Development Build
+$ gulp watch  # Erstellt einen Development Build und startet den Entwicklungsserver</code></pre>
+<h4 id="build-target">Build Target</h4>
+<p>Die Unterscheidung zwischen Development und Production Build wird anhand der <code>nodejs</code> Umgebungsvariable <code>NODE_ENV</code> vorgenommen. Ohne diese Angabe wird immer ein Development Build erstellt (siehe <code>config.js</code>). Für das Development Target werden Javascript und CSS zusätzlich mit <em>Sourcemaps</em> versehen, für die Produktiv-Version dagegen werden die Dateien von unnötigem Ballast befreit (<code>terser</code> für Javascript, <code>cssnano</code> für CSS).</p>
+<pre><code># Erstellen eines Production Build
+$ NODE_ENV=production gulp build</code></pre>
+<h4 id="build-mode">Build Mode</h4>
+<p>Zusätzlich gibt es für dieses Projekt die Unterscheidung zwischen ‘online’ und ‘offline’ Versionen. Im Fall der ‘online’ Version wird auf eine API zugegriffen, um die benötigten Inhalte zu laden, und um die Antworten der Benutzer zu speichern, um ihnen einen Vergleich mit Anderen zu ermöglichen. Diese Unterscheidung kann beim Aufruf von gulp auf der Kommandozeile mit einem Parameter getroffen werden. Soweit verfügbar, wird standardmäßig der ‘online’ Modus verwendet, (siehe <code>config.js</code>).</p>
+<pre><code># Erstellen eines Development Build für den &#39;offline&#39; Modus
+$ gulp --api-mode=offline</code></pre>
+<h4 id="npm-shortcuts">npm Shortcuts</h4>
+<p>Da die Handhabung mit dem Setzen der Umgebungsvariable und das Übergeben des Parameters etwas umständlich ist, sind in <code>package.json</code> <code>npm</code> ein paar Shortcuts definiert, z.B.:</p>
+<pre><code># Erstellen eines Production Build für den &#39;offline&#39; Modus per gulp Script
+$ NODE_ENV=production gulp build --api-mode=offline
+
+# Erstellen eines Production Build für den &#39;offline&#39; Modus per npm Script
+$ npm run build:prod:offline</code></pre>
+<h3 id="konventionen-1">Konventionen</h3>
+<p>Der Javascript Code ist in ES6 (bzw. ES2015) verfasst. Als CSS-Preprocessor wird Sass mit der <code>scss</code> Syntax verwendet. Die Code Style Konventionen wurden von den ursprünglichen Entwicklern übernommen und nur an wenigen Stellen leicht angepasst.</p>
+<p>Erleichtert wird die Anwendung des definierten Programmierstils durch die Integration in Editoren mit Hilfe von <a href="http://editorconfig.org">editorconfig</a>. Die entsprechende Konfigurationsdatei ist im Wurzelverzeichnis als <code>.editorconfig</code> zu finden.</p>
+<p>Die statische Überprüfung des Javascript Quellcodes wird mit Hilfe von <a href="https://eslint.org">eslint</a> durchgeführt.Für Sass wird <a href="https://github.com/sasstools/sass-lint">sass-lint</a> verwendet und HTML wird mit Hilfe von <a href="https://github.com/htmlhint/HTMLHint">HTMLHint</a> überprüft.</p>
+<p>Die zugehörigen Konfigurationsdateien befinden sich im Wurzelverzeichnis, wie oben in der Auflistung angegeben.</p>
+<h2 id="javascript">Javascript</h2>
+<h3 id="verwendete-bibliotheken">Verwendete Bibliotheken</h3>
+<p>Die grundlegende Architektur der WebApp wurde implementiert auf Basis von <a href="https://preactjs.com/">preact</a>, von den Entwicklern beworben mit</p>
+<blockquote>
+<p>Fast 3kB alternative to React with the same modern API.</p>
+</blockquote>
+<p>Es ist allerdings keine exakte Reimplementierung, weswegen ein eigener Teil der Dokumentation der <a href="https://preactjs.com/guide/v10/differences-to-react">Erläuterung der Unterschiede zu React</a> gewidmet ist.</p>
+<p>Für Visualisierungen wird die großartige und weit verbreitete Bibliothek <a href="https://d3js.org/">D3.js</a> verwendet.</p>
+<h3 id="verzeichnisstruktur-1">Verzeichnisstruktur</h3>
+<p>Die folgende Auflistung gibt einen groben Überblick über die Verzeichnisstruktur der Javascript Quelldateien in <code>src/js/</code>. Als Einstieg dient <code>main.jsx</code> bzw. <code>main-offline.jsx</code> für den “offline” Modus. <code>config.js</code> ist die zentrale Konfigurationsdatei der WebApp. In <code>components</code> liegen die Komponenten der <em>preact</em>-WebApp. Der Quellcode für die D3-Visualisierungen befindet sich unter <code>d3</code>.</p>
+<pre><code>├── main.jsx            // Einstiegspunkt für App in &quot;online&quot; Modus
+├── main-offline.jsx    // Einstiegspunkt für App in &quot;offline&quot; Modus
+├── config.js           // Konfiguration der WebApp
+├── components/         // Verzeichnis für (p)react Komponenten
+│   ├── Index.jsx       // Web App Haupt-Komponente
+│   └── partials/       // Vezeichnis für Teilkomponenten
+├── content/            // Verzeichnis für &quot;offline&quot; Inhalte
+├── d3/                 // d3 Module
+└── utilities/          // Verzeichnis für Hilfs-Bibliotheken und Werkzeuge</code></pre>
+<h2 id="sass">Sass</h2>
+<p>Zum Kompilieren von Sass zu CSS wird <code>gulp-sass</code> verwendet, das <code>node-sass</code> benutzt, welches wiederum auf <code>libsass</code> basiert. <code>node-sass</code> hat sich beim wiederholten gedankenlosen Aktualisieren von <code>nodejs</code> und / oder <code>npm</code> als notorischer Nerventöter herausgestellt, daher an dieser Stelle der <a href="https://github.com/sass/node-sass/blob/master/TROUBLESHOOTING.md">Verweis zur <em>Troubleshooting</em> Dokumentation</a> von <code>node-sass</code>. Meist reichte im Falle eines Problems aber ein <code>npm rebuild node-sass</code>.</p>
+<h3 id="struktur">Struktur</h3>
+<p>In der Datei <code>main.scss</code> werden alle Stile eingebunden, die in den <em>Partials</em> definiert werden, woraus die endgültige CSS-Datei generiert wird. Die Struktur des <code>src/scss</code> Verzeichnisses sieht folgendermaßen aus:</p>
+<pre><code>├── base     // Stile für HTML Elemente
+├── config   // Globale Variable
+├── modules  // Stile für Module
+└── tools    // Definierte *mixins* und Funktionen</code></pre>
+<h3 id="konventionen-techniken-und-tools">Konventionen, Techniken und Tools</h3>
+<p>Generell wird eine “mobile first” Strategie verfolgt. Als Standard-Einheit wird <code>rem</code> verwendet, auf deren Grundlage die Basis-Einheit definiert ist. Da sich alle Größen auf diese Einheit beziehen sollten, wird so das Skalieren des Layouts erleichtert.</p>
+<p>Sass wird in diesem Projekt mit der scss-Syntax verwendet. Stilistisch ist es in “oldschool BEM-Style” gehalten, Zitat der ursprünglichen Entwickler. Sie beziehen sich zudem auf bestimmte Guidelines:</p>
+<blockquote>
+<p>Hugo Giraudel wrote an awesome piece on everything you need to know about Sass, it’s called <a href="http://sass-guidelin.es/">Sass Guidelines</a> and you should really have a look at it. I agree with this guideline in almost all points, but I try to keep something more simple, and some things more strict, the linter will let you know :)</p>
+</blockquote>
+<p>ʕ̡̢̡ॢ•̫͡ॢ•ʔ̢̡̢</p>
+<p>Regeln mit Browser-spezifischen Präfixen (<em>vendor prefixes</em>) werden dem CSS automatisch durch <a href="https://github.com/postcss/autoprefixer">autoprefixer</a> hinzugefügt. Die Liste der zu unterstützenden Browser ist in <code>package.json</code> unter <code>browserslist</code> zu finden.</p>
+<h2 id="bilddateien">Bilddateien</h2>
+<p>Alle in diesem Projekt verwendeten Bilddateien befinden sich unter <code>src/img/</code>.</p>
+<p>Icons in Form von SVG-Dateien befinden sich im Unterordner <code>src/img/sprites</code> und werden im Build-Prozess mittels <code>gulp-svg-sprite</code> zu einem Sprite zusammengefasst. Sie wie folgt in HTML referenziert werden:</p>
+<pre><code>&lt;svg class=&quot;icon  icon--arrow-left&quot;&gt;
+  &lt;use xlink:href=&quot;assets/img/sprites.svg#icon--arrow-left&quot;/&gt;
+&lt;/svg&gt;</code></pre>
+<p>Stil-Definitionen für Icons sind unter <code>src/scss/modules/_icons.scss</code> zu finden. Für die Unterstützung von Fragmentbezeichnern (<em>fragment identifier</em>) in Internet Explorer wird <a href="https://github.com/Keyamoon/svgxuse">svgxuse</a> verwendet.</p>
+<h2 id="entwicklungshistorie">Entwicklungshistorie</h2>
+<p>Ursprünglich wurde dieses Modul als Auftragsarbeit von einer externen Firma entwickelt. Unglücklicherweise wurden während der Entwicklungsphase keine Code-Audits durchgeführt und die Qualität des gelieferten Codes ließ zu wünschen übrig. Teile des Codes mussten komplett neu entwickelt werden, da eine Fehlerbehebung nicht anders möglich erschien. Dies führte zu einer heterogenen Software-Architektur, da bei der Neuentwicklung in erster Linie auf Wartbarkeit und Verständlichkeit Wert gelegt wurde und einer angemessenen Dokumentierung.</p>
+<h3 id="gründe-für-eine-teilweise-neu-implementierung-des-moduls">Gründe für eine teilweise Neu-Implementierung des Moduls</h3>
+<p>Teile des Codes mussten neu entwickelt werden, da insbesondere die originale Implementierung der Visualisierungs-Komponente größtenteils unwartbar war. Einfache Fehler zu beheben war nur schwer möglich, da der Code mit einer unerkennbaren Logik in Module aufgeteilt war und gängige Techniken und von d3 ignoriert wurden.</p>
+<ol type="1">
+<li><p>Ein “Modul” mit dem Namen “redraw” war nur dazu gedacht, SVG-Elemente, die in anderen Code-Fragmenten erstellt wurden, zu aktualisieren. ﴾͡๏̯͡๏﴿<br />
+Um Wartbarkeit als Feature zu integrieren, wurde stattdessen die damals übliche Vorgehensweise implementiert (<a href="https://observablehq.com/@d3/general-update-pattern?collection=@d3/d3-selection">General update pattern</a>). Inzwischen wird diese schon wieder als veraltet bezeichnet, und die Verwendung von <a href="https://observablehq.com/@d3/selection-join">selection.join</a> empfohlen.</p></li>
+<li><p>Multiple-Choice-Fragen wurden ursprünglich gegen die Labels der Antworten validiert. Dabei wurde die Javascript Funktion <code>parseInt</code> auf alphanumerische Strings angewendet, bzw. unflexible Reguläre Ausdrücke verwendet, die scheitern würden, sobald sich die Struktur der Labels oder die natürliche Sprache änderte. ( ・_・)ノ⌒●~*<br />
+Um dieses Problem zu beheben, wurde dem <em>offline</em>-Datensatz die notwendigen Daten hinzugefügt, um mathematische Validierung über Funktionen zu ermöglichen. Für jede Frage wird nun eine Referenz zu einer <em>Validator</em>-Funktion angegeben, die die Daten, die mit der gegebenen Antwort verknüpft sind, mit dem errechneten Ergebnis vergleicht. Aktuell ist die <em>online</em>-Version des Moduls nicht funktionsfähig, da die API noch nicht aktualisiert wurde (Stand Juni 2020).</p></li>
+<li><p>Die aktuelle Implementierung ist nach wie vor sehr unflexibel bezogen auf die Anordnung der verschiedenen Fragetypen. Die Fragen sind in einem zweidimensionalen Array angeordnet, wobei der erste Index den Fragetyp und der zweite die Reihenfolge innerhalb der Gruppe definiert. Verschiedene Fragetypen können daher momentan nicht willkürlich abgewechselt werden. Immerhin wurde der größte Fehler behoben, der beim einfachen Umordnen von Fragen innerhalb einer Gruppe zum “abstürzen” der Web App führte. ٩(̾●̮̮̃̾•̃̾)۶</p></li>
+<li><p>Die Implementierung des Aufgabentyps zum Manipulieren von Graphen war fehlerhaft. Das Ergebnis wurde nur dann richtigerweise als korrekt erkannt, wenn exakt das Intervall wie in der Aufgabenstellung ausgewählt wurde. Mehrere “falsche Lösungen” konnten gefunden werden. Dadurch konnte der Nutzer mit der folgenden Aufgabe fortführen, auch wenn die Steigung des betrachteten Intervalls außerhalb der Toleranzgrenzen lag. Auch waren Konstellationen möglich, bei denen der betrachtete Teil des Graphen außerhalb des sichtbaren Bereichs lag. Transformationen des Auswahlbereichs wurden durch das <em>Parsen</em> von Werten aus dem SVG DOM mit Hilfe von wackeligen Regulären Ausdrücken durchgeführt. Fügte man der Transformation eine Rotation hinzu, funktionierte nichts mehr. Das weckt die Vermutung, dass die Geometrie der Anfasser aus diesem Grund zweimal definiert wurde, siehe 5.<br />
+Dieser Teil wurde nahezu komplett neu implementiert, da der ursprüngliche Code zusätzlich zu den beschriebenen Fehlern unverständlich, verworren und dadurch unwartbar war. Es wurden “Techniken” verwendet, bei denen ein Modul in den SVG-DOM hineinschrieb und ein anderes Modul diesen Wert wieder auslas. ⊂(©෴©)つ<br />
+Zur Verbesserung der Verständlichkeit wurde eine Controller-zentrische Architektur in Verbindung mit spezifischem Event-Dispatching gewählt. Zur Validierung der Nutzer-gewählten Auswahl werden drei Auswahlbereiche definiert: die <code>reference range</code> bezeichnet den ursprünglich ausgewählten Bereich, auf den der benutzerdefinierte Bereich (<code>selected range</code>) initialisiert wird. Der Graph wird anhand letzterer transformiert. Die <code>solution range</code> definiert die exakte Lösung und wird für die Validierung der Nutzerauswahl verwendet. Sobald der betrachtete Teil des Graphen sichtbar ist und das Validierungskriterium der Frage erfüllt ist, wird die benutzerdefinierte Auswahl als Lösung anerkannt und das Fortfahren zur folgenden Frage wird ermöglicht.</p></li>
+<li><p>Die Geometrie der Anfasser zum Skalieren der Graphen wurde zweimal definiert, einmal für die horizontale und einmal für die vertikale Version, auch wenn sie sich nur durch eine 90 Grad Rotation unterscheiden (ganz der Wahrheit entsprechend ist das nicht, die inneren Linien waren in einem Fall falsch ausgerichtet). Auch dieser Code war so verworren und verständlich wie ein Stück Hirn in Aspik. In einem Aufruf der Funktion bezog sich die Variable “width” tatsächlich auf die Breite, im anderen Fall auf die Höhe (und umgekehrt bei “height”). Dieser Fakt wurde, vermutlich aus Scham, mit keinem Kommentar erwähnt. ℃ↂ_ↂ<br />
+Die Geometrie ist nun nur einmal definiert und wird als SVG Symbol jeweils referenziert und durch Matrizen transformiert. Nebenbei wurde ein zeichnerisches Problem behoben.</p></li>
+</ol>
+<p>Dies sind nur Beispiele für die Probleme, die der ursprüngliche Code mitgebracht hat. Die Liste ist sicherlich nicht vollständig und es befinden sich höchstwahrscheinlich nach wie vor Eigenheiten und Fehler im Code, die man hinterfragen kann bzw. sollte.</p>
+</body>
+</html>

+ 127 - 0
gulpfile.babel.js

@@ -0,0 +1,127 @@
+/**
+ * Available build configurations: 4 in case of modules connecting to API, otherwise only 2
+ * - development - offline
+ * - development - online
+ * - production - offline
+ * - production - online
+ *
+ * Distinction between development and production targets by environment variable process.env.NODE_ENV
+ * Online / offline versions (API usage or locally defined data) by command line arguments (default: online)
+ */
+
+import path from 'path';
+import config from './config';
+
+import gulp from 'gulp';
+
+import { clean } from './tasks/clean';
+import { html } from './tasks/html';
+import { sprites } from './tasks/sprites';
+import { copyFonts, copyImages } from './tasks/copy';
+import { lint, lintJs, lintSass, lintHtml } from './tasks/lint';
+import { styles } from './tasks/styles';
+import { scripts } from './tasks/scripts';
+import { reload, serve } from './tasks/server';
+
+// Delete previously built files, generate html and sprites
+const prepare = gulp.series(
+  clean,
+  html,
+  sprites
+);
+
+// Copy assets
+const finish = gulp.parallel(
+  copyFonts,
+  copyImages
+);
+
+// Execute complete build process
+const build = gulp.series(
+  prepare,
+  gulp.parallel(lintSass, lintJs, lintHtml),
+  gulp.parallel(styles, scripts),
+  finish
+);
+
+// Watch files and react accordingly
+const watchFiles = () => {
+  // HTML
+  gulp.watch(
+    path.join(
+      config.paths.src,
+      config.paths.html,
+      '*.html'
+    ),
+    gulp.series(html, reload)
+  );
+
+  // Javascript
+  gulp.watch(
+    path.join(
+      config.paths.src,
+      config.paths.js,
+      '/**/*.(js|jsx|json)'
+    ),
+    gulp.series(scripts, reload)
+  );
+
+  // Sass
+  gulp.watch(
+    path.join(
+      config.paths.src,
+      config.paths.sass,
+      '/**/*.scss'
+    ),
+    gulp.series(styles, reload)
+  );
+
+  // Fonts
+  gulp.watch(
+    path.join(
+      config.paths.src,
+      config.paths.fonts,
+      '*.(woff|woff2)'
+    ),
+    gulp.series(copyFonts, reload)
+  );
+
+  // Images
+  gulp.watch(
+    path.join(
+      config.paths.src,
+      config.paths.images,
+      '*.(png|jpg)'
+    ),
+    gulp.series(copyImages, reload)
+  );
+
+  // Sprites
+  gulp.watch(
+    path.join(
+      config.paths.src,
+      config.paths.images,
+      config.paths.sprites,
+      '*.svg'
+    ),
+    gulp.series(sprites, copyImages, reload)
+  );
+};
+
+// Build first, then serve and watch files
+const watch = gulp.series(
+  build,
+  serve,
+  watchFiles
+);
+
+// hack-a-di-hack: export named default task as default
+// (which is a javascript keyword but at the same time the name
+// of the gulp default task. Very good, gulp. Or, maybe, rather not
+export {
+  watch as default,
+  build,
+  lint,
+  prepare,
+  watch
+};

+ 78 - 0
package.json

@@ -0,0 +1,78 @@
+{
+  "name": "manipulating-graphs",
+  "version": "1.0.0",
+  "description": "Wie werden Finanzstatistiken verzerrt?",
+  "author": "Tabea David <tabea.david@kf-interactive.com>, zitzmann@mpib-berlin.mpg.de",
+  "browserslist": [
+    "last 2 versions",
+    "ie >= 11"
+  ],
+  "devDependencies": {
+    "@babel/core": "^7.10.0",
+    "@babel/plugin-proposal-object-rest-spread": "^7.10.0",
+    "@babel/plugin-transform-object-assign": "^7.8.3",
+    "@babel/plugin-transform-react-jsx": "^7.9.4",
+    "@babel/preset-env": "^7.10.0",
+    "@babel/preset-react": "^7.10.0",
+    "@babel/register": "^7.9.0",
+    "@rollup/plugin-babel": "^5.0.2",
+    "@rollup/plugin-commonjs": "^12.0.0",
+    "@rollup/plugin-json": "^4.0.3",
+    "@rollup/plugin-node-resolve": "^8.0.0",
+    "autoprefixer": "^9.8.0",
+    "babel-eslint": "^10.1.0",
+    "babel-preset-env": "^1.7.0",
+    "babel-preset-react": "^6.24.1",
+    "browser-sync": "^2.26.7",
+    "cssnano": "^4.1.10",
+    "del": "^5.1.0",
+    "eslint-plugin-react": "^7.20.0",
+    "gulp": "^4.0.2",
+    "gulp-cached": "^1.1.1",
+    "gulp-eslint": "^6.0.0",
+    "gulp-htmlhint": "^3.0.0",
+    "gulp-if": "^3.0.0",
+    "gulp-plumber": "^1.2.1",
+    "gulp-postcss": "^8.0.0",
+    "gulp-rename": "^2.0.0",
+    "gulp-replace": "^1.0.0",
+    "gulp-sass": "^4.1.0",
+    "gulp-sass-lint": "^1.4.0",
+    "gulp-sourcemaps": "^2.6.5",
+    "gulp-svg-sprite": "^1.5.0",
+    "lodash": "^4.17.15",
+    "posthtml": "^0.13.0",
+    "posthtml-expressions": "^1.4.1",
+    "rollup": "^2.10.9",
+    "rollup-plugin-terser": "^6.1.0",
+    "yargs": "^15.3.1"
+  },
+  "scripts": {
+    "build": "gulp build",
+    "build:dev": "npm run build",
+    "build:prod": "NODE_ENV=production npm run build",
+    "build:dev:offline": "npm run build --api-mode=offline",
+    "build:prod:offline": "NODE_ENV=production npm run build --api-mode=offline",
+    "build:doc": "npm run build:doc:md",
+    "build:doc:html": "pandoc --css css/github-markdown.css --css css/github-syntax-highlight.css -s -o doc/html/index.html $(ls doc/*.md)",
+    "build:doc:md": "pandoc -o readme.md $(ls doc/*.md)",
+    "preinstall": "npm i -g gulp",
+    "start": "gulp",
+    "watch": "npm start",
+    "watch:dev": "npm run watch",
+    "watch:prod": "NODE_ENV=production npm run watch",
+    "watch:dev:offline": "npm run watch --api-mode=offline",
+    "watch:prod:offline": "NODE_ENV=production npm run watch --api-mode=offline"
+  },
+  "dependencies": {
+    "d3": "^5.16.0",
+    "es6-promise": "^4.2.8",
+    "fontfaceobserver": "^2.1.0",
+    "frckl-helpers": "^1.0.0",
+    "frckl-reset": "^1.1.1",
+    "frckl-tools": "^2.0.1",
+    "normalize.css": "^8.0.1",
+    "preact": "^10.4.4",
+    "svgxuse": "^1.2.6"
+  }
+}

+ 9 - 0
public/browserconfig.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<browserconfig>
+  <msapplication>
+    <tile>
+      <square150x150logo src="/assets/img/mstile-150x150.png"/>
+      <TileColor>#d0021b</TileColor>
+    </tile>
+  </msapplication>
+</browserconfig>

BIN
public/favicon.ico


+ 5 - 0
public/robots.txt

@@ -0,0 +1,5 @@
+# www.robotstxt.org/
+
+# Allow crawling of all content
+User-agent: *
+Disallow:

+ 666 - 0
readme.md

@@ -0,0 +1,666 @@
+`<link rel="stylesheet" href="css/cssgithub-markdown.css" />`{=html}
+`<link rel="stylesheet" href="css/github-syntax-highlight.css" />`{=html}
+
+## Inhalt
+
+-   [Einführung](#einführung)
+-   [Übersicht der Module](#übersicht-der-module)
+-   [Verzeichnisstruktur](#verzeichnisstruktur)
+-   [Modul 2: Diagramme verstehen](#modul-2-diagramme-verstehen)
+-   [Entwicklungsumgebung](#build-tool-chain)
+-   [Javascript](#javascript)
+-   [Sass](#sass)
+-   [Bilddateien](#bilddateien)
+-   [Entwicklungshistorie](#entwicklungshistorie)
+
+## Einführung
+
+Das Projekt [RisikoAtlas](https://risikoatlas.de) hat die Förderung der
+Risikokompetenz zum Ziel. Zu diesem Zweck wurden am Harding-Zentrum für
+Risikokompetenz am Max-Planck-Institut für Bildungsforschung digitale
+Werkzeuge entwickelt, die auf wissenschaftlichen Erkenntnissen beruhen.
+Neben interaktiven Visualisierungen evidenzbasierter
+Risikokommunikation, einer App zur Entscheidungsunterstützung und einer
+Browser-Erweiterung als Leseassistenz sind Lernvisualisierungen zur
+Verbesserung der Risikokompetenz Teil des Projektes. Das vorliegende
+Modul ist eines dieser sechs Lernmodule.
+
+Die Lernmodule wurden ursprünglich entwickelt von [kf
+interactive](https://www.kf-interactive.com).
+
+## Übersicht der Module
+
+Die folgende Liste gibt einen Überblick über die Ziele der Module:
+
+1.  Module01 -- Risiken vergleichen **\***
+2.  **Module02 -- Diagramme verstehen** **\***
+3.  Module03 -- Trends schätzen **\***
+4.  Module04 -- Stichproben verstehen (original: [Rock 'n
+    poll](http://rocknpoll.graphics/))
+5.  Module05 -- Relative Risiken verstehen **\***
+6.  Module06 -- Wachstumsprozesse verstehen
+
+Alle mit einem **\*** versehenen Module greifen über eine API auf eine
+Datenbank zu. Auf diese Weise rufen sie die benötigten Daten ab, und
+speichern andererseits Antworten der Benutzer, um ihnen den Vergleich zu
+Anderen zu ermöglichen. Für jedes dieser Module existiert auch eine
+offline-Version, die ausschließlich auf lokale Daten zugreift.
+
+## Verzeichnisstruktur
+
+Im Wurzelverzeichnis liegen die für den Build Prozess notwendigen
+Konfigurationsdateien. Das Verzeichnis `doc/` enthält detailliertere
+Dokumentationen zu einzelnen Aspekten des Projekts. Alle für die WebApp
+benötigten Dateien werden in `public/` erstellt bzw. dorthin kopiert. Im
+`src/` Verzeichnis befinden sich alle Quelldateien, Bilder und Fonts.
+Der `tasks/` Ordner enthält Javascript-Dateien, die die Teilschritte des
+Build-Prozesses definieren.
+
+Die grobe Struktur sieht folgendermaßen aus:
+
+
+    ├── .editorconfig        // Konfiguration für Texteditoren
+    ├── .babelrc             // Konfiguration von babel
+    ├── .eslintrc.yml        // Konfiguration des Javascript Linters
+    ├── .htmlhintrc          // Konfiguration des HTML Linters
+    ├── .sass-lint.yml       // Konfiguration des Sass Linters
+    ├── config.js            // Konfiguration des Build-Systems
+    ├── gulpfile.babel.js    // gulp Datei, verwendet Definitionen unter `tasks/`
+    ├── package.json         // npm Abhängigkeiten, Shortcuts für gulp tasks
+    ├── doc/                 // Dokumentation in markdown
+    ├── public/              // Zielverzeichnis für den Build-Prozess
+    ├── src/                 // Quellverzeichnis
+    │   ├── fonts            // Font Dateien
+    │   ├── html             // HTML 'Templates'
+    │   ├── img              // Bilder und Sprites
+    │   ├── js               // Javascript Quelldateien
+    │   └── scss             // Sass stylesheets
+    └── tasks/               // Definitionen für den gulp Build-Prozess
+
+## Modul 2: Diagramme verstehen
+
+Dieses Modul stellt ein interaktives Werkzeug als Hilfe zur Verbesserung
+des Verständnisses von Diagrammen bereit.\
+Dazu werden den Nutzern Multiple-Choice-Fragen zu Charakteristiken des
+gezeigten Graphen präsentiert. Die Lösung und gegebenenfalls die falsch
+gewählte Antwort werden grafisch hervorgehoben. Ein zweiter Typ von Test
+ermöglicht es den Nutzern, Teile des Diagramms auszuwählen und zu
+skalieren, um einen in der Frage vorgegebenen Effekt zu erzielen. Dies
+soll zeigen, wie einfache Manipulationen die Interpretation von Graphen
+beeinflussen können. Erst wenn eine korrekte Lösung gefunden wurde, kann
+zur folgenden Aufgabe weitergegangen werden.
+
+### Javascript Verzeichnisstruktur
+
+Für einen besseren Überblick sind die Quell-Dateien mit kurzen
+Beschreibungen aufgelistet:
+
+    ├── main.jsx                  // Einstiegspunkt für App *mit* Verwendung der API ("online mode")
+    ├── main-offline.jsx          // Einstiegspunkt für App *ohne* Verwendung der API ("offline mode")
+    ├── config.js                 // Globale Konfiguration der WebApp
+    ├── components                // (p)react Komponenten
+    │   ├── Index.jsx             // Web App Haupt-Komponente
+    │   └── partials
+    │       ├── GraphItem.jsx     // Komponente für d3 Visualisierungen
+    │       ├── IntroItem.jsx     // Komponente für die Einführung zu den Fragen
+    │       └── QuestionItem.jsx  // Komponente für Benutzereingaben für Fragen
+    ├── content
+    │   ├── Gruppe-x_item-y.json  // Definitionen der Fragen
+    │   ├── module.json           // Definition der (meisten) Labels und Texte des User Interfaces
+    │   └── offline.js            // Definition der Reihenfolge der Fragen
+    ├── d3                        // d3 Module
+    │   ├── configuration.js      // Konfiguration für d3-Module
+    │   ├── axes.js               // Achsendefinitionen
+    │   ├── main.js               // d3 Haupt-Modul
+    │   ├── defs.js               // Defintionen für SVG-Element <defs> (clip-path, gradient)
+    │   ├── gradient.js           // Darstellung der Steigung innerhalb eines Intervalls (Fragetyp 3)
+    │   ├── grid.js               // Skalierendes Hintergrundraster
+    │   ├── handleConnector.js    // Definition der Verbindungslinien zwischen Anfassern (Fragetyp 3)
+    │   ├── handleSymbols.js      // Definition der Geometrie der Anfasser als SVG symbols (Fragetyp 3)
+    │   ├── handle.js             // Konstruktor und Definition des Verhaltens der Anfasser (Fragetyp 3)
+    │   ├── legend.js             // Legende der Graphen
+    │   ├── bar.js                // Balkendiagramm als Feedback für Benutzer
+    │   ├── lineGraphs.js         // Darstellung der Graphen
+    │   ├── points.js             // Hervorhebung der Lösung / falschen Antwort (Fragetyp 1)
+    │   ├── scales.js             // d3 Skalen für x- und y-Achsen
+    │   ├── range.js              // Definition eines Auswahlbereichs (Fragetyp 3)
+    │   └── rangeController.js    // Controller für Auswahlbereich; Kommunikation zwischen Visualisierungs-Modulen (untereinander) und Anwendung
+    └── utilities
+        ├── api.js                // API für Lese- und Schreibzugriff auf die Datenbank
+        ├── enableTouch.js
+        ├── fonts.js
+        ├── formatter.js
+        ├── math.js
+        ├── randomizer.js
+        └── validator.js          // Funktionen zur Validierung der Benutzerantworten
+
+### Wie ändere ich Bezeichner und Datenbasis?
+
+Um den Benutzern den Vergleich zu anderen zu ermöglichen, wird die
+Anbindung an eine Datenbank ermöglicht. Diese ist über eine API
+anzusprechen, die im Abschnitt "Online Version" dokumentiert ist. Es ist
+aber genauso möglich, auf diese Datenbankanbindung zu verzichten und
+stattdessen nur "lokale" Daten zu nutzen. Die dafür relevanten Dateien
+sind unter "Offline Version" dokumentiert.
+
+#### *Offline* Version
+
+Die "lokalen" Inhalte und Daten sind in den `json`-Dateien Dateien unter
+`src/js/content` definiert.
+
+#### Labels
+
+##### Umgebende Texte
+
+In `module.json` sind die umgebenden Texte und Labels definiert, die
+nicht direkt Teil der WebApp sind. Dies umfasst die einleitenden
+(`introtext`) und abschließenden Texte (`outrotext`) und die Labels der
+Buttons und Vergleichsansichten mit anderen Nutzern.
+
+    {
+      "title": "Wie werden Finanzstatistiken verzerrt?",
+      "introtext": "Um zu lernen, wie Statistiken verzerrt werden, …",
+      "outrotext": "Was haben Sie geübt? …",
+      "success": "Sie haben die Lösung erreicht!",
+      "next": "Weiter",
+      "start": "Start",
+      "restart": "Neustart",
+      "usersVoteHeadline": "So wählten die User",
+      "usersRight": "Andere Nutzer lagen richtig"
+    }
+
+##### Labels der WebApp
+
+Der Titel der Aufgabe, Achsenbezeichner und Legendentext werden aus der
+`json`-Datei der jeweiligen Aufgabe (Namensschema
+`Gruppe-<x>_item-<a>.json`) gelesen und können dort geändert werden
+(siehe anschließenden Abschnitt zu Daten). Die Beschriftung der x-Achse
+wird direkt aus den Daten übernommen, die y-Achse dagegen automatisch
+von der Javascript-Bibliothek [d3js](https://d3js.org) generiert.
+
+```{=html}
+<pre>
+"text": "<b>Welchen Gewinn erzielte Citroniger im 4. Quartal 2016?</b>",
+"antworten": [
+    {
+      "antwort": "<b>15 Millionen</b>",
+      …
+    },
+    …
+],
+"data": [
+    "id": 1,
+    "konkurrent": "<b>Apfelreich</b>",
+    "koordinaten": [
+        { "x": <b>"Q1/2013"</b>, "y": 21.29 },
+        …
+    ],
+    …
+],
+</pre>
+```
+#### Daten
+
+In der Datei `offline.js` wird die Reihenfolge der Fragen bzw. Aufgaben
+definiert. Die Daten der jeweiligen Aufgabe ist in der entsprechenden
+Datei benannt nach dem Schema `Gruppe-<x>_item-<y>.json` definiert.
+
+Zu jeder Aufgabe sind Metadaten definiert, wie die ID der Aufgabe, Titel
+(`text`) und `gruppe`, was den Aufgabentyp bezeichnet. Im `data` Array
+sind die Koordinaten und Metadaten der Graphen enthalten.
+
+    {
+      "id": 1,
+      "konkurrent": "Apfelreich",
+      "koordinaten": [
+        { "x": "Q1/2013", "y": 21.29 },
+        …
+      ]
+    }
+
+**1. Finden eines bestimmten x- oder y-Wertes eines Graphen z**
+
+Die Antworten dieses Typs bestehen aus einem Label (`"antwort"`) und
+Koordinaten für den entsprechenden Punkt, z.B.:
+
+    {
+      "antwort": "15 Millionen",
+      "korrekt": false,
+      "coordinates": {"x": "Q4/2016", "y": 15.00}
+    }
+
+**2. Beurteilung und Vergleich der Entwicklung der Graphen (negative /
+positive / unbeständige Entwicklung, Mittelwert)**
+
+Bei diesem Aufgabentyp soll der Graph benannt werden, der die gefragte
+Eigenschaft besitzt:
+
+    {
+      "antwort": "Apfelreich",
+      "korrekt": true
+    },
+
+**3. Manipulation des Graphen, so dass im betrachteten Teilsegment die
+grafische Wirkung der Steigung / des Gefälles verstärkt bzw.
+abgeschwächt wird.**
+
+Bei diesem Aufgabentyp sollen Graphen so manipuliert werden, dass im
+betrachteten Teilsegment die grafische Wirkung der Steigung / des
+Gefälles verstärkt bzw. abgeschwächt wird. Dies geschieht durch das
+Verändern der Grenzen einer Auswahl, das vertikale und horizontale
+Skalieren der Graphen basierend auf der gewählten Auswahl.
+
+Die Steigung wird grafisch visualisiert, sobald sich beide Punkte
+gleichzeitig innerhalb des Auswahlbereichs und des sichtbaren Bereichs
+befinden. Wurde der Graph so transformiert, dass die betrachtete
+Steigung innerhalb der Grenzwerte liegt, dann wird die Steigungsstrecke
+grün gefärbt und der Button zum fortfahren zur nächsten Aufgabe
+eingeblendet. Die Grenzwerte sind mit der Aufgabe definiert in der
+`json`-Datei als `anstiegUntereGrenze` und `anstiegObereGrenze`. Die
+Grenzen des betrachteten Intervalls werden mit `untereGrenze` und
+`obereGrenze` festgelegt.
+
+#### *Online* Version
+
+Die Konfiguration für den Zugriff auf die API ist in `src/js/config.js`
+definiert. Ein Request wird zusammengesetzt aus der URL der API, einem
+Endpunkt und optionalen Informationen, die je nach HTTP Methode als
+Parameter der URL angehängt werden, oder im *Message Body* des Requests
+verschickt werden.
+
+##### Anwendungsdaten
+
+1.  Abfrage aller Aufgaben in einer Liste. Die Ordnung (Id) entspricht
+    der Ordnung der in der Excel-Datei abgelegten Liste. Aufgaben sind
+    zusätzlich qualifiziert durch "Gruppe" (int) und "Item" (string),
+    aus denen der Name konstruiert wird (Gruppe=2, Item="c" =\>
+    Name="Frage 2c")
+
+`GET api/M2_Aufgaben/Liste`
+
+2.  Abfrage einer spezifischen Aufgabe
+
+`GET api/M2_Aufgaben/ById/{id}`
+
+3.  Abfrage einer Liste von Aufgaben, die zu einer bestimmten Gruppe
+    gehören
+
+`GET api/M2_Aufgaben/Gruppe?gruppe={gruppe}`
+
+Abfrage einer durch eine Gruppe und Item qualifizierte Aufgabe????
+
+`GET api/M2_Aufgaben/Auswahl?gruppe={gruppe}&item={item}`
+
+##### Benutzerdaten
+
+1.  Abfrage aller Benutzer-Daten
+
+`GET api/M2_AufgabenUserData/Liste`
+
+2.  Abfrage eines spezifischen Benutzer-Datensatzes
+
+`GET api/M2_AufgabenUserData/ById/{id}`
+
+3.  Abfrage aller Daten für einen bestimmten Benutzer
+
+`GET api/M2_AufgabenUserData/ByUserId?userId={userId}`
+
+4.  Gibt die UserData für eine durch eine Gruppe und Item qualifizierte
+    Aufgabe zurück
+
+`GET api/M2_AufgabenUserData/ByAufgabe?gruppe={gruppe}&item={item}`
+
+5.  Gibt die UserData für eine durch die AufgabeId qualifizierte Aufgabe
+    zurück
+
+`GET api/M2_AufgabenUserData/ByAufgabeId?aufgabeId={aufgabeId}`
+
+6.  Gibt für alle Fragen die Anteile korrekter und falscher Antworten
+    zurück
+
+`GET api/M2_AufgabenUserData/Proportions`
+
+7.  Gibt für eine bestimmte Frage die Anteile korrekter und falscher
+    Antworten zurück
+
+`GET api/M2_AufgabenUserData/ProportionsByAufgabeId?aufgabeId={aufgabeId}`
+
+8.  Erzeugt einen neuen Datenrecord in der M2_AufgabeUserData-Tabelle.
+    Dabei sind die Angaben der AufgabeId und der UserId erforderlich, um
+    die Antworten einer Aufgabe/einem User zuordnen zu können. Die Id
+    wird nicht spezifiziert, da diese von der Datenbank automatisch
+    zugewiesen wird
+
+`POST api/M2_AufgabenUserData/Create`
+
+## Build Tool Chain
+
+In diesem Abschnitt werden die technischen Voraussetzungen für die
+Erstellung von im Browser lauffähigem Code und und die Installation und
+Verwendung der Entwicklungsumgebung erläutert.
+
+### Voraussetzungen
+
+Dieses Projekt wurde entwickelt auf Basis von
+[nodejs](https://nodejs.org) unter Verwendung von
+[npm](https://www.npmjs.com/) als Paket-Manager. Mit den folgenden
+Versionen wurde zuletzt getestet:
+
+    nodejs: v14.4.0
+       npm:  6.14.4
+
+Alle Abhängigkeiten sind definiert in der `npm` Konfigurations-Datei
+`package.json`. Wie üblich werden diese installiert mit dem Befehl
+`npm install`. Als Task-Manager dieses Projekts wird
+[gulp](https://gulpjs.com/) dabei global installiert.
+
+Für das Erstellen der Dokumentation aus den einzelnen
+*Markdown*-Dateien, die im Verzeichnis `doc/` liegen, wird
+[pandoc](https://pandoc.org) verwendet. Dieses ist für viele
+Betriebssysteme und Distributionen verfügbar, muss aber gesondert
+installiert werden.
+
+### Konfiguration
+
+Die Build Konfiguration ist in `config.js` im Wurzelverzeichnis
+definiert. Außerdem sind in der Datei `package.json` die zu
+unterstützenden Browser-Versionen für `autoprefixer` angegeben.
+
+Konfigurationen für Babel, Editoren und *Linter* sind ebenfalls im
+Wurzelverzeichnis zu finden:
+
+           babel: .babelrc
+    editorconfig: .editorconfig
+            html: .htmlhintrc
+      javascript: .eslintrc.yml
+            sass: .sass-lint.yml
+
+### Erstellen von Builds
+
+Alle Schritte zum Erstellen von Builds sind in den Javascript-Dateien
+unter `tasks/` definiert und werden von der `gulp` Konfigurationsdatei
+`gulpfile.babel.js` importiert. Dort sind die Teilschritte in *Tasks*
+zusammengefasst, die man am häufigsten benötigt.
+
+    $ gulp        # Default task, Kurzform für 'gulp watch'
+    $ gulp build  # Erstellt einen Development Build
+    $ gulp watch  # Erstellt einen Development Build und startet den Entwicklungsserver
+
+#### Build Target
+
+Die Unterscheidung zwischen Development und Production Build wird anhand
+der `nodejs` Umgebungsvariable `NODE_ENV` vorgenommen. Ohne diese Angabe
+wird immer ein Development Build erstellt (siehe `config.js`). Für das
+Development Target werden Javascript und CSS zusätzlich mit *Sourcemaps*
+versehen, für die Produktiv-Version dagegen werden die Dateien von
+unnötigem Ballast befreit (`terser` für Javascript, `cssnano` für CSS).
+
+    # Erstellen eines Production Build
+    $ NODE_ENV=production gulp build
+
+#### Build Mode
+
+Zusätzlich gibt es für dieses Projekt die Unterscheidung zwischen
+'online' und 'offline' Versionen. Im Fall der 'online' Version wird auf
+eine API zugegriffen, um die benötigten Inhalte zu laden, und um die
+Antworten der Benutzer zu speichern, um ihnen einen Vergleich mit
+Anderen zu ermöglichen. Diese Unterscheidung kann beim Aufruf von gulp
+auf der Kommandozeile mit einem Parameter getroffen werden. Soweit
+verfügbar, wird standardmäßig der 'online' Modus verwendet, (siehe
+`config.js`).
+
+    # Erstellen eines Development Build für den 'offline' Modus
+    $ gulp --api-mode=offline
+
+#### npm Shortcuts
+
+Da die Handhabung mit dem Setzen der Umgebungsvariable und das Übergeben
+des Parameters etwas umständlich ist, sind in `package.json` `npm` ein
+paar Shortcuts definiert, z.B.:
+
+    # Erstellen eines Production Build für den 'offline' Modus per gulp Script
+    $ NODE_ENV=production gulp build --api-mode=offline
+
+    # Erstellen eines Production Build für den 'offline' Modus per npm Script
+    $ npm run build:prod:offline
+
+### Erstellen der Dokumentation
+
+Die Dokumentation in einzelne *Markdown*-Dateien aufgeteilt, die im
+Verzeichnis `doc/` liegen. Zum Erstellen einer zusammenhängender
+Dokumentation sind folgende `npm` Scripts definiert, die auf `pandoc`
+basieren:
+
+    npm build:doc       // Kurzform für das Erstellen der Dokumentation im bevorzugten Ausgabeformat (Standard: Markdown)
+    npm build:doc:html  // Erstellt eine HTML Dokumentation als `index.html` in `doc/html`
+    npm build:doc:md    // Erstellt eine zusammenhängende Dokumentation als `readme.md` im Wurzelverzeichnis
+
+### Konventionen
+
+Der Javascript Code ist in ES6 (bzw. ES2015) verfasst. Als
+CSS-Preprocessor wird Sass mit der `scss` Syntax verwendet. Die Code
+Style Konventionen wurden von den ursprünglichen Entwicklern übernommen
+und nur an wenigen Stellen leicht angepasst.
+
+Das Projekt verwendet [editorconfig](http://editorconfig.org) für die
+Integration dieser Konventionen in Editoren, die entsprechende Datei
+heißt `.editorconfig`.
+
+Für die statische Überprüfung des Quellcodes werden folgende *Linter*
+verwendet:
+
+-   Javascript: [eslint](https://eslint.org)
+-   Sass: [sass-lint](https://github.com/sasstools/sass-lint)
+-   HTML: [HTMLHint](https://github.com/htmlhint/HTMLHint)
+
+Die zugehörigen Konfigurationsdateien befinden sich im Root-Verzeichnis,
+wie oben in der Auflistung angegeben.
+
+## Javascript
+
+### Verwendete Bibliotheken
+
+Die grundlegende Architektur der WebApp wurde implementiert auf Basis
+von [preact](https://preactjs.com/), von den Entwicklern beworben mit
+
+> Fast 3kB alternative to React with the same modern API.
+
+Es ist allerdings keine exakte Reimplementierung, weswegen ein eigener
+Teil der Dokumentation der [Erläuterung der Unterschiede zu
+React](https://preactjs.com/guide/v10/differences-to-react) gewidmet
+ist.
+
+Für Visualisierungen wird die großartige und weit verbreitete Bibliothek
+[D3.js](https://d3js.org/) verwendet.
+
+### Verzeichnisstruktur
+
+Die folgende Auflistung gibt einen groben Überblick über die
+Verzeichnisstruktur der Javascript Quelldateien in `src/js/`. Als
+Einstieg dient `main.jsx` bzw. `main-offline.jsx` für den "offline"
+Modus. `config.js` ist die zentrale Konfigurationsdatei der WebApp. In
+`components` liegen die Komponenten der *preact*-WebApp. Der Quellcode
+für die D3-Visualisierungen befindet sich unter `d3`.
+
+    ├── main.jsx            // Einstiegspunkt für App in "online" Modus
+    ├── main-offline.jsx    // Einstiegspunkt für App in "offline" Modus
+    ├── config.js           // Konfiguration der WebApp
+    ├── components/         // Verzeichnis für (p)react Komponenten
+    │   ├── Index.jsx       // Web App Haupt-Komponente
+    │   └── partials/       // Vezeichnis für Teilkomponenten
+    ├── content/            // Verzeichnis für "offline" Inhalte
+    ├── d3/                 // d3 Module
+    └── utilities/          // Verzeichnis für Hilfs-Bibliotheken und Werkzeuge
+
+## Sass
+
+Zum Kompilieren von Sass zu CSS wird `gulp-sass` verwendet, das
+`node-sass` benutzt, welches wiederum auf `libsass` basiert. `node-sass`
+hat sich beim wiederholten gedankenlosen Aktualisieren von `nodejs` und
+/ oder `npm` als notorischer Nerventöter herausgestellt, daher an dieser
+Stelle der [Verweis zur *Troubleshooting*
+Dokumentation](https://github.com/sass/node-sass/blob/master/TROUBLESHOOTING.md)
+von `node-sass`. Meist reichte im Falle eines Problems aber ein
+`npm rebuild node-sass`.
+
+### Struktur
+
+In der Datei `main.scss` werden alle Stile eingebunden, die in den
+*Partials* definiert werden, woraus die endgültige CSS-Datei generiert
+wird. Die Struktur des `src/scss` Verzeichnisses sieht folgendermaßen
+aus:
+
+    ├── base     // Stile für HTML Elemente
+    ├── config   // Globale Variable
+    ├── modules  // Stile für Module
+    └── tools    // Definierte *mixins* und Funktionen
+
+### Konventionen, Techniken und Tools
+
+Generell wird eine "mobile first" Strategie verfolgt. Als
+Standard-Einheit wird `rem` verwendet, auf deren Grundlage die
+Basis-Einheit definiert ist. Da sich alle Größen auf diese Einheit
+beziehen sollten, wird so das Skalieren des Layouts erleichtert.
+
+Sass wird in diesem Projekt mit der scss-Syntax verwendet. Stilistisch
+ist es in "oldschool BEM-Style" gehalten, Zitat der ursprünglichen
+Entwickler. Sie beziehen sich zudem auf bestimmte Guidelines:
+
+> Hugo Giraudel wrote an awesome piece on everything you need to know
+> about Sass, it's called [Sass Guidelines](http://sass-guidelin.es/)
+> and you should really have a look at it. I agree with this guideline
+> in almost all points, but I try to keep something more simple, and
+> some things more strict, the linter will let you know :)
+
+ʕ̡̢̡ॢ•̫͡ॢ•ʔ̢̡̢
+
+Regeln mit Browser-spezifischen Präfixen (*vendor prefixes*) werden dem
+CSS automatisch durch
+[autoprefixer](https://github.com/postcss/autoprefixer) hinzugefügt. Die
+Liste der zu unterstützenden Browser ist in `package.json` unter
+`browserslist` zu finden.
+
+## Bilddateien
+
+Alle in diesem Projekt verwendeten Bilddateien befinden sich unter
+`src/img/`.
+
+Icons in Form von SVG-Dateien befinden sich im Unterordner
+`src/img/sprites` und werden im Build-Prozess mittels `gulp-svg-sprite`
+zu einem Sprite zusammengefasst. Sie wie folgt in HTML referenziert
+werden:
+
+    <svg class="icon  icon--arrow-left">
+      <use xlink:href="assets/img/sprites.svg#icon--arrow-left"/>
+    </svg>
+
+Stil-Definitionen für Icons sind unter `src/scss/modules/_icons.scss` zu
+finden. Für die Unterstützung von Fragmentbezeichnern (*fragment
+identifier*) in Internet Explorer wird
+[svgxuse](https://github.com/Keyamoon/svgxuse) verwendet.
+
+## Entwicklungshistorie
+
+Ursprünglich wurde dieses Modul als Auftragsarbeit von einer externen
+Firma entwickelt. Unglücklicherweise wurden während der
+Entwicklungsphase keine Code-Audits durchgeführt und die Qualität des
+gelieferten Codes ließ zu wünschen übrig. Teile des Codes mussten
+komplett neu entwickelt werden, da eine Fehlerbehebung nicht anders
+möglich erschien. Dies führte zu einer heterogenen Software-Architektur,
+da bei der Neuentwicklung in erster Linie auf Wartbarkeit und
+Verständlichkeit Wert gelegt wurde und einer angemessenen
+Dokumentierung.
+
+### Gründe für eine teilweise Neu-Implementierung des Moduls
+
+Teile des Codes mussten neu entwickelt werden, da insbesondere die
+originale Implementierung der Visualisierungs-Komponente größtenteils
+unwartbar war. Einfache Fehler zu beheben war nur schwer möglich, da der
+Code mit einer unerkennbaren Logik in Module aufgeteilt war und gängige
+Techniken und von d3 ignoriert wurden.
+
+1.  Ein "Modul" mit dem Namen "redraw" war nur dazu gedacht,
+    SVG-Elemente, die in anderen Code-Fragmenten erstellt wurden, zu
+    aktualisieren. ﴾͡๏̯͡๏﴿\
+    Um Wartbarkeit als Feature zu integrieren, wurde stattdessen die
+    damals übliche Vorgehensweise implementiert ([General update
+    pattern](https://observablehq.com/@d3/general-update-pattern?collection=@d3/d3-selection)).
+    Inzwischen wird diese schon wieder als veraltet bezeichnet, und die
+    Verwendung von
+    [selection.join](https://observablehq.com/@d3/selection-join)
+    empfohlen.
+
+2.  Multiple-Choice-Fragen wurden ursprünglich gegen die Labels der
+    Antworten validiert. Dabei wurde die Javascript Funktion `parseInt`
+    auf alphanumerische Strings angewendet, bzw. unflexible Reguläre
+    Ausdrücke verwendet, die scheitern würden, sobald sich die Struktur
+    der Labels oder die natürliche Sprache änderte. ( ・\_・)ノ⌒●\~\*\
+    Um dieses Problem zu beheben, wurde dem *offline*-Datensatz die
+    notwendigen Daten hinzugefügt, um mathematische Validierung über
+    Funktionen zu ermöglichen. Für jede Frage wird nun eine Referenz zu
+    einer *Validator*-Funktion angegeben, die die Daten, die mit der
+    gegebenen Antwort verknüpft sind, mit dem errechneten Ergebnis
+    vergleicht. Aktuell ist die *online*-Version des Moduls nicht
+    funktionsfähig, da die API noch nicht aktualisiert wurde (Stand Juni
+    2020).
+
+3.  Die aktuelle Implementierung ist nach wie vor sehr unflexibel
+    bezogen auf die Anordnung der verschiedenen Fragetypen. Die Fragen
+    sind in einem zweidimensionalen Array angeordnet, wobei der erste
+    Index den Fragetyp und der zweite die Reihenfolge innerhalb der
+    Gruppe definiert. Verschiedene Fragetypen können daher momentan
+    nicht willkürlich abgewechselt werden. Immerhin wurde der größte
+    Fehler behoben, der beim einfachen Umordnen von Fragen innerhalb
+    einer Gruppe zum "abstürzen" der Web App führte. ٩(̾●̮̮̃̾•̃̾)۶
+
+4.  Die Implementierung des Aufgabentyps zum Manipulieren von Graphen
+    war fehlerhaft. Das Ergebnis wurde nur dann richtigerweise als
+    korrekt erkannt, wenn exakt das Intervall wie in der
+    Aufgabenstellung ausgewählt wurde. Mehrere "falsche Lösungen"
+    konnten gefunden werden. Dadurch konnte der Nutzer mit der folgenden
+    Aufgabe fortführen, auch wenn die Steigung des betrachteten
+    Intervalls außerhalb der Toleranzgrenzen lag. Auch waren
+    Konstellationen möglich, bei denen der betrachtete Teil des Graphen
+    außerhalb des sichtbaren Bereichs lag. Transformationen des
+    Auswahlbereichs wurden durch das *Parsen* von Werten aus dem SVG DOM
+    mit Hilfe von wackeligen Regulären Ausdrücken durchgeführt. Fügte
+    man der Transformation eine Rotation hinzu, funktionierte nichts
+    mehr. Das weckt die Vermutung, dass die Geometrie der Anfasser aus
+    diesem Grund zweimal definiert wurde, siehe 5.\
+    Dieser Teil wurde nahezu komplett neu implementiert, da der
+    ursprüngliche Code zusätzlich zu den beschriebenen Fehlern
+    unverständlich, verworren und dadurch unwartbar war. Es wurden
+    "Techniken" verwendet, bei denen ein Modul in den SVG-DOM
+    hineinschrieb und ein anderes Modul diesen Wert wieder auslas.
+    ⊂(©෴©)つ\
+    Zur Verbesserung der Verständlichkeit wurde eine
+    Controller-zentrische Architektur in Verbindung mit spezifischem
+    Event-Dispatching gewählt. Zur Validierung der Nutzer-gewählten
+    Auswahl werden drei Auswahlbereiche definiert: die `reference range`
+    bezeichnet den ursprünglich ausgewählten Bereich, auf den der
+    benutzerdefinierte Bereich (`selected range`) initialisiert wird.
+    Der Graph wird anhand letzterer transformiert. Die `solution range`
+    definiert die exakte Lösung und wird für die Validierung der
+    Nutzerauswahl verwendet. Sobald der betrachtete Teil des Graphen
+    sichtbar ist und das Validierungskriterium der Frage erfüllt ist,
+    wird die benutzerdefinierte Auswahl als Lösung anerkannt und das
+    Fortfahren zur folgenden Frage wird ermöglicht.
+
+5.  Die Geometrie der Anfasser zum Skalieren der Graphen wurde zweimal
+    definiert, einmal für die horizontale und einmal für die vertikale
+    Version, auch wenn sie sich nur durch eine 90 Grad Rotation
+    unterscheiden (ganz der Wahrheit entsprechend ist das nicht, die
+    inneren Linien waren in einem Fall falsch ausgerichtet). Auch dieser
+    Code war so verworren und verständlich wie ein Stück Hirn in Aspik.
+    In einem Aufruf der Funktion bezog sich die Variable "width"
+    tatsächlich auf die Breite, im anderen Fall auf die Höhe (und
+    umgekehrt bei "height"). Dieser Fakt wurde, vermutlich aus Scham,
+    mit keinem Kommentar erwähnt. ℃ↂ_ↂ\
+    Die Geometrie ist nun nur einmal definiert und wird als SVG Symbol
+    jeweils referenziert und durch Matrizen transformiert. Nebenbei
+    wurde ein zeichnerisches Problem behoben.
+
+Dies sind nur Beispiele für die Probleme, die der ursprüngliche Code
+mitgebracht hat. Die Liste ist sicherlich nicht vollständig und es
+befinden sich höchstwahrscheinlich nach wie vor Eigenheiten und Fehler
+im Code, die man hinterfragen kann bzw. sollte.

BIN
src/fonts/roboto-light.woff


BIN
src/fonts/roboto-light.woff2


BIN
src/fonts/roboto-medium.woff


BIN
src/fonts/roboto-medium.woff2


BIN
src/fonts/roboto-regular.woff


BIN
src/fonts/roboto-regular.woff2


BIN
src/fonts/roboto-thin.woff


BIN
src/fonts/roboto-thin.woff2


BIN
src/fonts/robotomono-light.woff


BIN
src/fonts/robotomono-light.woff2


+ 31 - 0
src/html/index.html

@@ -0,0 +1,31 @@
+<!doctype html>
+<html lang="de" class="no-js  no-touch">
+  <head>
+    <meta charset="utf-8" />
+    <title>Wie werden Finanzstatistiken verzerrt?</title>
+    <meta name="description" content="" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+
+    <link rel="apple-touch-icon" sizes="180x180" href="assets/img/apple-touch-icon.png" />
+    <link rel="icon" type="image/png" href="assets/img/favicon-32x32.png" sizes="32x32" />
+    <link rel="icon" type="image/png" href="assets/img/favicon-16x16.png" sizes="16x16" />
+    <link rel="mask-icon" href="assets/img/safari-pinned-tab.svg" color="#d0021b" />
+    <meta name="theme-color" content="#d0021b" />
+    <link rel="manifest" href="manifest.json" />
+    <link rel="stylesheet" href="{{ stylesheet }}" />
+  </head>
+
+  <body>
+    <noscript>
+      <div class="wrapper">
+        <h2>Diese Seite benötigt JavaScript.</h2>
+        <h3>Bitte ändern Sie die Konfiguration Ihres Browsers und aktivieren Sie JavaScript.</h3>
+        <p>
+          Falls JavaScript in Ihrem Browser deaktiviert wurde, ändern Sie dies bitte über das entsprechende Einstellungs-Menü Ihres Browsers.
+          Nur mit aktiviertem JavaScript kann unsere Seite richtig dargestellt werden und mit allen Funktionen genutzt werden.
+        </p>
+      </div>
+    </noscript>
+    <script src="{{ javascript }}"></script>
+  </body>
+</html>

BIN
src/img/android-chrome-192x192.png


BIN
src/img/android-chrome-512x512.png


BIN
src/img/apple-touch-icon.png


BIN
src/img/favicon-16x16.png


BIN
src/img/favicon-32x32.png


BIN
src/img/mstile-150x150.png


+ 3 - 0
src/img/safari-pinned-tab.svg

@@ -0,0 +1,3 @@
+<svg viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
+  <circle cx="602" cy="612" r="501" fill="#000000"/>
+</svg>

+ 1 - 0
src/img/sprites.svg

@@ -0,0 +1 @@
+<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 13" id="icon--correct" xmlns="http://www.w3.org/2000/svg"><path d="M13.542.465l-8.653 8.88-2.358-2.801a1.407 1.407 0 00-2.038-.141A1.54 1.54 0 00.358 8.52l3.368 4c.262.311.636.496 1.034.511l.053.001c.379 0 .743-.155 1.014-.432l9.744-10a1.543 1.543 0 00.013-2.122 1.407 1.407 0 00-2.042-.013z"/></symbol><symbol viewBox="0 0 16 13" id="icon--incorrect" xmlns="http://www.w3.org/2000/svg"><path d="M14.049.451a1.54 1.54 0 00-2.178 0L8 4.322 4.129.451a1.54 1.54 0 00-2.178 2.178L5.823 6.5l-3.872 3.872a1.54 1.54 0 102.178 2.177L8 8.677l3.871 3.872c.301.301.695.451 1.09.451a1.54 1.54 0 001.088-2.628L10.177 6.5l3.872-3.871a1.54 1.54 0 000-2.178z"/></symbol><symbol viewBox="0 0 7 4" id="icon--triangle" xmlns="http://www.w3.org/2000/svg"><path d="M3.5 4L0 0h7L3.5 4z"/></symbol></svg>

+ 3 - 0
src/img/sprites/correct.svg

@@ -0,0 +1,3 @@
+<svg viewBox="0 0 16 13" xmlns="http://www.w3.org/2000/svg">
+  <path d="M13.542.465l-8.653 8.88-2.358-2.801a1.407 1.407 0 0 0-2.038-.141A1.54 1.54 0 0 0 .358 8.52l3.368 4c.262.311.636.496 1.034.511l.053.001c.379 0 .743-.155 1.014-.432l9.744-10a1.543 1.543 0 0 0 .013-2.122 1.407 1.407 0 0 0-2.042-.013z"/>
+</svg>

+ 3 - 0
src/img/sprites/incorrect.svg

@@ -0,0 +1,3 @@
+<svg viewBox="0 0 16 13" xmlns="http://www.w3.org/2000/svg">
+  <path d="M14.049.451a1.54 1.54 0 0 0-2.178 0L8 4.322 4.129.451a1.54 1.54 0 0 0-2.178 2.178L5.823 6.5l-3.872 3.872a1.54 1.54 0 1 0 2.178 2.177L8 8.677l3.871 3.872c.301.301.695.451 1.09.451a1.54 1.54 0 0 0 1.088-2.628L10.177 6.5l3.872-3.871a1.54 1.54 0 0 0 0-2.178z"/>
+</svg>

+ 15 - 0
src/img/sprites/sprites.yaml

@@ -0,0 +1,15 @@
+icon--arrow-left:
+  title: Back
+  description: A leftward arrow
+
+icon--arrow-right:
+  title: Error
+  description: A rightward arrow
+
+icon--close:
+  title: Close
+  description: A cross to close something
+
+icon--menu:
+  title: Menu
+  description: A burger-menu icon for opening/closing the navigation

+ 1 - 0
src/img/sprites/triangle.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 7 4" xmlns="http://www.w3.org/2000/svg"><path d="M3.5 4L0 0h7L3.5 4z"/></svg>

+ 19 - 0
src/js/components/FinalScreen.jsx

@@ -0,0 +1,19 @@
+import { h, render } from 'preact'; // eslint-disable-line no-unused-vars
+
+// final screen component
+// displays outro text and a link to restart the module
+const FinalScreen = props => ( // eslint-disable-line no-unused-vars
+  <section class="wrapper">
+    <header className="header">
+      <div className="number--square  number--square--huge"><span>{props.number}</span></div>
+      <h1 className="header__title">{props.title}</h1>
+    </header>
+    <main className="intro">{props.outrotext.split('\n').map(item => <span>{item}<br /></span>)}</main>
+    <footer className="footer footer--titlescreen">
+      { !props.isFetching ? <a href="#" title={props.restart} className="button--wide" onClick={() => props.navigate('titlescreen')}>
+        {props.restart}</a> : [] }
+    </footer>
+  </section>
+);
+
+export default FinalScreen;

+ 321 - 0
src/js/components/Index.jsx

@@ -0,0 +1,321 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+// content and config:
+import content from '../content/module.json';
+import offlineQuestions from '../content/offline'; // two-dim. array questions[group][a=1|b=2|c=3]
+import config from '../config';
+// services and utilities:
+import api from '../utilities/api';
+import { fixedDigits } from '../utilities/formatter';
+// screens and items:
+import TitleScreen from './TitleScreen.jsx';
+import FinalScreen from './FinalScreen.jsx';
+import QuestionScreen from './QuestionScreen.jsx';
+
+/**
+ * titlescreen -> questionscreen (*n) -> finalscreen
+ */
+export default class App extends Component {
+
+  // construct and initialize functions
+  constructor (props) {
+
+    super(props);
+
+    this.state = {
+      route: 'titlescreen',        // current screen
+      chartMode: 'question',       // current chart mode: question || score
+      currentGroup: 0,             // current group
+      currentQuestion: 0,          // current question in group
+      currentAnswerIndex: null,    // index in question pair of the currently set answer
+      currentAnswerCorrect: false, // is the currently set question the correct one?
+      isFetching: false,           // currently performing XHR?
+      userId: null,                // current user id for posting
+      token: null,                 // current access token
+      voteRatios: null,            // get votes from other users
+      points: {}                   // get correct and incorrect points
+    };
+
+    // context binding
+    this.navigate = this.navigate.bind(this);
+    this.toNextQuestion = this.toNextQuestion.bind(this);
+    this.setAnswer = this.setAnswer.bind(this);
+    this.setData = this.setData.bind(this);
+    this.drawQuestions = this.drawQuestions.bind(this);
+    this.getUserVotes = this.getUserVotes.bind(this);
+    this.jumpTo = this.jumpTo.bind(this);
+    this.setInitialState = this.setInitialState.bind(this);
+    this.getFeedbackCoordinates = this.getFeedbackCoordinates.bind(this);
+  }
+
+  // initial setup, get accesToken and userId
+  componentDidMount () {
+
+    // register hashchange handler.
+    // Needs more work: reset state, otherwise artefacts will appear.
+    // Without event handler the site has to be reloaded after hash change
+    // onhashchange = this.jumpTo;
+
+    if (this.props.isOffline) {
+      this.setState({ questions: offlineQuestions });
+      this.jumpTo();
+    } else {
+      this.setState({ isFetching: true });
+      api.getToken().then(accessToken => {
+        this.setState({ token: accessToken });
+
+        // create user
+        api.createUser(accessToken)
+          .then(user => {
+            this.setState({
+              userId: user.userId,
+              isFetching: false
+            });
+
+            // draw first question
+            this.drawQuestions();
+          });
+      });
+    }
+  }
+
+  /**
+   * Navigation using window.location.hash, else go to title screen
+   *
+   * @route: The state's route (string). See switch statement in render function
+   * @currentGroup: 1-based index of current 'question group'
+   * @currentQuestion: 1-based index of current question
+   *
+   * example: .../#question_2_1
+   */
+  jumpTo () {
+    if (window.location.hash) {
+      const hashInfo = window.location.hash.replace('#', '').split('_');
+
+      this.setState({
+        route: hashInfo[0],
+        currentGroup: parseInt(hashInfo[1]) - 1 || 0,
+        currentQuestion: parseInt(hashInfo[2]) - 1 || 0
+      });
+    }
+  }
+
+  // navigate to specific route
+  navigate (route) {
+    if (route === 'titlescreen') this.setInitialState();
+    this.setState({ route });
+  }
+
+  // set data
+  setData (isCorrect) {
+    // handle continue button visibility
+    this.setState({ currentAnswerIndex: isCorrect ? 1 : null });
+  }
+
+  // get number of questions
+  getNumberQuestions () {
+    let sum = 0;
+
+    for (let i = 0; i < this.state.questions.length; i += 1) {
+      sum += this.state.questions[i].length;
+    }
+
+    return sum;
+  }
+
+  // get question set from api
+  drawQuestions () {
+    const questionSet = [];
+
+    api.get(config.api.list)
+      .then(json => {
+        [ ...json ].forEach(item => {
+          if (questionSet.length < item.gruppe) {
+            questionSet.push([]);
+          }
+          questionSet[item.gruppe - 1].push(item);
+        });
+        this.setState({ questions: questionSet });
+        this.jumpTo();
+      });
+  }
+
+  // update state depending on chart mode and current question
+  toNextQuestion () {
+
+    // Show "score screen" after each question of first and second question groups in online mode
+    if (this.state.chartMode === 'question' && this.state.currentGroup < 2 && !this.props.isOffline) {
+
+      this.setState({ chartMode: 'score', points: {} });
+    // Else, if exists, display next question in group
+    } else if (this.state.currentQuestion < this.state.questions[this.state.currentGroup].length - 1) {
+      this.setState({
+        route: 'question',
+        chartMode: 'question',
+        currentQuestion: this.state.currentQuestion + 1,
+        currentAnswerIndex: null,
+        points: {},
+        voteRatios: null
+      });
+    // Else, if exists, jump to fist question in next group
+    } else if (this.state.currentGroup < this.state.questions.length - 1) {
+
+      this.setState({
+        route: 'question',
+        chartMode: 'question',
+        currentGroup: this.state.currentGroup + 1,
+        currentQuestion: 0,
+        currentAnswerIndex: null,
+        voteRatios: null
+      });
+    // Else show "final screen"
+    } else {
+      // go to finalscreen
+      this.setState({ route: 'finalscreen' });
+    }
+  }
+
+  // set initial state (for start and restart)
+  setInitialState () {
+    this.setState({
+      route: 'titlescreen',
+      chartMode: 'question',
+      currentGroup: 0,
+      currentQuestion: 0,
+      currentAnswerIndex: null,
+      voteRatios: null
+    });
+  }
+
+  /**
+   * Get coordinates for solution and, if applicable, incorrect answer.
+   *
+   * @param question: Data structure defining the question
+   * @param answer: Answer selected by user
+   * @return: Coordinates of solution, and answer, if incorrect
+   */
+  getFeedbackCoordinates (question, answer) {
+
+    const points = { correct: null, incorrect: null };
+    const index = question.antworten.indexOf(answer);
+
+    let correctAnswer;
+    if (!answer.korrekt) {
+      correctAnswer = question.antworten.find(a => a.korrekt === true);
+    } else {
+      correctAnswer = answer;
+    }
+
+    points.correct = correctAnswer.koordinate;
+
+    // If answer is not correct display both solution, and answer
+    if (!answer.korrekt) {
+      points.incorrect = question.antworten[index].koordinate;
+    }
+
+    return points;
+  }
+
+  /**
+   * Submit answer to API, get percentage of correct answers by other users (multiple choice questions)
+   * By convention (react / preact), the render function is automatically invoked after this.setState(...) has been called
+   *
+   * @param answer: Answer selected by user
+   */
+  setAnswer (answer) {
+    const question = this.state.questions[this.state.currentGroup][this.state.currentQuestion];
+    const cAnswerIndex = question.antworten.indexOf(answer);
+
+    let prepareState = {
+      currentAnswerIndex: cAnswerIndex,
+      currentAnswerCorrect: answer.korrekt
+    };
+
+    if (this.state.currentGroup === 0) {  // only search for result coordinates for first question group.
+      prepareState.points = this.getFeedbackCoordinates(question, answer);
+    }
+
+    if (!this.props.isOffline) {
+      const payload = {
+        userId: this.state.userId,
+        aufgabeId: question.id,
+        antwort: question.antworten[cAnswerIndex].antwort,
+        korrekt: answer.korrekt
+      };
+
+      api.post(config.api.create, payload, this.state.token)
+        .then(() => {
+          this.getUserVotes(question.id).then(
+            json => {
+              prepareState.voteRatios = [ json.auswertung.korrekt, json.auswertung.inkorrekt ];
+              this.setState(prepareState);
+            }
+          );
+        });
+    } else {
+      this.setState(prepareState);
+    }
+  }
+
+  // get user votes by question id
+  getUserVotes (aufgabeId) {
+    return api.get(config.api.proportions, { aufgabeId });
+  }
+
+  // RENDER
+  render () {
+    let outputContent;
+
+    switch (this.state.route) {
+      case 'question': {
+
+        // calculate number of questions from nested questions array
+        const total = this.state.questions.map(e => e.length).reduce((s, e) => (s + e));
+        // calculate current question 'index' (positive number)
+        let index = 0;
+        for (let i = 0; i < this.state.currentGroup; i += 1) {
+          index += this.state.questions[i].length;
+        }
+        index += this.state.currentQuestion + 1;
+
+        const headerState = `${fixedDigits(index, 2)}/${total}`;
+
+        outputContent = <QuestionScreen
+          {...content}
+          route={this.state.route}
+          chartMode={this.state.chartMode}
+          headerState={headerState}
+          questions={this.state.questions[this.state.currentGroup]}
+          currentQuestion={this.state.currentQuestion}
+          currentAnswerIndex={this.state.currentAnswerIndex}
+          currentAnswerCorrect={this.state.currentAnswerCorrect}
+          toNextQuestion={this.toNextQuestion}
+          currentGroup={this.state.currentGroup}
+          getUserVotes={this.getUserVotes}
+          voteRatios={this.state.voteRatios}
+          points={this.state.points}
+          setData={this.setData}
+          setAnswer={this.state.currentAnswerIndex === null ? this.setAnswer : () => {} } />; // eslint-disable-line no-empty-function
+        break; }
+
+      case 'finalscreen':
+        outputContent = <FinalScreen
+          {...content}
+          navigate={this.navigate}
+          isFetching={this.state.isFetching} />;
+        break;
+
+      case 'titlescreen':
+      default:
+        outputContent = <TitleScreen
+          {...content}
+          navigate={ this.navigate }
+          navigateTo='question'
+          isFetching={this.state.isFetching} />;
+        break;
+
+    }
+
+    return outputContent;
+  }
+
+}

+ 82 - 0
src/js/components/QuestionScreen.jsx

@@ -0,0 +1,82 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import GraphItem from './partials/GraphItem.jsx';
+import IntroItem from './partials/IntroItem.jsx';
+import QuestionItem from './partials/QuestionItem.jsx';
+import UserVoteItem from './partials/UserVoteItem.jsx';
+import HeaderLightItem from './partials/HeaderLightItem.jsx';
+
+// module02: Question Screen
+// outputs the wrapper which contains
+// - a) the current question
+// - b) the current user vote result
+// - c) the initial intro item
+export default class QuestionScreen extends Component {
+
+  // get answer and validate it
+  getAnswers (question) {
+    return Object.prototype.hasOwnProperty.call(question, 'antworten') ? question.antworten : [];
+  }
+
+  // RENDER
+  render () {
+    let topItem = ''; // content (will either be questions or statistics)
+    let isCorrectClass = '';
+    let isIncorrectClass = '';
+    let buttonLabel = this.props.next;
+    const question = this.props.questions[this.props.currentQuestion];
+    const answers = this.getAnswers(question);
+
+    // set classes to style lines in svg graph
+    const hasAnswered = this.props.currentAnswerIndex !== null;
+    const correct = answers.find(item => item.korrekt === true);
+
+    if (this.props.currentGroup === 1 && hasAnswered) {
+      isCorrectClass = `correct--${answers.indexOf(correct) + 1}`;
+      isIncorrectClass = !this.props.currentAnswerCorrect
+        ? `incorrect--${this.props.currentAnswerIndex + 1}` : '';
+    } else if (this.props.currentGroup === 2) {
+      isCorrectClass = this.props.currentAnswerCorrect ? 'correct' : 'incorrect';
+      buttonLabel = `${this.props.success} ${this.props.next}`;
+    }
+
+    // d3js properties to pass through
+    const graphItemProps = {
+      dataSet: question,
+      setData: this.props.setData,
+      chartMode: this.props.chartMode,
+      currentQuestion: this.props.currentQuestion,
+      points: this.props.points
+    };
+
+    // render sub component depending on chartMode
+    if (this.props.chartMode === 'question') {
+      topItem = <QuestionItem question={question} answers={answers} currentGroup={this.props.currentGroup}
+        currentQuestion={this.props.currentQuestion} currentAnswerIndex={this.props.currentAnswerIndex} setAnswer={this.props.setAnswer} />;
+    } else if (this.props.voteRatios !== null) {
+      topItem = <UserVoteItem usersRight={this.props.usersRight} voteRatios={this.props.voteRatios} />;
+    } else {
+      topItem = <IntroItem headline={this.props.usersVoteHeadline} />;
+    }
+
+    // output
+    return (
+      <section className="wrapper  wrapper__question">
+        <HeaderLightItem { ...this.props } />
+
+        <main className={`wrapper__main  wrapper--centered  wrapper__main--question  ${isCorrectClass}  ${isIncorrectClass}`}>
+          {topItem}
+          <GraphItem { ...graphItemProps } />
+        </main>
+
+        <footer className="footer footer--chart">
+          {this.props.currentAnswerIndex !== null
+            ? <a href="#" title={this.props.next} className="button--wide" onClick={() => this.props.toNextQuestion('score')}>
+              {buttonLabel}
+            </a>
+            : []}
+        </footer>
+      </section>
+    );
+  }
+
+}

+ 19 - 0
src/js/components/TitleScreen.jsx

@@ -0,0 +1,19 @@
+import { h, render } from 'preact'; // eslint-disable-line no-unused-vars
+
+// Title component
+// displays intro text and a link to start the module
+const TitleScreen = props => ( // eslint-disable-line no-unused-vars
+  <section class="wrapper">
+    <header className="header">
+      <div className="number--square  number--square--huge"><span>{props.number}</span></div>
+      <h1 className="header__title">{props.title}</h1>
+    </header>
+    <main className="intro">{props.introtext.split('\n').map(item => <span>{item}<br /></span>)}</main>
+    <footer className="footer footer--titlescreen">
+      { !props.isFetching ? <a href="#" title="Start" className="button--wide" onClick={() => props.navigate(props.navigateTo)}>
+        {props.start}</a> : [] }
+    </footer>
+  </section>
+);
+
+export default TitleScreen;

+ 73 - 0
src/js/components/partials/GraphItem.jsx

@@ -0,0 +1,73 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import d3Linegraph from '../../d3/main';
+import * as d3 from 'd3';
+
+// displays the current Graph Item depending on mode
+export default class GraphItem extends Component {
+
+  // update data and replace it depending on group
+  updateData () {
+    const options = {
+      entries: this.props.dataSet.data,
+      questionGroup: this.props.dataSet.gruppe,
+      points: this.props.points,
+      // TODO: define in database / "offline content" along with graph data
+      xAxisUnit: 'Zeit (Quartal)',
+      yAxisUnit: 'Gewinn in Millionen Euro'
+    };
+
+    if (this.props.dataSet.gruppe === 1 || this.props.dataSet.gruppe === 3) {
+      options.konkurrent = this.props.dataSet.konkurrent.toUpperCase();
+    }
+
+    if (this.props.dataSet.gruppe === 3) {
+
+      // Initial (selected) range
+      // TODO: change api to reflect this (reference/selected/solution) structure *per graph*
+      // TODO: add to documentation that reference range is hard coded here
+      options.referenceRange = {
+        lowerBound: 'Q1/2015',
+        upperBound: 'Q4/2015'
+      };
+
+      // Solution range
+      options.solutionRange = {
+        lowerBound: this.props.dataSet.untereGrenze,
+        upperBound: this.props.dataSet.obereGrenze,
+        gradient: {
+          min: this.props.dataSet.anstiegUntereGrenze,
+          max: this.props.dataSet.anstiegObereGrenze
+        }
+      };
+      options.setDataMethod = this.props.setData;
+    }
+
+    const draw = d3Linegraph().options(options); // create and initialise d3 base element
+
+    this.container.innerHTML = ''; // clear container
+    d3.select(this.container).call(draw); // call / run d3 base element
+  }
+
+  // LIFECYCLE methods
+  componentDidMount () {
+    this.updateData();
+  }
+
+  shouldComponentUpdate (nextProps) {
+    const isQuestion = nextProps.chartMode === 'question';
+    const isFirstGroup = nextProps.dataSet.gruppe === 1;
+    const isNextQuestion = nextProps.currentQuestion !== this.props.currentQuestion;
+
+    return (isQuestion && isFirstGroup) || isNextQuestion;
+  }
+
+  componentDidUpdate () {
+    this.updateData();
+  }
+
+  // output entry point for d3js module
+  render () {
+    return (<div className="chart  chart--centered" ref={ elem => (this.container = elem) }></div>);
+  }
+
+}

+ 15 - 0
src/js/components/partials/HeaderLightItem.jsx

@@ -0,0 +1,15 @@
+import { h, render } from 'preact'; // eslint-disable-line no-unused-vars
+
+// header light component
+// outputs the header for the different screens excluding Title and Final Screen
+const HeaderLightItem = props => ( // eslint-disable-line no-unused-vars
+  <header className="header  header--light">
+    <div className="header__state">{props.headerState}</div>
+    <div className="number--square">
+      <span>{props.number}</span>
+    </div>
+    <h1 className="header__title--light">{props.title}</h1>
+  </header>
+);
+
+export default HeaderLightItem;

+ 12 - 0
src/js/components/partials/IntroItem.jsx

@@ -0,0 +1,12 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+
+// module02: Intro Item
+// outputs the initial intro screen
+const IntroItem = props => ( // eslint-disable-line no-unused-vars
+  <div className="question  stats__manipula">
+    <div className="stats__title">{props.headline}</div>
+    <div className="stats__options">Lädt Statistik</div>
+  </div>
+);
+
+export default IntroItem;

+ 36 - 0
src/js/components/partials/QuestionItem.jsx

@@ -0,0 +1,36 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+
+// module02: Question Item
+// outputs the current question
+export default class QuestionItem extends Component {
+
+  render () {
+    const keyPrefix = `${this.props.currentGroup}_${this.props.currentQuestion}`;
+    const hasQuestion = this.props.currentGroup < 2;
+
+    return (
+      <div className="question  question__manipula">
+        <div className="question__title">{this.props.question.text}</div>
+
+        { hasQuestion
+          ? <div className="question__options">
+            {this.props.answers.map((antwort, index) => {
+              const markerClass = index === this.props.currentAnswerIndex ? 'question__options__item--marked' : '';
+              const correctClass = antwort.korrekt && this.props.currentAnswerIndex !== null ? 'question__options__item--correct' : '';
+
+              return (
+                <p className={`question__options__item ${markerClass} ${correctClass}`} key={`${keyPrefix}_${index}`}>
+                  <input class="checkbox-input" name="option" id={`option-${index}`} value="1" type="radio" />
+                  <label onClick={ () => this.props.setAnswer(antwort) } class="checkbox-label" for={`option-${index}`}>
+                    {antwort.antwort}
+                  </label>
+                </p>
+              );
+            })}
+          </div>
+          : []}
+
+      </div>
+    );
+  }
+}

+ 16 - 0
src/js/components/partials/UserVoteItem.jsx

@@ -0,0 +1,16 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import VoteItem from './VoteItem.jsx';
+
+// module02: User Vote Item
+// outputs the user vote result
+const UserVoteItem = props => ( // eslint-disable-line no-unused-vars
+  <div className="question  stats__manipula">
+    <div className="stats__options">
+      <div className="stats__stat-item--right">
+        <VoteItem label={props.usersRight} votesFraction={props.voteRatios[0]} activeColor='#7ed321' />
+      </div>
+    </div>
+  </div>
+);
+
+export default UserVoteItem;

+ 55 - 0
src/js/components/partials/VoteItem.jsx

@@ -0,0 +1,55 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import { generateKey } from '../../utilities/randomizer';
+import animatedBar from '../../d3/bar';
+import * as d3 from 'd3';
+
+// vote component
+export default class VoteItem extends Component {
+
+  constructor (props) {
+    super(props);
+    this.key = generateKey('graph');
+  }
+
+  static get defaultProps () {
+    return { width: 845 };
+  }
+
+  setData (props) {
+    if (props.votesFraction !== undefined) { // eslint-disable-line
+      const countup = animatedBar().options({
+        startval: 0,
+        endval: parseFloat(props.votesFraction),
+        duration: 750,
+        round: 100,
+        width: props.width,
+        classname: 'd3-increment',
+        colors: [ '#E3E3E3', props.activeColor ],
+        tag: 'p'
+      });
+
+      this.container.innerHTML = '';
+      d3.select(this.container).call(countup);
+    }
+  }
+
+  componentDidMount () {
+    this.setData(this.props);
+  }
+
+  componentWillReceiveProps (nextProps) {
+    if (JSON.stringify(nextProps) !== JSON.stringify(this.props)) {
+      this.setData(nextProps);
+    }
+  }
+
+  render () {
+    return (
+      <div className="vote__item" key={this.key}>
+        <div className="vote__item__label">{this.props.label}</div>
+        <div className="vote__item__graph" ref={ elem => (this.container = elem) }></div>
+      </div>
+    );
+  }
+
+}

+ 105 - 0
src/js/config.js

@@ -0,0 +1,105 @@
+export default {
+
+  // >>> general configuration >>>
+  api: {
+    create: 'M2_AufgabenUserData/Create',
+    list: 'M2_Aufgaben/Liste',
+    proportions: 'M2_AufgabenUserData/ProportionsByAufgabeId',
+    url: 'https://www.adaptivetoolbox.net/risikoatlas/api/',
+    user: 'jens.becker%40kf-interactive.com',
+    pwd: 'P4ssw0rd%21'
+  },
+  fonts: {
+    default: {
+      family: 'Roboto',
+      weight: 300,
+      style: 'normal'
+    },
+    thin: {
+      family: 'Roboto',
+      weight: 200,
+      style: 'normal'
+    },
+    bold: {
+      family: 'Roboto',
+      weight: 600,
+      style: 'normal'
+    },
+    regular: {
+      family: 'Roboto',
+      weight: 400,
+      style: 'normal'
+    },
+    mono: {
+      family: 'Roboto Mono',
+      weight: 300,
+      style: 'normal'
+    }
+  },
+  // <<< general configuration <<<
+
+  // >>> d3 configuration >>>
+  colors: {
+    axis: '#9b9b9b',
+    bright: '#fff',
+    correct: '#7ed321',
+    error: '#d0021b',
+    graph: '#616161',
+    highlight: '#d0021b',
+    light: '#d8d8d8',
+    units: '#616161'
+  },
+
+  duration: 250,
+  height: 400,
+  margin: {
+    top: 20,
+    right: 10,
+    bottom: 150,
+    left: 60
+  },
+
+  module: 2,
+
+  box: {
+    width: 50,
+    height: 24
+  },
+
+  questionGroup: 1,
+  radius: 6,
+  setDataMethod: () => {}, // eslint-disable-line no-empty-function
+  width: 850,
+
+  y: {
+    min: 0,
+    max: 100
+  },
+
+  // handle configuration
+  handles: {
+    connectorHeight: 5,
+    horizontal: { transform: { a: 1, b: 0, c: 0, d: 1 } },
+    vertical: { transform: { a: 0, b: 1, c: -1, d: 0 } },
+    scale: {
+      svgid: 'scale-handle',
+      maxDim: 35,
+      minDim: 20
+    },
+    size: {
+      svgid: 'size-handle',
+      radius: {
+        large: 9,
+        small: 4
+      }
+    },
+    symbolId: {
+      scale: 'scale-handle',
+      size: 'size-handle'
+    },
+    strokeWidth: 2,
+    maxDim: 35, // long side
+    minDim: 20 // short side
+  }
+  // <<< d3 configuration <<<
+};

+ 106 - 0
src/js/content/Gruppe-1_item-a.json

@@ -0,0 +1,106 @@
+{
+  "data": [
+    {
+      "id": 1,
+      "konkurrent": "Apfelreich",
+      "koordinaten": [
+        { "x": "Q1/2013", "y": 21.29 },
+        { "x": "Q2/2013", "y": 3.08 },
+        { "x": "Q3/2013", "y": 22.29 },
+        { "x": "Q4/2013", "y": 13.24 },
+        { "x": "Q1/2014", "y": 1.4 },
+        { "x": "Q2/2014", "y": 3.05 },
+        { "x": "Q3/2014", "y": 17.14 },
+        { "x": "Q4/2014", "y": 10.86 },
+        { "x": "Q1/2015", "y": 19.13 },
+        { "x": "Q2/2015", "y": 19.92 },
+        { "x": "Q3/2015", "y": 1.68 },
+        { "x": "Q4/2015", "y": 0.99 },
+        { "x": "Q1/2016", "y": 5.71 },
+        { "x": "Q2/2016", "y": 5.8 },
+        { "x": "Q3/2016", "y": 15.81 },
+        { "x": "Q4/2016", "y": 2.76 },
+        { "x": "Q1/2017", "y": 11.45 },
+        { "x": "Q2/2017", "y": 2.9 },
+        { "x": "Q3/2017", "y": 7.82 },
+        { "x": "Q4/2017", "y": 7.23 }
+      ]
+    },
+    {
+      "id": 2,
+      "konkurrent": "Birnenbox",
+      "koordinaten": [
+        { "x": "Q1/2013", "y": 18.19 },
+        { "x": "Q2/2013", "y": 19.45 },
+        { "x": "Q3/2013", "y": 15.13 },
+        { "x": "Q4/2013", "y": 19.35 },
+        { "x": "Q1/2014", "y": 19.6 },
+        { "x": "Q2/2014", "y": 19.47 },
+        { "x": "Q3/2014", "y": 22.71 },
+        { "x": "Q4/2014", "y": 22.11 },
+        { "x": "Q1/2015", "y": 24.29 },
+        { "x": "Q2/2015", "y": 20.18 },
+        { "x": "Q3/2015", "y": 20.22 },
+        { "x": "Q4/2015", "y": 21.03 },
+        { "x": "Q1/2016", "y": 19.6 },
+        { "x": "Q2/2016", "y": 20.83 },
+        { "x": "Q3/2016", "y": 24.41 },
+        { "x": "Q4/2016", "y": 24.53 },
+        { "x": "Q1/2017", "y": 15.53 },
+        { "x": "Q2/2017", "y": 23.85 },
+        { "x": "Q3/2017", "y": 15.13 },
+        { "x": "Q4/2017", "y": 17.5 }
+      ]
+    },
+    {
+      "id": 3,
+      "konkurrent": "Citroniger",
+      "koordinaten": [
+        { "x": "Q1/2013", "y": 1.64 },
+        { "x": "Q2/2013", "y": 0.09 },
+        { "x": "Q3/2013", "y": 3.16 },
+        { "x": "Q4/2013", "y": 4.35 },
+        { "x": "Q1/2014", "y": 6.57 },
+        { "x": "Q2/2014", "y": 8.22 },
+        { "x": "Q3/2014", "y": 12.37 },
+        { "x": "Q4/2014", "y": 4.84 },
+        { "x": "Q1/2015", "y": 7.56 },
+        { "x": "Q2/2015", "y": 15.77 },
+        { "x": "Q3/2015", "y": 19.86 },
+        { "x": "Q4/2015", "y": 5.82 },
+        { "x": "Q1/2016", "y": 10.71 },
+        { "x": "Q2/2016", "y": 9.92 },
+        { "x": "Q3/2016", "y": 22.93 },
+        { "x": "Q4/2016", "y": 18.93 },
+        { "x": "Q1/2017", "y": 12.63 },
+        { "x": "Q2/2017", "y": 0.81 },
+        { "x": "Q3/2017", "y": 33.07 },
+        { "x": "Q4/2017", "y": 28.0 }
+      ]
+    }
+  ],
+  "id": 1,
+  "gruppe": 1,
+  "item": "a",
+  "name": "Frage 1a",
+  "text": "Welchen Gewinn erzielte Citroniger im 4. Quartal 2016?",
+  "antworten": [
+    {
+      "antwort": "15 Millionen",
+      "korrekt": false,
+      "coordinates": {"x": "Q4/2016", "y": 15.00}
+    },
+    {
+      "antwort": "19 Millionen",
+      "korrekt": true,
+      "coordinates": {"x": "Q4/2016", "y": 18.93}
+    },
+    {
+      "antwort": "24 Millionen",
+      "korrekt": false,
+      "coordinates": {"x": "Q4/2016", "y": 24.00}
+    }
+  ],
+  "konkurrent": "Citroniger",
+  "quartal": "Q4/2016"
+}

+ 286 - 0
src/js/content/Gruppe-1_item-b.json

@@ -0,0 +1,286 @@
+{
+  "data": [
+    {
+      "id": 4,
+      "konkurrent": "Apfelreich",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": -8.02
+        },
+        {
+          "x": "Q2/2013",
+          "y": -0.39
+        },
+        {
+          "x": "Q3/2013",
+          "y": 8.05
+        },
+        {
+          "x": "Q4/2013",
+          "y": -3.59
+        },
+        {
+          "x": "Q1/2014",
+          "y": 4.69
+        },
+        {
+          "x": "Q2/2014",
+          "y": 2.12
+        },
+        {
+          "x": "Q3/2014",
+          "y": 9.88
+        },
+        {
+          "x": "Q4/2014",
+          "y": 1.8
+        },
+        {
+          "x": "Q1/2015",
+          "y": -16.19
+        },
+        {
+          "x": "Q2/2015",
+          "y": -4.59
+        },
+        {
+          "x": "Q3/2015",
+          "y": -7.93
+        },
+        {
+          "x": "Q4/2015",
+          "y": -9.18
+        },
+        {
+          "x": "Q1/2016",
+          "y": -9.37
+        },
+        {
+          "x": "Q2/2016",
+          "y": -2.48
+        },
+        {
+          "x": "Q3/2016",
+          "y": 3.28
+        },
+        {
+          "x": "Q4/2016",
+          "y": -2.87
+        },
+        {
+          "x": "Q1/2017",
+          "y": -5.39
+        },
+        {
+          "x": "Q2/2017",
+          "y": -17.07
+        },
+        {
+          "x": "Q3/2017",
+          "y": -19.39
+        },
+        {
+          "x": "Q4/2017",
+          "y": -5.1
+        }
+      ]
+    },
+    {
+      "id": 5,
+      "konkurrent": "Birnenbox",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": -1.0
+        },
+        {
+          "x": "Q2/2013",
+          "y": 4.99
+        },
+        {
+          "x": "Q3/2013",
+          "y": -9.02
+        },
+        {
+          "x": "Q4/2013",
+          "y": 1.25
+        },
+        {
+          "x": "Q1/2014",
+          "y": -10.1
+        },
+        {
+          "x": "Q2/2014",
+          "y": 4.57
+        },
+        {
+          "x": "Q3/2014",
+          "y": 8.41
+        },
+        {
+          "x": "Q4/2014",
+          "y": 2.66
+        },
+        {
+          "x": "Q1/2015",
+          "y": -10.58
+        },
+        {
+          "x": "Q2/2015",
+          "y": -2.56
+        },
+        {
+          "x": "Q3/2015",
+          "y": 7.14
+        },
+        {
+          "x": "Q4/2015",
+          "y": 7.24
+        },
+        {
+          "x": "Q1/2016",
+          "y": -13.83
+        },
+        {
+          "x": "Q2/2016",
+          "y": -18.69
+        },
+        {
+          "x": "Q3/2016",
+          "y": 3.78
+        },
+        {
+          "x": "Q4/2016",
+          "y": 1.9
+        },
+        {
+          "x": "Q1/2017",
+          "y": -2.1
+        },
+        {
+          "x": "Q2/2017",
+          "y": -0.98
+        },
+        {
+          "x": "Q3/2017",
+          "y": -7.56
+        },
+        {
+          "x": "Q4/2017",
+          "y": -0.3
+        }
+      ]
+    },
+    {
+      "id": 6,
+      "konkurrent": "Citroniger",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": -3.33
+        },
+        {
+          "x": "Q2/2013",
+          "y": -12.13
+        },
+        {
+          "x": "Q3/2013",
+          "y": -3.37
+        },
+        {
+          "x": "Q4/2013",
+          "y": -16.11
+        },
+        {
+          "x": "Q1/2014",
+          "y": -8.99
+        },
+        {
+          "x": "Q2/2014",
+          "y": -9.79
+        },
+        {
+          "x": "Q3/2014",
+          "y": -2.25
+        },
+        {
+          "x": "Q4/2014",
+          "y": -18.27
+        },
+        {
+          "x": "Q1/2015",
+          "y": 7.24
+        },
+        {
+          "x": "Q2/2015",
+          "y": -16.03
+        },
+        {
+          "x": "Q3/2015",
+          "y": 0.64
+        },
+        {
+          "x": "Q4/2015",
+          "y": -3.0
+        },
+        {
+          "x": "Q1/2016",
+          "y": 0.95
+        },
+        {
+          "x": "Q2/2016",
+          "y": 2.06
+        },
+        {
+          "x": "Q3/2016",
+          "y": 3.05
+        },
+        {
+          "x": "Q4/2016",
+          "y": -5.07
+        },
+        {
+          "x": "Q1/2017",
+          "y": -2.2
+        },
+        {
+          "x": "Q2/2017",
+          "y": -2.85
+        },
+        {
+          "x": "Q3/2017",
+          "y": -18.84
+        },
+        {
+          "x": "Q4/2017",
+          "y": 5.98
+        }
+      ]
+    }
+  ],
+  "antworten":[
+    {
+      "antwort": "3. Quartal 2015",
+      "korrekt": false,
+      "coordinates": {"x": "Q3/2015", "y": 0.64}
+    },
+    {
+      "antwort": "4. Quartal 2015",
+      "korrekt": false,
+      "coordinates": {"x": "Q4/2015", "y": -3.0}
+    },
+    {
+      "antwort": "1. Quartal 2015",
+      "korrekt": true,
+      "coordinates": {"x": "Q1/2015", "y": 7.24}
+    }
+  ],
+  "gruppe": 1,
+  "id": 2,
+  "item": "b",
+  "name": "Frage 1b",
+  "text": "Wann erzielte Citroniger erstmals Gewinn?",
+  "konkurrent": "Citroniger",
+  "quartal": "Q1/2015"
+}

+ 286 - 0
src/js/content/Gruppe-1_item-c.json

@@ -0,0 +1,286 @@
+{
+  "data": [
+    {
+      "id": 7,
+      "konkurrent": "Apfelreich",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 10.82
+        },
+        {
+          "x": "Q2/2013",
+          "y": 28.88
+        },
+        {
+          "x": "Q3/2013",
+          "y": 12.71
+        },
+        {
+          "x": "Q4/2013",
+          "y": 12.48
+        },
+        {
+          "x": "Q1/2014",
+          "y": 21.77
+        },
+        {
+          "x": "Q2/2014",
+          "y": 14.74
+        },
+        {
+          "x": "Q3/2014",
+          "y": 20.32
+        },
+        {
+          "x": "Q4/2014",
+          "y": 6.39
+        },
+        {
+          "x": "Q1/2015",
+          "y": 3.6
+        },
+        {
+          "x": "Q2/2015",
+          "y": 2.03
+        },
+        {
+          "x": "Q3/2015",
+          "y": 18.85
+        },
+        {
+          "x": "Q4/2015",
+          "y": 17.26
+        },
+        {
+          "x": "Q1/2016",
+          "y": 17.68
+        },
+        {
+          "x": "Q2/2016",
+          "y": 8.39
+        },
+        {
+          "x": "Q3/2016",
+          "y": 11.78
+        },
+        {
+          "x": "Q4/2016",
+          "y": 12.5
+        },
+        {
+          "x": "Q1/2017",
+          "y": 3.86
+        },
+        {
+          "x": "Q2/2017",
+          "y": 7.0
+        },
+        {
+          "x": "Q3/2017",
+          "y": 9.61
+        },
+        {
+          "x": "Q4/2017",
+          "y": 9.5
+        }
+      ]
+    },
+    {
+      "id": 8,
+      "konkurrent": "Birnenbox",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 20.51
+        },
+        {
+          "x": "Q2/2013",
+          "y": 24.25
+        },
+        {
+          "x": "Q3/2013",
+          "y": 24.14
+        },
+        {
+          "x": "Q4/2013",
+          "y": 23.03
+        },
+        {
+          "x": "Q1/2014",
+          "y": 18.4
+        },
+        {
+          "x": "Q2/2014",
+          "y": 15.97
+        },
+        {
+          "x": "Q3/2014",
+          "y": 19.69
+        },
+        {
+          "x": "Q4/2014",
+          "y": 17.13
+        },
+        {
+          "x": "Q1/2015",
+          "y": 18.45
+        },
+        {
+          "x": "Q2/2015",
+          "y": 15.63
+        },
+        {
+          "x": "Q3/2015",
+          "y": 21.32
+        },
+        {
+          "x": "Q4/2015",
+          "y": 19.01
+        },
+        {
+          "x": "Q1/2016",
+          "y": 24.1
+        },
+        {
+          "x": "Q2/2016",
+          "y": 24.82
+        },
+        {
+          "x": "Q3/2016",
+          "y": 16.87
+        },
+        {
+          "x": "Q4/2016",
+          "y": 21.82
+        },
+        {
+          "x": "Q1/2017",
+          "y": 22.39
+        },
+        {
+          "x": "Q2/2017",
+          "y": 19.66
+        },
+        {
+          "x": "Q3/2017",
+          "y": 23.31
+        },
+        {
+          "x": "Q4/2017",
+          "y": 15.27
+        }
+      ]
+    },
+    {
+      "id": 9,
+      "konkurrent": "Citroniger",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 1.05
+        },
+        {
+          "x": "Q2/2013",
+          "y": 0.15
+        },
+        {
+          "x": "Q3/2013",
+          "y": 0.41
+        },
+        {
+          "x": "Q4/2013",
+          "y": 4.25
+        },
+        {
+          "x": "Q1/2014",
+          "y": 8.96
+        },
+        {
+          "x": "Q2/2014",
+          "y": 10.31
+        },
+        {
+          "x": "Q3/2014",
+          "y": 3.69
+        },
+        {
+          "x": "Q4/2014",
+          "y": 11.17
+        },
+        {
+          "x": "Q1/2015",
+          "y": 13.74
+        },
+        {
+          "x": "Q2/2015",
+          "y": 7.23
+        },
+        {
+          "x": "Q3/2015",
+          "y": 2.74
+        },
+        {
+          "x": "Q4/2015",
+          "y": 8.79
+        },
+        {
+          "x": "Q1/2016",
+          "y": 5.79
+        },
+        {
+          "x": "Q2/2016",
+          "y": 1.48
+        },
+        {
+          "x": "Q3/2016",
+          "y": 28.16
+        },
+        {
+          "x": "Q4/2016",
+          "y": 2.66
+        },
+        {
+          "x": "Q1/2017",
+          "y": 26.77
+        },
+        {
+          "x": "Q2/2017",
+          "y": 2.61
+        },
+        {
+          "x": "Q3/2017",
+          "y": 34.85
+        },
+        {
+          "x": "Q4/2017",
+          "y": 3.25
+        }
+      ]
+    }
+  ],
+  "antworten":[
+    {
+      "antwort": "2. Quartal 2016",
+      "korrekt": false,
+      "coordinates": {"x": "Q2/2016", "y": 1.48}
+    },
+    {
+      "antwort": "2. Quartal 2017",
+      "korrekt": false,
+      "coordinates": {"x": "Q2/2017", "y": 2.61}
+    },
+    {
+      "antwort": "4. Quartal 2017",
+      "korrekt": true,
+      "coordinates": {"x": "Q4/2017", "y": 3.25}
+    }
+  ],
+  "gruppe": 1,
+  "id": 3,
+  "item": "c",
+  "name": "Frage 1c",
+  "text": "Wann hatte Citroniger die größte Abnahme gegenüber dem Vorquartal?",
+  "konkurrent": "Citroniger",
+  "quartal": "Q4/2017"
+}

+ 281 - 0
src/js/content/Gruppe-2_item-a.json

@@ -0,0 +1,281 @@
+{
+  "data": [
+    {
+      "id": 10,
+      "konkurrent": "Apfelreich",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 28.97
+        },
+        {
+          "x": "Q2/2013",
+          "y": 17.61
+        },
+        {
+          "x": "Q3/2013",
+          "y": 24.31
+        },
+        {
+          "x": "Q4/2013",
+          "y": 0.2
+        },
+        {
+          "x": "Q1/2014",
+          "y": 19.62
+        },
+        {
+          "x": "Q2/2014",
+          "y": 2.13
+        },
+        {
+          "x": "Q3/2014",
+          "y": 22.51
+        },
+        {
+          "x": "Q4/2014",
+          "y": 8.92
+        },
+        {
+          "x": "Q1/2015",
+          "y": 20.71
+        },
+        {
+          "x": "Q2/2015",
+          "y": 9.76
+        },
+        {
+          "x": "Q3/2015",
+          "y": 0.82
+        },
+        {
+          "x": "Q4/2015",
+          "y": 14.38
+        },
+        {
+          "x": "Q1/2016",
+          "y": 17.26
+        },
+        {
+          "x": "Q2/2016",
+          "y": 11.48
+        },
+        {
+          "x": "Q3/2016",
+          "y": 2.56
+        },
+        {
+          "x": "Q4/2016",
+          "y": 13.26
+        },
+        {
+          "x": "Q1/2017",
+          "y": 7.49
+        },
+        {
+          "x": "Q2/2017",
+          "y": 3.79
+        },
+        {
+          "x": "Q3/2017",
+          "y": 0.27
+        },
+        {
+          "x": "Q4/2017",
+          "y": 2.79
+        }
+      ]
+    },
+    {
+      "id": 11,
+      "konkurrent": "Birnenbox",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 16.36
+        },
+        {
+          "x": "Q2/2013",
+          "y": 17.57
+        },
+        {
+          "x": "Q3/2013",
+          "y": 23.71
+        },
+        {
+          "x": "Q4/2013",
+          "y": 19.38
+        },
+        {
+          "x": "Q1/2014",
+          "y": 24.48
+        },
+        {
+          "x": "Q2/2014",
+          "y": 20.05
+        },
+        {
+          "x": "Q3/2014",
+          "y": 19.04
+        },
+        {
+          "x": "Q4/2014",
+          "y": 19.76
+        },
+        {
+          "x": "Q1/2015",
+          "y": 16.14
+        },
+        {
+          "x": "Q2/2015",
+          "y": 21.61
+        },
+        {
+          "x": "Q3/2015",
+          "y": 17.47
+        },
+        {
+          "x": "Q4/2015",
+          "y": 15.04
+        },
+        {
+          "x": "Q1/2016",
+          "y": 19.75
+        },
+        {
+          "x": "Q2/2016",
+          "y": 18.53
+        },
+        {
+          "x": "Q3/2016",
+          "y": 21.96
+        },
+        {
+          "x": "Q4/2016",
+          "y": 24.23
+        },
+        {
+          "x": "Q1/2017",
+          "y": 20.34
+        },
+        {
+          "x": "Q2/2017",
+          "y": 18.3
+        },
+        {
+          "x": "Q3/2017",
+          "y": 17.06
+        },
+        {
+          "x": "Q4/2017",
+          "y": 21.8
+        }
+      ]
+    },
+    {
+      "id": 12,
+      "konkurrent": "Citroniger",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 0.33
+        },
+        {
+          "x": "Q2/2013",
+          "y": 2.38
+        },
+        {
+          "x": "Q3/2013",
+          "y": 3.3
+        },
+        {
+          "x": "Q4/2013",
+          "y": 5.19
+        },
+        {
+          "x": "Q1/2014",
+          "y": 1.74
+        },
+        {
+          "x": "Q2/2014",
+          "y": 7.66
+        },
+        {
+          "x": "Q3/2014",
+          "y": 2.87
+        },
+        {
+          "x": "Q4/2014",
+          "y": 12.93
+        },
+        {
+          "x": "Q1/2015",
+          "y": 7.67
+        },
+        {
+          "x": "Q2/2015",
+          "y": 16.7
+        },
+        {
+          "x": "Q3/2015",
+          "y": 12.13
+        },
+        {
+          "x": "Q4/2015",
+          "y": 20.09
+        },
+        {
+          "x": "Q1/2016",
+          "y": 1.51
+        },
+        {
+          "x": "Q2/2016",
+          "y": 9.44
+        },
+        {
+          "x": "Q3/2016",
+          "y": 23.98
+        },
+        {
+          "x": "Q4/2016",
+          "y": 5.2
+        },
+        {
+          "x": "Q1/2017",
+          "y": 25.4
+        },
+        {
+          "x": "Q2/2017",
+          "y": 33.34
+        },
+        {
+          "x": "Q3/2017",
+          "y": 19.44
+        },
+        {
+          "x": "Q4/2017",
+          "y": 3.83
+        }
+      ]
+    }
+  ],
+  "antworten":[
+    {
+      "antwort": "Apfelreich",
+      "korrekt": true
+    },
+    {
+      "antwort": "Birnenbox",
+      "korrekt": false
+    },
+    {
+      "antwort": "Citroniger",
+      "korrekt": false
+    }
+  ],
+  "gruppe": 2,
+  "id": 4,
+  "item": "a",
+  "name": "Frage 2a",
+  "text": "Welches der drei Unternehmen hat die negativste Entwicklung?"
+}

+ 286 - 0
src/js/content/Gruppe-2_item-b.json

@@ -0,0 +1,286 @@
+{
+  "anstiegObereGrenze": null,
+  "anstiegUntereGrenze": null,
+  "antwortFalsch1": "A",
+  "antwortFalsch2": "C",
+  "antwortKorrekt": "B",
+  "data": [
+    {
+      "id": 13,
+      "konkurrent": "Apfelreich",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 13.85
+        },
+        {
+          "x": "Q2/2013",
+          "y": 14.97
+        },
+        {
+          "x": "Q3/2013",
+          "y": 10.77
+        },
+        {
+          "x": "Q4/2013",
+          "y": 26.73
+        },
+        {
+          "x": "Q1/2014",
+          "y": 13.79
+        },
+        {
+          "x": "Q2/2014",
+          "y": 6.56
+        },
+        {
+          "x": "Q3/2014",
+          "y": 22.36
+        },
+        {
+          "x": "Q4/2014",
+          "y": 22.08
+        },
+        {
+          "x": "Q1/2015",
+          "y": 18.8
+        },
+        {
+          "x": "Q2/2015",
+          "y": 1.67
+        },
+        {
+          "x": "Q3/2015",
+          "y": 1.89
+        },
+        {
+          "x": "Q4/2015",
+          "y": 2.37
+        },
+        {
+          "x": "Q1/2016",
+          "y": 11.59
+        },
+        {
+          "x": "Q2/2016",
+          "y": 5.03
+        },
+        {
+          "x": "Q3/2016",
+          "y": 6.13
+        },
+        {
+          "x": "Q4/2016",
+          "y": 13.47
+        },
+        {
+          "x": "Q1/2017",
+          "y": 11.77
+        },
+        {
+          "x": "Q2/2017",
+          "y": 12.23
+        },
+        {
+          "x": "Q3/2017",
+          "y": 11.9
+        },
+        {
+          "x": "Q4/2017",
+          "y": 0.53
+        }
+      ]
+    },
+    {
+      "id": 14,
+      "konkurrent": "Birnenbox",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 17.44
+        },
+        {
+          "x": "Q2/2013",
+          "y": 15.79
+        },
+        {
+          "x": "Q3/2013",
+          "y": 18.86
+        },
+        {
+          "x": "Q4/2013",
+          "y": 20.82
+        },
+        {
+          "x": "Q1/2014",
+          "y": 22.24
+        },
+        {
+          "x": "Q2/2014",
+          "y": 15.6
+        },
+        {
+          "x": "Q3/2014",
+          "y": 18.73
+        },
+        {
+          "x": "Q4/2014",
+          "y": 18.12
+        },
+        {
+          "x": "Q1/2015",
+          "y": 15.2
+        },
+        {
+          "x": "Q2/2015",
+          "y": 20.23
+        },
+        {
+          "x": "Q3/2015",
+          "y": 15.53
+        },
+        {
+          "x": "Q4/2015",
+          "y": 15.82
+        },
+        {
+          "x": "Q1/2016",
+          "y": 21.88
+        },
+        {
+          "x": "Q2/2016",
+          "y": 23.83
+        },
+        {
+          "x": "Q3/2016",
+          "y": 17.35
+        },
+        {
+          "x": "Q4/2016",
+          "y": 23.57
+        },
+        {
+          "x": "Q1/2017",
+          "y": 20.76
+        },
+        {
+          "x": "Q2/2017",
+          "y": 18.77
+        },
+        {
+          "x": "Q3/2017",
+          "y": 21.69
+        },
+        {
+          "x": "Q4/2017",
+          "y": 18.09
+        }
+      ]
+    },
+    {
+      "id": 15,
+      "konkurrent": "Citroniger",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 0.63
+        },
+        {
+          "x": "Q2/2013",
+          "y": 1.7
+        },
+        {
+          "x": "Q3/2013",
+          "y": 0.72
+        },
+        {
+          "x": "Q4/2013",
+          "y": 1.38
+        },
+        {
+          "x": "Q1/2014",
+          "y": 8.1
+        },
+        {
+          "x": "Q2/2014",
+          "y": 0.38
+        },
+        {
+          "x": "Q3/2014",
+          "y": 12.54
+        },
+        {
+          "x": "Q4/2014",
+          "y": 4.57
+        },
+        {
+          "x": "Q1/2015",
+          "y": 0.64
+        },
+        {
+          "x": "Q2/2015",
+          "y": 7.12
+        },
+        {
+          "x": "Q3/2015",
+          "y": 13.18
+        },
+        {
+          "x": "Q4/2015",
+          "y": 2.37
+        },
+        {
+          "x": "Q1/2016",
+          "y": 9.54
+        },
+        {
+          "x": "Q2/2016",
+          "y": 12.45
+        },
+        {
+          "x": "Q3/2016",
+          "y": 19.2
+        },
+        {
+          "x": "Q4/2016",
+          "y": 25.85
+        },
+        {
+          "x": "Q1/2017",
+          "y": 6.37
+        },
+        {
+          "x": "Q2/2017",
+          "y": 23.83
+        },
+        {
+          "x": "Q3/2017",
+          "y": 4.47
+        },
+        {
+          "x": "Q4/2017",
+          "y": 17.55
+        }
+      ]
+    }
+  ],
+  "antworten":[
+    {
+      "antwort": "Apfelreich",
+      "korrekt": true
+    },
+    {
+      "antwort": "Birnenbox",
+      "korrekt": false
+    },
+    {
+      "antwort": "Citroniger",
+      "korrekt": false
+    }
+  ],
+  "gruppe": 2,
+  "id": 5,
+  "item": "b",
+  "name": "Frage 2b",
+  "text": "Welches der drei Unternehmen hat im Mittel den höchsten Gewinn?"
+}

+ 281 - 0
src/js/content/Gruppe-2_item-c.json

@@ -0,0 +1,281 @@
+{
+  "data": [
+    {
+      "id": 16,
+      "konkurrent": "Apfelreich",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 14.79
+        },
+        {
+          "x": "Q2/2013",
+          "y": 22.65
+        },
+        {
+          "x": "Q3/2013",
+          "y": 6.46
+        },
+        {
+          "x": "Q4/2013",
+          "y": 15.62
+        },
+        {
+          "x": "Q1/2014",
+          "y": 21.49
+        },
+        {
+          "x": "Q2/2014",
+          "y": 10.01
+        },
+        {
+          "x": "Q3/2014",
+          "y": 7.5
+        },
+        {
+          "x": "Q4/2014",
+          "y": 0.72
+        },
+        {
+          "x": "Q1/2015",
+          "y": 19.87
+        },
+        {
+          "x": "Q2/2015",
+          "y": 13.23
+        },
+        {
+          "x": "Q3/2015",
+          "y": 1.74
+        },
+        {
+          "x": "Q4/2015",
+          "y": 5.13
+        },
+        {
+          "x": "Q1/2016",
+          "y": 3.44
+        },
+        {
+          "x": "Q2/2016",
+          "y": 16.49
+        },
+        {
+          "x": "Q3/2016",
+          "y": 10.67
+        },
+        {
+          "x": "Q4/2016",
+          "y": 10.48
+        },
+        {
+          "x": "Q1/2017",
+          "y": 1.19
+        },
+        {
+          "x": "Q2/2017",
+          "y": 7.58
+        },
+        {
+          "x": "Q3/2017",
+          "y": 8.89
+        },
+        {
+          "x": "Q4/2017",
+          "y": 0.21
+        }
+      ]
+    },
+    {
+      "id": 17,
+      "konkurrent": "Birnenbox",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 24.17
+        },
+        {
+          "x": "Q2/2013",
+          "y": 23.14
+        },
+        {
+          "x": "Q3/2013",
+          "y": 24.34
+        },
+        {
+          "x": "Q4/2013",
+          "y": 15.43
+        },
+        {
+          "x": "Q1/2014",
+          "y": 15.17
+        },
+        {
+          "x": "Q2/2014",
+          "y": 22.38
+        },
+        {
+          "x": "Q3/2014",
+          "y": 20.15
+        },
+        {
+          "x": "Q4/2014",
+          "y": 15.16
+        },
+        {
+          "x": "Q1/2015",
+          "y": 17.15
+        },
+        {
+          "x": "Q2/2015",
+          "y": 23.74
+        },
+        {
+          "x": "Q3/2015",
+          "y": 15.08
+        },
+        {
+          "x": "Q4/2015",
+          "y": 24.62
+        },
+        {
+          "x": "Q1/2016",
+          "y": 22.64
+        },
+        {
+          "x": "Q2/2016",
+          "y": 18.67
+        },
+        {
+          "x": "Q3/2016",
+          "y": 24.63
+        },
+        {
+          "x": "Q4/2016",
+          "y": 23.59
+        },
+        {
+          "x": "Q1/2017",
+          "y": 17.93
+        },
+        {
+          "x": "Q2/2017",
+          "y": 20.75
+        },
+        {
+          "x": "Q3/2017",
+          "y": 15.26
+        },
+        {
+          "x": "Q4/2017",
+          "y": 19.24
+        }
+      ]
+    },
+    {
+      "id": 18,
+      "konkurrent": "Citroniger",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 1.12
+        },
+        {
+          "x": "Q2/2013",
+          "y": 1.18
+        },
+        {
+          "x": "Q3/2013",
+          "y": 1.68
+        },
+        {
+          "x": "Q4/2013",
+          "y": 4.38
+        },
+        {
+          "x": "Q1/2014",
+          "y": 1.11
+        },
+        {
+          "x": "Q2/2014",
+          "y": 4.97
+        },
+        {
+          "x": "Q3/2014",
+          "y": 10.22
+        },
+        {
+          "x": "Q4/2014",
+          "y": 5.75
+        },
+        {
+          "x": "Q1/2015",
+          "y": 4.66
+        },
+        {
+          "x": "Q2/2015",
+          "y": 3.09
+        },
+        {
+          "x": "Q3/2015",
+          "y": 10.94
+        },
+        {
+          "x": "Q4/2015",
+          "y": 13.6
+        },
+        {
+          "x": "Q1/2016",
+          "y": 14.36
+        },
+        {
+          "x": "Q2/2016",
+          "y": 18.08
+        },
+        {
+          "x": "Q3/2016",
+          "y": 28.86
+        },
+        {
+          "x": "Q4/2016",
+          "y": 19.48
+        },
+        {
+          "x": "Q1/2017",
+          "y": 1.0
+        },
+        {
+          "x": "Q2/2017",
+          "y": 22.22
+        },
+        {
+          "x": "Q3/2017",
+          "y": 10.11
+        },
+        {
+          "x": "Q4/2017",
+          "y": 22.33
+        }
+      ]
+    }
+  ],
+  "antworten":[
+    {
+      "antwort": "Apfelreich",
+      "korrekt": false
+    },
+    {
+      "antwort": "Birnenbox",
+      "korrekt": false
+    },
+    {
+      "antwort": "Citroniger",
+      "korrekt": true
+    }
+  ],
+  "gruppe": 2,
+  "id": 6,
+  "item": "c",
+  "name": "Frage 2c",
+  "text": "Welches der drei Unternehmen hat die positivste Entwicklung?"
+}

+ 281 - 0
src/js/content/Gruppe-2_item-d.json

@@ -0,0 +1,281 @@
+{
+  "data": [
+    {
+      "id": 19,
+      "konkurrent": "Apfelreich",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 16.38
+        },
+        {
+          "x": "Q2/2013",
+          "y": 0.21
+        },
+        {
+          "x": "Q3/2013",
+          "y": 6.51
+        },
+        {
+          "x": "Q4/2013",
+          "y": 2.38
+        },
+        {
+          "x": "Q1/2014",
+          "y": 17.22
+        },
+        {
+          "x": "Q2/2014",
+          "y": 4.15
+        },
+        {
+          "x": "Q3/2014",
+          "y": 23.54
+        },
+        {
+          "x": "Q4/2014",
+          "y": 2.6
+        },
+        {
+          "x": "Q1/2015",
+          "y": 6.32
+        },
+        {
+          "x": "Q2/2015",
+          "y": 0.21
+        },
+        {
+          "x": "Q3/2015",
+          "y": 9.23
+        },
+        {
+          "x": "Q4/2015",
+          "y": 3.68
+        },
+        {
+          "x": "Q1/2016",
+          "y": 4.12
+        },
+        {
+          "x": "Q2/2016",
+          "y": 0.96
+        },
+        {
+          "x": "Q3/2016",
+          "y": 19.87
+        },
+        {
+          "x": "Q4/2016",
+          "y": 3.31
+        },
+        {
+          "x": "Q1/2017",
+          "y": 14.5
+        },
+        {
+          "x": "Q2/2017",
+          "y": 3.41
+        },
+        {
+          "x": "Q3/2017",
+          "y": 9.64
+        },
+        {
+          "x": "Q4/2017",
+          "y": 0.51
+        }
+      ]
+    },
+    {
+      "id": 20,
+      "konkurrent": "Birnenbox",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 18.25
+        },
+        {
+          "x": "Q2/2013",
+          "y": 17.58
+        },
+        {
+          "x": "Q3/2013",
+          "y": 19.88
+        },
+        {
+          "x": "Q4/2013",
+          "y": 22.47
+        },
+        {
+          "x": "Q1/2014",
+          "y": 23.03
+        },
+        {
+          "x": "Q2/2014",
+          "y": 16.73
+        },
+        {
+          "x": "Q3/2014",
+          "y": 16.5
+        },
+        {
+          "x": "Q4/2014",
+          "y": 22.36
+        },
+        {
+          "x": "Q1/2015",
+          "y": 18.58
+        },
+        {
+          "x": "Q2/2015",
+          "y": 17.12
+        },
+        {
+          "x": "Q3/2015",
+          "y": 22.03
+        },
+        {
+          "x": "Q4/2015",
+          "y": 16.62
+        },
+        {
+          "x": "Q1/2016",
+          "y": 15.87
+        },
+        {
+          "x": "Q2/2016",
+          "y": 22.16
+        },
+        {
+          "x": "Q3/2016",
+          "y": 17.37
+        },
+        {
+          "x": "Q4/2016",
+          "y": 16.52
+        },
+        {
+          "x": "Q1/2017",
+          "y": 17.82
+        },
+        {
+          "x": "Q2/2017",
+          "y": 21.87
+        },
+        {
+          "x": "Q3/2017",
+          "y": 17.68
+        },
+        {
+          "x": "Q4/2017",
+          "y": 24.21
+        }
+      ]
+    },
+    {
+      "id": 21,
+      "konkurrent": "Citroniger",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 0.94
+        },
+        {
+          "x": "Q2/2013",
+          "y": 2.37
+        },
+        {
+          "x": "Q3/2013",
+          "y": 2.78
+        },
+        {
+          "x": "Q4/2013",
+          "y": 2.47
+        },
+        {
+          "x": "Q1/2014",
+          "y": 2.98
+        },
+        {
+          "x": "Q2/2014",
+          "y": 7.38
+        },
+        {
+          "x": "Q3/2014",
+          "y": 10.86
+        },
+        {
+          "x": "Q4/2014",
+          "y": 12.33
+        },
+        {
+          "x": "Q1/2015",
+          "y": 5.17
+        },
+        {
+          "x": "Q2/2015",
+          "y": 6.83
+        },
+        {
+          "x": "Q3/2015",
+          "y": 1.57
+        },
+        {
+          "x": "Q4/2015",
+          "y": 3.88
+        },
+        {
+          "x": "Q1/2016",
+          "y": 12.85
+        },
+        {
+          "x": "Q2/2016",
+          "y": 16.78
+        },
+        {
+          "x": "Q3/2016",
+          "y": 19.08
+        },
+        {
+          "x": "Q4/2016",
+          "y": 27.26
+        },
+        {
+          "x": "Q1/2017",
+          "y": 31.45
+        },
+        {
+          "x": "Q2/2017",
+          "y": 16.41
+        },
+        {
+          "x": "Q3/2017",
+          "y": 14.03
+        },
+        {
+          "x": "Q4/2017",
+          "y": 20.71
+        }
+      ]
+    }
+  ],
+  "antworten":[
+    {
+      "antwort": "Apfelreich",
+      "korrekt": true
+    },
+    {
+      "antwort": "Birnenbox",
+      "korrekt": false
+    },
+    {
+      "antwort": "Citroniger",
+      "korrekt": false
+    }
+  ],
+  "gruppe": 2,
+  "id": 7,
+  "item": "d",
+  "name": "Frage 2d",
+  "text": "Welches der drei Unternehmen entwickelt sich am unbeständigsten?"
+}

+ 272 - 0
src/js/content/Gruppe-3_item-a.json

@@ -0,0 +1,272 @@
+{
+  "data": [
+    {
+      "id": 22,
+      "konkurrent": "Apfelreich",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 19.85
+        },
+        {
+          "x": "Q2/2013",
+          "y": 1.74
+        },
+        {
+          "x": "Q3/2013",
+          "y": 20.85
+        },
+        {
+          "x": "Q4/2013",
+          "y": 0.52
+        },
+        {
+          "x": "Q1/2014",
+          "y": 22.34
+        },
+        {
+          "x": "Q2/2014",
+          "y": 1.83
+        },
+        {
+          "x": "Q3/2014",
+          "y": 8.53
+        },
+        {
+          "x": "Q4/2014",
+          "y": 1.31
+        },
+        {
+          "x": "Q1/2015",
+          "y": 19.14
+        },
+        {
+          "x": "Q2/2015",
+          "y": 2.22
+        },
+        {
+          "x": "Q3/2015",
+          "y": 11.85
+        },
+        {
+          "x": "Q4/2015",
+          "y": 5.19
+        },
+        {
+          "x": "Q1/2016",
+          "y": 9.67
+        },
+        {
+          "x": "Q2/2016",
+          "y": 7.71
+        },
+        {
+          "x": "Q3/2016",
+          "y": 10.13
+        },
+        {
+          "x": "Q4/2016",
+          "y": 0.47
+        },
+        {
+          "x": "Q1/2017",
+          "y": 13.23
+        },
+        {
+          "x": "Q2/2017",
+          "y": 0.14
+        },
+        {
+          "x": "Q3/2017",
+          "y": 25.0
+        },
+        {
+          "x": "Q4/2017",
+          "y": 0.28
+        }
+      ]
+    },
+    {
+      "id": 23,
+      "konkurrent": "Birnenbox",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 22.19
+        },
+        {
+          "x": "Q2/2013",
+          "y": 15.17
+        },
+        {
+          "x": "Q3/2013",
+          "y": 16.85
+        },
+        {
+          "x": "Q4/2013",
+          "y": 17.59
+        },
+        {
+          "x": "Q1/2014",
+          "y": 18.5
+        },
+        {
+          "x": "Q2/2014",
+          "y": 19.74
+        },
+        {
+          "x": "Q3/2014",
+          "y": 24.75
+        },
+        {
+          "x": "Q4/2014",
+          "y": 22.39
+        },
+        {
+          "x": "Q1/2015",
+          "y": 15.49
+        },
+        {
+          "x": "Q2/2015",
+          "y": 20.92
+        },
+        {
+          "x": "Q3/2015",
+          "y": 22.69
+        },
+        {
+          "x": "Q4/2015",
+          "y": 24.58
+        },
+        {
+          "x": "Q1/2016",
+          "y": 20.85
+        },
+        {
+          "x": "Q2/2016",
+          "y": 24.4
+        },
+        {
+          "x": "Q3/2016",
+          "y": 19.4
+        },
+        {
+          "x": "Q4/2016",
+          "y": 23.77
+        },
+        {
+          "x": "Q1/2017",
+          "y": 19.03
+        },
+        {
+          "x": "Q2/2017",
+          "y": 23.32
+        },
+        {
+          "x": "Q3/2017",
+          "y": 17.0
+        },
+        {
+          "x": "Q4/2017",
+          "y": 16.37
+        }
+      ]
+    },
+    {
+      "id": 24,
+      "konkurrent": "Citroniger",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 1.56
+        },
+        {
+          "x": "Q2/2013",
+          "y": 0.96
+        },
+        {
+          "x": "Q3/2013",
+          "y": 2.46
+        },
+        {
+          "x": "Q4/2013",
+          "y": 6.73
+        },
+        {
+          "x": "Q1/2014",
+          "y": 3.68
+        },
+        {
+          "x": "Q2/2014",
+          "y": 10.88
+        },
+        {
+          "x": "Q3/2014",
+          "y": 1.02
+        },
+        {
+          "x": "Q4/2014",
+          "y": 12.72
+        },
+        {
+          "x": "Q1/2015",
+          "y": 3.29
+        },
+        {
+          "x": "Q2/2015",
+          "y": 3.24
+        },
+        {
+          "x": "Q3/2015",
+          "y": 16.97
+        },
+        {
+          "x": "Q4/2015",
+          "y": 13.25
+        },
+        {
+          "x": "Q1/2016",
+          "y": 7.76
+        },
+        {
+          "x": "Q2/2016",
+          "y": 8.71
+        },
+        {
+          "x": "Q3/2016",
+          "y": 26.28
+        },
+        {
+          "x": "Q4/2016",
+          "y": 29.65
+        },
+        {
+          "x": "Q1/2017",
+          "y": 9.52
+        },
+        {
+          "x": "Q2/2017",
+          "y": 21.01
+        },
+        {
+          "x": "Q3/2017",
+          "y": 13.23
+        },
+        {
+          "x": "Q4/2017",
+          "y": 22.33
+        }
+      ]
+    }
+  ],
+  "anstiegObereGrenze": 0.0,
+  "anstiegUntereGrenze": -0.5,
+  "untereGrenze": "Q4/2016",
+  "obereGrenze": "Q3/2017",
+  "konkurrent":"Citroniger",
+  "gruppe": 3,
+  "id": 8,
+  "item": "a",
+  "name": "Frage 3a",
+  "text": "Beschönigen Sie die Abnahme von Citroniger von Q4/2016 nach Q3/2017!"
+}

+ 272 - 0
src/js/content/Gruppe-3_item-b.json

@@ -0,0 +1,272 @@
+{
+  "data": [
+    {
+      "id": 25,
+      "konkurrent": "Apfelreich",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 2.84
+        },
+        {
+          "x": "Q2/2013",
+          "y": 2.73
+        },
+        {
+          "x": "Q3/2013",
+          "y": 23.01
+        },
+        {
+          "x": "Q4/2013",
+          "y": 1.46
+        },
+        {
+          "x": "Q1/2014",
+          "y": 25.09
+        },
+        {
+          "x": "Q2/2014",
+          "y": 0.3
+        },
+        {
+          "x": "Q3/2014",
+          "y": 18.62
+        },
+        {
+          "x": "Q4/2014",
+          "y": 2.99
+        },
+        {
+          "x": "Q1/2015",
+          "y": 17.4
+        },
+        {
+          "x": "Q2/2015",
+          "y": 0.18
+        },
+        {
+          "x": "Q3/2015",
+          "y": 14.81
+        },
+        {
+          "x": "Q4/2015",
+          "y": 7.68
+        },
+        {
+          "x": "Q1/2016",
+          "y": 0.17
+        },
+        {
+          "x": "Q2/2016",
+          "y": 1.35
+        },
+        {
+          "x": "Q3/2016",
+          "y": 15.09
+        },
+        {
+          "x": "Q4/2016",
+          "y": 4.4
+        },
+        {
+          "x": "Q1/2017",
+          "y": 6.71
+        },
+        {
+          "x": "Q2/2017",
+          "y": 1.02
+        },
+        {
+          "x": "Q3/2017",
+          "y": 10.84
+        },
+        {
+          "x": "Q4/2017",
+          "y": 0.06
+        }
+      ]
+    },
+    {
+      "id": 26,
+      "konkurrent": "Birnenbox",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 8.24
+        },
+        {
+          "x": "Q2/2013",
+          "y": 14.58
+        },
+        {
+          "x": "Q3/2013",
+          "y": 17.51
+        },
+        {
+          "x": "Q4/2013",
+          "y": 17.31
+        },
+        {
+          "x": "Q1/2014",
+          "y": 16.4
+        },
+        {
+          "x": "Q2/2014",
+          "y": 15.92
+        },
+        {
+          "x": "Q3/2014",
+          "y": 18.38
+        },
+        {
+          "x": "Q4/2014",
+          "y": 20.46
+        },
+        {
+          "x": "Q1/2015",
+          "y": 22.06
+        },
+        {
+          "x": "Q2/2015",
+          "y": 20.72
+        },
+        {
+          "x": "Q3/2015",
+          "y": 26.08
+        },
+        {
+          "x": "Q4/2015",
+          "y": 26.78
+        },
+        {
+          "x": "Q1/2016",
+          "y": 24.33
+        },
+        {
+          "x": "Q2/2016",
+          "y": 18.62
+        },
+        {
+          "x": "Q3/2016",
+          "y": 27.58
+        },
+        {
+          "x": "Q4/2016",
+          "y": 21.55
+        },
+        {
+          "x": "Q1/2017",
+          "y": 21.67
+        },
+        {
+          "x": "Q2/2017",
+          "y": 19.96
+        },
+        {
+          "x": "Q3/2017",
+          "y": 20.68
+        },
+        {
+          "x": "Q4/2017",
+          "y": 20.84
+        }
+      ]
+    },
+    {
+      "id": 27,
+      "konkurrent": "Citroniger",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 18.69
+        },
+        {
+          "x": "Q2/2013",
+          "y": 11.0
+        },
+        {
+          "x": "Q3/2013",
+          "y": 11.49
+        },
+        {
+          "x": "Q4/2013",
+          "y": 14.78
+        },
+        {
+          "x": "Q1/2014",
+          "y": 13.2
+        },
+        {
+          "x": "Q2/2014",
+          "y": 7.64
+        },
+        {
+          "x": "Q3/2014",
+          "y": 13.46
+        },
+        {
+          "x": "Q4/2014",
+          "y": 18.84
+        },
+        {
+          "x": "Q1/2015",
+          "y": 9.29
+        },
+        {
+          "x": "Q2/2015",
+          "y": 19.53
+        },
+        {
+          "x": "Q3/2015",
+          "y": 16.82
+        },
+        {
+          "x": "Q4/2015",
+          "y": 16.81
+        },
+        {
+          "x": "Q1/2016",
+          "y": 17.38
+        },
+        {
+          "x": "Q2/2016",
+          "y": 10.02
+        },
+        {
+          "x": "Q3/2016",
+          "y": 13.87
+        },
+        {
+          "x": "Q4/2016",
+          "y": 7.32
+        },
+        {
+          "x": "Q1/2017",
+          "y": 15.03
+        },
+        {
+          "x": "Q2/2017",
+          "y": 6.67
+        },
+        {
+          "x": "Q3/2017",
+          "y": 12.1
+        },
+        {
+          "x": "Q4/2017",
+          "y": 0.89
+        }
+      ]
+    }
+  ],
+  "anstiegObereGrenze": 4.0,
+  "anstiegUntereGrenze": 0.5,
+  "untereGrenze": "Q2/2013",
+  "obereGrenze": "Q4/2015",
+  "konkurrent":"Birnenbox",
+  "gruppe": 3,
+  "id": 9,
+  "item": "b",
+  "name": "Frage 3b",
+  "text": "Beschönigen Sie die Zunahme von Birnenbox von Q2/2013 nach Q4/2015!"
+}

+ 272 - 0
src/js/content/Gruppe-3_item-c.json

@@ -0,0 +1,272 @@
+{
+  "data": [
+    {
+      "id": 28,
+      "konkurrent": "Apfelreich",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 8.61
+        },
+        {
+          "x": "Q2/2013",
+          "y": 2.43
+        },
+        {
+          "x": "Q3/2013",
+          "y": 16.94
+        },
+        {
+          "x": "Q4/2013",
+          "y": 1.25
+        },
+        {
+          "x": "Q1/2014",
+          "y": 12.81
+        },
+        {
+          "x": "Q2/2014",
+          "y": 4.72
+        },
+        {
+          "x": "Q3/2014",
+          "y": 16.68
+        },
+        {
+          "x": "Q4/2014",
+          "y": 2.43
+        },
+        {
+          "x": "Q1/2015",
+          "y": 6.52
+        },
+        {
+          "x": "Q2/2015",
+          "y": 5.61
+        },
+        {
+          "x": "Q3/2015",
+          "y": 9.22
+        },
+        {
+          "x": "Q4/2015",
+          "y": 6.53
+        },
+        {
+          "x": "Q1/2016",
+          "y": 14.45
+        },
+        {
+          "x": "Q2/2016",
+          "y": 3.37
+        },
+        {
+          "x": "Q3/2016",
+          "y": 19.4
+        },
+        {
+          "x": "Q4/2016",
+          "y": 3.73
+        },
+        {
+          "x": "Q1/2017",
+          "y": 20.79
+        },
+        {
+          "x": "Q2/2017",
+          "y": 0.82
+        },
+        {
+          "x": "Q3/2017",
+          "y": 21.33
+        },
+        {
+          "x": "Q4/2017",
+          "y": 0.28
+        }
+      ]
+    },
+    {
+      "id": 29,
+      "konkurrent": "Birnenbox",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 7.75
+        },
+        {
+          "x": "Q2/2013",
+          "y": 12.66
+        },
+        {
+          "x": "Q3/2013",
+          "y": 16.02
+        },
+        {
+          "x": "Q4/2013",
+          "y": 19.17
+        },
+        {
+          "x": "Q1/2014",
+          "y": 14.78
+        },
+        {
+          "x": "Q2/2014",
+          "y": 23.76
+        },
+        {
+          "x": "Q3/2014",
+          "y": 18.35
+        },
+        {
+          "x": "Q4/2014",
+          "y": 19.55
+        },
+        {
+          "x": "Q1/2015",
+          "y": 18.89
+        },
+        {
+          "x": "Q2/2015",
+          "y": 21.09
+        },
+        {
+          "x": "Q3/2015",
+          "y": 24.09
+        },
+        {
+          "x": "Q4/2015",
+          "y": 24.5
+        },
+        {
+          "x": "Q1/2016",
+          "y": 19.47
+        },
+        {
+          "x": "Q2/2016",
+          "y": 24.85
+        },
+        {
+          "x": "Q3/2016",
+          "y": 18.26
+        },
+        {
+          "x": "Q4/2016",
+          "y": 21.65
+        },
+        {
+          "x": "Q1/2017",
+          "y": 26.01
+        },
+        {
+          "x": "Q2/2017",
+          "y": 20.35
+        },
+        {
+          "x": "Q3/2017",
+          "y": 25.99
+        },
+        {
+          "x": "Q4/2017",
+          "y": 27.16
+        }
+      ]
+    },
+    {
+      "id": 30,
+      "konkurrent": "Citroniger",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 13.55
+        },
+        {
+          "x": "Q2/2013",
+          "y": 14.72
+        },
+        {
+          "x": "Q3/2013",
+          "y": 12.8
+        },
+        {
+          "x": "Q4/2013",
+          "y": 2.36
+        },
+        {
+          "x": "Q1/2014",
+          "y": 16.8
+        },
+        {
+          "x": "Q2/2014",
+          "y": 9.2
+        },
+        {
+          "x": "Q3/2014",
+          "y": 10.86
+        },
+        {
+          "x": "Q4/2014",
+          "y": 18.91
+        },
+        {
+          "x": "Q1/2015",
+          "y": 19.56
+        },
+        {
+          "x": "Q2/2015",
+          "y": 18.62
+        },
+        {
+          "x": "Q3/2015",
+          "y": 13.97
+        },
+        {
+          "x": "Q4/2015",
+          "y": 13.55
+        },
+        {
+          "x": "Q1/2016",
+          "y": 5.09
+        },
+        {
+          "x": "Q2/2016",
+          "y": 10.12
+        },
+        {
+          "x": "Q3/2016",
+          "y": 1.82
+        },
+        {
+          "x": "Q4/2016",
+          "y": 19.53
+        },
+        {
+          "x": "Q1/2017",
+          "y": 6.63
+        },
+        {
+          "x": "Q2/2017",
+          "y": 7.7
+        },
+        {
+          "x": "Q3/2017",
+          "y": 7.13
+        },
+        {
+          "x": "Q4/2017",
+          "y": 9.37
+        }
+      ]
+    }
+  ],
+  "gruppe": 3,
+  "id": 10,
+  "item": "c",
+  "anstiegObereGrenze": 0.0,
+  "anstiegUntereGrenze": -0.5,
+  "untereGrenze": "Q1/2015",
+  "obereGrenze": "Q1/2016",
+  "konkurrent":"Citroniger",
+  "name": "Frage 3c",
+  "text": "Beschönigen Sie die Abnahme von Citroniger von Q1/2015 nach Q1/2016!"
+}

+ 272 - 0
src/js/content/Gruppe-3_item-d.json

@@ -0,0 +1,272 @@
+{
+  "data": [
+    {
+      "id": 31,
+      "konkurrent": "Apfelreich",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 17.15
+        },
+        {
+          "x": "Q2/2013",
+          "y": 1.79
+        },
+        {
+          "x": "Q3/2013",
+          "y": 12.74
+        },
+        {
+          "x": "Q4/2013",
+          "y": 2.03
+        },
+        {
+          "x": "Q1/2014",
+          "y": 2.22
+        },
+        {
+          "x": "Q2/2014",
+          "y": 4.81
+        },
+        {
+          "x": "Q3/2014",
+          "y": 11.18
+        },
+        {
+          "x": "Q4/2014",
+          "y": 0.98
+        },
+        {
+          "x": "Q1/2015",
+          "y": 5.15
+        },
+        {
+          "x": "Q2/2015",
+          "y": 6.32
+        },
+        {
+          "x": "Q3/2015",
+          "y": 10.97
+        },
+        {
+          "x": "Q4/2015",
+          "y": 3.13
+        },
+        {
+          "x": "Q1/2016",
+          "y": 9.78
+        },
+        {
+          "x": "Q2/2016",
+          "y": 4.12
+        },
+        {
+          "x": "Q3/2016",
+          "y": 2.95
+        },
+        {
+          "x": "Q4/2016",
+          "y": 2.61
+        },
+        {
+          "x": "Q1/2017",
+          "y": 24.05
+        },
+        {
+          "x": "Q2/2017",
+          "y": 1.55
+        },
+        {
+          "x": "Q3/2017",
+          "y": 15.51
+        },
+        {
+          "x": "Q4/2017",
+          "y": 0.6
+        }
+      ]
+    },
+    {
+      "id": 32,
+      "konkurrent": "Birnenbox",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 12.08
+        },
+        {
+          "x": "Q2/2013",
+          "y": 11.52
+        },
+        {
+          "x": "Q3/2013",
+          "y": 17.47
+        },
+        {
+          "x": "Q4/2013",
+          "y": 11.45
+        },
+        {
+          "x": "Q1/2014",
+          "y": 16.41
+        },
+        {
+          "x": "Q2/2014",
+          "y": 16.86
+        },
+        {
+          "x": "Q3/2014",
+          "y": 21.95
+        },
+        {
+          "x": "Q4/2014",
+          "y": 22.6
+        },
+        {
+          "x": "Q1/2015",
+          "y": 21.98
+        },
+        {
+          "x": "Q2/2015",
+          "y": 19.55
+        },
+        {
+          "x": "Q3/2015",
+          "y": 26.25
+        },
+        {
+          "x": "Q4/2015",
+          "y": 20.95
+        },
+        {
+          "x": "Q1/2016",
+          "y": 18.87
+        },
+        {
+          "x": "Q2/2016",
+          "y": 19.63
+        },
+        {
+          "x": "Q3/2016",
+          "y": 20.07
+        },
+        {
+          "x": "Q4/2016",
+          "y": 20.33
+        },
+        {
+          "x": "Q1/2017",
+          "y": 19.04
+        },
+        {
+          "x": "Q2/2017",
+          "y": 26.65
+        },
+        {
+          "x": "Q3/2017",
+          "y": 25.34
+        },
+        {
+          "x": "Q4/2017",
+          "y": 22.26
+        }
+      ]
+    },
+    {
+      "id": 33,
+      "konkurrent": "Citroniger",
+      "koordinaten": [
+        {
+          "x": "Q1/2013",
+          "y": 0.85
+        },
+        {
+          "x": "Q2/2013",
+          "y": 12.98
+        },
+        {
+          "x": "Q3/2013",
+          "y": 7.23
+        },
+        {
+          "x": "Q4/2013",
+          "y": 11.17
+        },
+        {
+          "x": "Q1/2014",
+          "y": 10.05
+        },
+        {
+          "x": "Q2/2014",
+          "y": 11.25
+        },
+        {
+          "x": "Q3/2014",
+          "y": 11.87
+        },
+        {
+          "x": "Q4/2014",
+          "y": 0.95
+        },
+        {
+          "x": "Q1/2015",
+          "y": 0.89
+        },
+        {
+          "x": "Q2/2015",
+          "y": 7.31
+        },
+        {
+          "x": "Q3/2015",
+          "y": 15.87
+        },
+        {
+          "x": "Q4/2015",
+          "y": 12.07
+        },
+        {
+          "x": "Q1/2016",
+          "y": 16.81
+        },
+        {
+          "x": "Q2/2016",
+          "y": 18.57
+        },
+        {
+          "x": "Q3/2016",
+          "y": 9.89
+        },
+        {
+          "x": "Q4/2016",
+          "y": 2.07
+        },
+        {
+          "x": "Q1/2017",
+          "y": 17.9
+        },
+        {
+          "x": "Q2/2017",
+          "y": 5.59
+        },
+        {
+          "x": "Q3/2017",
+          "y": 11.53
+        },
+        {
+          "x": "Q4/2017",
+          "y": 20.64
+        }
+      ]
+    }
+  ],
+  "anstiegObereGrenze": 4.0,
+  "anstiegUntereGrenze": 0.5,
+  "untereGrenze": "Q1/2014",
+  "obereGrenze": "Q3/2015",
+  "konkurrent":"Birnenbox",
+  "gruppe": 3,
+  "id": 11,
+  "item": "d",
+  "name": "Frage 3d",
+  "text": "Beschönigen Sie die Zunahme von Birnenbox von Q1/2014 nach Q3/2015!"
+}

+ 11 - 0
src/js/content/module.json

@@ -0,0 +1,11 @@
+{
+  "title": "Wie werden Finanzstatistiken verzerrt?",
+  "introtext": "Um zu lernen, wie Statistiken verzerrt werden, muss man erst ihre Darstellung verstehen. Liniendiagramme sind eine gängige Methode eine oder mehrere Entwicklungen von Finanzprodukten über die Zeit abzubilden. Sie sollten diese Diagramme gut genug verstehen, dass Sie sie selbst manipulieren könnten. Nur so verstehen Sie die Tricks, mit denen Finanzprodukte angepreist werden.\n\nDas lernen Sie hier mithilfe unserer fiktiven Unternehmen „Apfelreich“, „Birnenbox“ und „Citroniger“. Los geht's!",
+  "outrotext": "Was haben Sie geübt?\n\n(1) Abgelesen wurden Punktwerte, welche ein Verständnis von Achsen, ihren Bezeichnern sowie den Achsen-Ticks im Verhältnis zu den Daten und ihrer Legende verlangen.\n(2) Interpretiert wurden Trends, die steigen, fallen oder auch eine Volatilität ausdrücken können. Hierbei haben Sie die kompletten Linienverläufe mit Blick auf die Achsen, ihre Bezeichner und die Achsen-Ticks sowie die Legende miteinander in Vergleich gesetzt.\n(3) Manipulation wurde erlernt, um in der realen Welt Diagramme auf bestimmte Strategien der Manipulation selbst prüfen zu können.",
+  "success": "Sie haben die Lösung erreicht!",
+  "next": "Weiter",
+  "start": "Start",
+  "restart": "Neustart",
+  "usersVoteHeadline": "So wählten die User",
+  "usersRight": "Andere Nutzer lagen richtig"
+}

+ 17 - 0
src/js/content/offline.js

@@ -0,0 +1,17 @@
+import question1A from './Gruppe-1_item-a.json';
+import question1B from './Gruppe-1_item-b.json';
+import question1C from './Gruppe-1_item-c.json';
+import question2A from './Gruppe-2_item-a.json';
+import question2B from './Gruppe-2_item-b.json';
+import question2C from './Gruppe-2_item-c.json';
+import question2D from './Gruppe-2_item-d.json';
+import question3A from './Gruppe-3_item-a.json';
+import question3B from './Gruppe-3_item-b.json';
+import question3C from './Gruppe-3_item-c.json';
+import question3D from './Gruppe-3_item-d.json';
+
+export default [
+  [ question1A, question1B, question1C ],
+  [ question2A, question2B, question2C, question2D ],
+  [ question3A, question3B, question3C, question3D ]
+];

+ 96 - 0
src/js/d3/axes.js

@@ -0,0 +1,96 @@
+import * as d3 from 'd3';
+
+// d3js module: add axes
+// call it: addAxes(group, { xScale, yScale }, options);
+export default (group, scales, options) => {
+  // add stroke styles
+  const addStyles = axis => {
+    axis.selectAll('path')
+      .attr('stroke-width', '2')
+      .attr('stroke', options.colors.axis);
+
+    axis.selectAll('line')
+      .attr('stroke', options.colors.axis);
+  };
+
+  // rotate labels to make them readable
+  const rotateLabels = axis => {
+    axis.selectAll('text')
+      .style('text-anchor', 'end')
+      .style('font', '11px sans-serif')
+      .attr('dx', '-.8em')
+      .attr('dy', '.15em')
+      .attr('fill', options.colors.units)
+      .attr('transform', 'rotate(-48)');
+  };
+
+  // size labels
+  const sizeLabels = axis => {
+    axis.selectAll('text')
+      .attr('fill', options.colors.units)
+      .style('font', '11px sans-serif');
+  };
+
+  // append x axis unit
+  const addUnitX = axis => {
+    axis.append('text')
+      .attr('transform', `translate(${options.graphWidth / 2}, ${options.margin.bottom / 2 + 5})`)
+      .attr('fill', options.colors.units)
+      .style('font', '16px sans-serif')
+      .style('text-anchor', 'middle')
+      .text(options.xAxisUnit);
+  };
+
+  // append y axis unit
+  const addUnitY = axis => {
+    axis.append('text')
+      .attr('transform', 'rotate(-90)')
+      .attr('x', 0 - options.graphHeight / 2)
+      .attr('y', 0 - options.margin.left)
+      .attr('dy', '1em')
+      .attr('fill', options.colors.units)
+      .style('font', '16px sans-serif')
+      .style('text-anchor', 'middle')
+      .text(options.yAxisUnit);
+  };
+
+  // add the X Axis
+  let addClass = `x-axis  mod${options.module}__x-axis`;
+
+  // module02, grap part
+  if (Object.prototype.hasOwnProperty.call(options, 'currentQuestion')) {
+    addClass += `  mod${options.module}__x-axis--${options.currentQuestion + 1}`;
+  }
+
+  // append x axis
+  const xAxis = group.append('g')
+    .attr('transform', `translate(0, ${options.graphHeight})`)
+    .attr('class', addClass)
+    .call(d3.axisBottom(scales.xScale));
+
+  // add clip path for module 2
+  if (options.module === 2) xAxis.attr('clip-path', 'url(#polygonmask)');
+
+  // depending on number of child notes, rotate labels
+  if (xAxis.node().childNodes.length > 20) {
+    rotateLabels(xAxis);
+  } else {
+    sizeLabels(xAxis);
+  }
+
+  // call methods
+  addStyles(xAxis);
+  addUnitX(xAxis);
+
+  // add the Y Axis
+  const yAxis = group.append('g')
+    .attr('class', 'y-axis')
+    .call(d3.axisLeft(scales.yScale));
+
+  // call remaining methods
+  sizeLabels(yAxis);
+  addStyles(yAxis);
+  addUnitY(yAxis);
+
+  return { xAxis, yAxis };
+};

+ 85 - 0
src/js/d3/bar.js

@@ -0,0 +1,85 @@
+import * as d3 from 'd3';
+
+// d3js module: animated line
+export default () => {
+  // defaults
+  const options = {
+    strokewidth: 2,
+    duration: 1500,
+    endval: 0.10,
+    width: 800,
+    height: 26,
+    round: 1,
+    margin: { right: 50 },
+    colors: [ '#E3E3E3', '#D0021B' ]
+  };
+
+  // start
+  const draw = selection => {
+    const textWidth = 40;
+    const yval = options.strokewidth;
+    const yvalText = options.height - options.strokewidth * 2;
+    const xval = options.width * options.endval;
+    const xvalText = options.endval < 0.51 ? xval : xval - textWidth;
+
+    // add svg
+    const svg = selection
+      .append('svg')
+      .attr('width', options.width)
+      .attr('height', options.height);
+
+    // append first line
+    svg.append('line')
+      .attr('x1', 0)
+      .attr('y1', yval)
+      .attr('x2', options.width)
+      .attr('y2', yval)
+      .attr('stroke-width', options.strokewidth)
+      .attr('stroke', options.colors[0])
+      .attr('stroke-linecap', 'round');
+
+    // append second line
+    svg.append('line')
+      .attr('x1', 0)
+      .attr('y1', yval)
+      .attr('x2', 0)
+      .attr('y2', yval)
+      .attr('stroke-width', options.strokewidth)
+      .attr('stroke', options.colors[1])
+      .attr('stroke-linecap', 'round')
+      .transition()
+      .duration(options.duration)
+      .attr('x2', xval)
+      .attr('y2', yval);
+
+    // append text
+    svg.append('text')
+      .data([ options.endval * 100 ])
+      .text(0)
+      .attr('x', 0)
+      .attr('y', yvalText)
+      .transition()
+      .duration(options.duration)
+      .attr('x', xvalText)
+      .attr('y', yvalText)
+      .tween('text', (endval, index, curObj) => {
+        const i = d3.interpolate(curObj[index].textContent, endval);
+
+        return t => {
+          const val = Math.round(i(t) * options.round) / options.round;
+          const localeString = parseFloat(val.toFixed(0)).toLocaleString('de-DE');
+
+          curObj[index].textContent = `${localeString}%`;
+        };
+      });
+  };
+
+  // "setter"
+  draw.options = input => {
+    Object.assign(options, input);
+
+    return draw;
+  };
+
+  return draw;
+};

+ 62 - 0
src/js/d3/defs.js

@@ -0,0 +1,62 @@
+export default (selection, options) => {
+  const defs = selection.append('defs');
+
+  // clippath
+  defs.append('clipPath')
+    .attr('id', 'mask')
+    .append('rect')
+    .attr('x', 0)
+    .attr('y', 0)
+    .attr('width', options.graphWidth)
+    .attr('height', options.graphHeight);
+
+  // clippath
+  const polygonData = [
+    [
+      { x: 0, y: 0 },
+      { x: options.graphWidth, y: 0 },
+      { x: options.graphWidth, y: options.margin.bottom },
+      { x: -options.margin.left * 2, y: options.margin.bottom }
+    ]
+  ];
+
+  defs.append('clipPath')
+    .attr('id', 'polygonmask')
+    .selectAll('polygon')
+    .data(polygonData)
+    .enter()
+    .append('polygon')
+    .attr('points', points => points.map(d => `${d.x}, ${d.y}`).join(' '));
+
+  // linear gradient 2
+  const gradientLine = defs.append('linearGradient')
+    .attr('id', 'gradient2')
+    .attr('x1', 0)
+    .attr('y1', 0)
+    .attr('x2', 0)
+    .attr('y2', 1);
+
+  gradientLine.append('stop')
+    .attr('offset', '0%')
+    .attr('stop-color', '#fff');
+
+  gradientLine.append('stop')
+    .attr('offset', '45%')
+    .attr('stop-color', '#fff');
+
+  gradientLine.append('stop')
+    .attr('offset', '45%')
+    .attr('stop-color', options.colors.light);
+
+  gradientLine.append('stop')
+    .attr('offset', '55%')
+    .attr('stop-color', options.colors.light);
+
+  gradientLine.append('stop')
+    .attr('offset', '55%')
+    .attr('stop-color', '#fff');
+
+  gradientLine.append('stop')
+    .attr('offset', '100%')
+    .attr('stop-color', '#fff');
+};

+ 150 - 0
src/js/d3/gradient.js

@@ -0,0 +1,150 @@
+const createGradient = (scales, options) => {
+
+  let x1;
+  let x2;
+  let y1;
+  let y2;
+  let m;
+
+  let group;
+  let graphGroup;
+  let textGroup;
+
+  let leftCircle;
+  let rightCircle;
+  let gradientLine;
+
+  let textBoxOffset;
+
+  let state = 'inactive';
+
+  const gradient = (selection) => {
+
+    group = selection.append('g');
+
+    graphGroup = group
+      .append('g')
+      .attr('clip-path', 'url(#mask)')
+      .attr('class', 'mod2-gradientangle__connector'); // .attr('class', 'mod2-gradientangle__graph')
+
+    leftCircle = graphGroup.append('circle')
+      .attr('class', 'mod2-gradientangle--circle  mod2-gradientangle--circle--left')
+      .attr('r', options.radius)
+      .attr('fill', options.colors.light);
+
+    rightCircle = graphGroup.append('circle')
+      .attr('class', 'mod2-gradientangle--circle  mod2-gradientangle--circle--right')
+      .attr('r', options.radius)
+      .attr('fill', options.colors.light);
+
+    gradientLine = graphGroup.append('line')
+      .attr('class', 'mod2-gradientangle--line')
+      .attr('stroke', options.colors.light)
+      .attr('stroke-dasharray', '15 3 2 3')
+      .attr('stroke-width', 3);
+
+    textGroup = group
+      .append('g')
+      .attr('class', 'mod2-gradientangle__box');
+
+    textGroup.append('rect')
+      .attr('class', 'mod2-gradientangle__rect')
+      .attr('y', 12)
+      .attr('stroke', options.colors.light)
+      .attr('stroke-width', 2)
+      .attr('width', options.box.width)
+      .attr('height', options.box.height)
+      .attr('fill', '#ffffff');
+
+    textGroup.append('text')
+      .attr('class', 'mod2-gradientangle__text')
+      .attr('y', options.margin.top * 1.5)
+      .attr('text-anchor', 'middle')
+      .attr('fill', options.colors.light)
+      .style('font-weight', 500)
+      .attr('transform', `translate(${options.box.width / 2}, 0)`);
+
+    gradient.update(selection);
+  };
+
+
+  gradient.update = () => {
+
+    gradientLine
+      .attr('x1', x1)
+      .attr('y1', y1)
+      .attr('x2', x2)
+      .attr('y2', y2);
+
+    leftCircle
+      .attr('cx', x1)
+      .attr('cy', y1);
+
+    rightCircle
+      .attr('cx', x2)
+      .attr('cy', y2);
+
+    textBoxOffset = (x2 - x1) / 2 - options.box.width / 2 + x1;
+    textGroup
+      .attr('transform', `translate(${textBoxOffset}, ${options.box.height / 1.5})`);
+
+    textGroup.select('text')
+      .text(m);
+
+    group
+      .attr('class', `mod2-gradientangle  mod2-gradientangle--${state}`);
+  };
+
+
+  gradient.x1 = (val) => {
+    if (val === undefined) {
+      return x1;
+    }
+    x1 = val;
+    return gradient;
+  };
+
+  gradient.x2 = (val) => {
+    if (val === undefined) {
+      return x2;
+    }
+    x2 = val;
+    return gradient;
+  };
+
+  gradient.y1 = (val) => {
+    if (val === undefined) {
+      return y1;
+    }
+    y1 = val;
+    return gradient;
+  };
+
+  gradient.y2 = (val) => {
+    if (val === undefined) {
+      return y2;
+    }
+    y2 = val;
+    return gradient;
+  };
+
+  gradient.m = (val) => {
+    if (val === undefined) {
+      return m;
+    }
+    m = val;
+    return gradient;
+  };
+
+  gradient.state = (val) => {
+    if (val === undefined) {
+      return state;
+    }
+    state = val;
+    return gradient;
+  };
+
+  return gradient;
+};
+
+export default createGradient;

+ 57 - 0
src/js/d3/grid.js

@@ -0,0 +1,57 @@
+import * as d3 from 'd3';
+
+const createGrid = (scales, options, dispatcher) => {
+
+  const xGridlines = () => d3.axisBottom(scales.xScale).ticks(5);
+  const yGridlines = () => d3.axisLeft(scales.yScale).ticks(5);
+
+  const update = (selection) => {
+
+    const gridGroup = selection.selectAll('g.grid');
+    const gridy = gridGroup.selectAll('.grid--y');
+    const gridx = gridGroup.selectAll('g.grid--x');
+
+    // create x axis grid lines
+    gridx.call(
+      xGridlines()
+        .tickSize(-options.graphHeight)
+        .tickFormat('')
+    );
+
+    // create y axis grid lines
+    gridy.call(
+      yGridlines()
+        .tickSize(-options.graphWidth)
+        .tickFormat('')
+    );
+  };
+
+  // constructor
+  const grid = (selection) => {
+
+    dispatcher.on('scaling.grid', () => {
+      // console.log('updating grid');
+      update(selection);
+    });
+
+    const gridGroup = selection.append('g')
+      .attr('class', 'grid')
+      .attr('clip-path', 'url(#mask)');
+
+    // create group for x grid lines
+    gridGroup.append('g')
+      .attr('class', 'grid--x')
+      .attr('transform', `translate(0, ${options.graphHeight})`);
+
+    // create group for y grid lines
+    gridGroup.append('g')
+      .attr('class', `grid--y  mod${options.module}__grid--y`);
+
+    // draw / update grid lines
+    update(selection);
+  };
+
+  return grid;
+};
+
+export default createGrid;

+ 180 - 0
src/js/d3/handle.js

@@ -0,0 +1,180 @@
+import * as d3 from 'd3';
+
+const createHandle = (scales, config, controller) => {
+
+  let x = config.x;
+  let y = config.y;
+  let min = config.min;
+  let max = config.max;
+  let dimension = config.dimension;
+  let dx; // relative position of click event to symbol origin
+  let dy;
+  let width = config.width;
+  let height = config.height;
+  let offset = config.offset;
+  let transform = config.transform;
+  let cls = config.cls;
+  let symbolId = config.symbolId;
+  let type = config.type;
+
+  let use; // d3 selection
+  let node; // plain svg node
+
+  const handle = (selection) => {
+
+    use = selection.append('use')
+      .attr('class', `mod2-drag mod2-drag--${cls}`)
+      .attr('href', `#${symbolId}`);
+
+    node = use.node();
+
+    handle.update();
+
+    let onDragging;
+
+    if (dimension === 'x') {
+      if (type === 'rescale') {
+        onDragging = handle.onRescalingX;
+      } else if (type === 'resize') {
+        onDragging = handle.onResizingX;
+      }
+    } else {
+      onDragging = handle.onRescalingY;
+    }
+
+    use.call(
+      d3.drag()
+        .on('start', handle.onDragStart)
+        .on('drag', onDragging)
+        .on('end', handle.onDragEnd));
+  };
+
+  handle.update = (withTransition = false) => {
+
+    let element = d3.select(node);
+
+    if (withTransition) {
+      element = element.transition()
+        .duration(config.duration);
+    }
+
+    element
+      .attr('transform', `matrix(
+        ${transform.a}
+        ${transform.b}
+        ${transform.c}
+        ${transform.d}
+        ${x + offset.x}
+        ${y + offset.y}
+      )`);
+  };
+
+  handle.onDragStart = () => {
+
+    d3.select(node)
+      .classed('mod2-drag--active', true);
+
+    // relative offset of click position to handle origin
+    dx = d3.event.x - x;
+    dy = d3.event.y - y;
+
+    controller.onDragStart(handle);
+  };
+
+  handle.onResizingX = () => {
+    if (d3.event.x >= min + dx && d3.event.x <= max - dx) {
+
+      x = d3.event.x - dx;
+      handle.update();
+
+      controller.onResizingX(handle);
+    }
+  };
+
+  handle.onRescalingX = () => {
+    if (d3.event.x >= min + dx && d3.event.x <= max - dx - width) {
+
+      x = d3.event.x - dx;
+      handle.update();
+
+      controller.onRescalingX(handle);
+    }
+  };
+
+  handle.onRescalingY = () => {
+    if (d3.event.y >= min + offset.y + dy && d3.event.y <= max - offset.y - height - dy) {
+      y = d3.event.y - dy;
+      handle.update();
+
+      controller.onRescalingY(handle);
+    }
+  };
+
+  handle.onDragEnd = () => {
+    if (type === 'resize') {
+      controller.onResized(handle);
+    }
+    d3.select(node).classed('mod2-drag--active', false);
+  };
+
+
+  handle.x = (val) => {
+    if (val === undefined) {
+      return x;
+    }
+    x = val;
+    return handle;
+  };
+
+  handle.y = (val) => {
+    if (val === undefined) {
+      return y;
+    }
+    y = val;
+    return handle;
+  };
+
+  handle.width = (val) => {
+    if (val === undefined) {
+      return width;
+    }
+    width = val;
+    return handle;
+  };
+
+  handle.height = (val) => {
+    if (val === undefined) {
+      return height;
+    }
+    height = val;
+    return handle;
+  };
+
+  handle.min = (val) => {
+    if (val === undefined) {
+      return min;
+    }
+    min = val;
+    return handle;
+  };
+
+  handle.max = (val) => {
+    if (val === undefined) {
+      return max;
+    }
+    max = val;
+    return handle;
+  };
+
+  handle.dimension = (val) => {
+    if (val === undefined) {
+      return dimension;
+    }
+    dimension = val;
+    return handle;
+  };
+
+  return handle;
+};
+
+export default createHandle;

+ 44 - 0
src/js/d3/handleConnector.js

@@ -0,0 +1,44 @@
+import * as d3 from 'd3';
+
+const createHandleConnector = (config, _leftHandle, _rightHandle) => {
+
+  let rightHandle = _rightHandle;
+  let leftHandle = _leftHandle;
+
+  let offset = {
+    x: leftHandle.width() / 2,
+    y: -config.connectorHeight / 2
+  };
+
+  let node; // plain svg node
+
+  const handleConnector = (selection) => {
+    let rect = selection.append('rect')
+      .attr('class', 'mod2-rect__connector')
+      .attr('height', config.connectorHeight)
+      .attr('stroke-width', config.strokeWidth)
+      .attr('stroke', config.colors.axis);
+
+    node = rect.node();
+    handleConnector.update();
+  };
+
+  handleConnector.update = (withTransition = false) => {
+
+    let element = d3.select(node);
+
+    if (withTransition) {
+      element = element.transition()
+        .duration(config.duration);
+    }
+
+    element
+      .attr('x', leftHandle.x() + offset.x)
+      .attr('y', leftHandle.y() + offset.y)
+      .attr('width', rightHandle.x() - leftHandle.x() - leftHandle.width());
+  };
+
+  return handleConnector;
+};
+
+export default createHandleConnector;

+ 103 - 0
src/js/d3/handleSymbols.js

@@ -0,0 +1,103 @@
+import * as d3 from 'd3';
+
+const createHandleSymbols = (config) => {
+
+  const margin = config.strokeWidth;
+  const width = config.maxDim;
+  const height = config.minDim;
+
+  const x1 = margin;
+  const x2 = height / 2 + margin;
+  const x3 = width - height / 2 + margin;
+  const x4 = width + margin;
+  const y1 = margin;
+  const y2 = height / 2 + margin;
+  const y3 = height + margin;
+
+  const polygon = [
+    { x: x1, y: y2 },
+    { x: x2, y: y1 },
+    { x: x3, y: y1 },
+    { x: x4, y: y2 },
+    { x: x3, y: y3 },
+    { x: x2, y: y3 },
+    { x: x1, y: y2 }
+  ];
+
+  let lines = []; // Magic numbers for 'friction' lines
+  for (let i = 0; i < 3; i += 1) {
+    lines.push({
+      x1: (width + margin) * (0.42 + i * 0.10),
+      y1: (height + margin) * 0.35,
+      x2: (width + margin) * (0.42 + i * 0.10),
+      y2: (height + margin) * 0.75
+    });
+  }
+
+  const geometry = { polygon, lines };
+
+
+  const drawPolygon = d3.line().x(d => d.x).y(d => d.y);
+
+  const handleSymbols = (selection) => {
+
+    // >>> scale symbol
+    const scaleSymbol = selection.selectAll(`symbol#${config.scale.svgid}`)
+      .data([geometry])
+      .enter()
+      .append('symbol')
+      .attr('class', 'scale-symbol')
+      .attr('id', config.symbolId.scale);
+
+    scaleSymbol.append('path')
+      .attr('class', 'handle-outline scale-handle-outline')
+      .attr('stroke', config.colors.axis)
+      .attr('stroke-width', config.strokeWidth)
+      .attr('fill', config.colors.bright)
+      .attr('d', d => `${drawPolygon(d.polygon)} Z`); // Add 'Z' for a closed path
+
+    scaleSymbol.selectAll('.scale-handle-line')
+      .data(d => d.lines)
+      .enter()
+      .append('line')
+      .attr('x1', d => d.x1)
+      .attr('y1', d => d.y1)
+      .attr('x2', d => d.x2)
+      .attr('y2', d => d.y2)
+      .attr('stroke-width', 1)
+      .attr('stroke', config.colors.highlight);
+    // <<< scale symbol
+
+    // >>> size symbol
+    const sizeSymbol = selection.selectAll(`symbol#${config.size.svgid}`)
+      .data([config])
+      .enter()
+      .append('symbol')
+      .attr('class', 'size-symbol')
+      .attr('id', config.symbolId.size);
+
+    sizeSymbol
+      .append('circle')
+      .attr('class', 'handle-outline size-handle-outline')
+      .attr('cx', config.size.radius.large + config.strokeWidth)
+      .attr('cy', config.size.radius.large + config.strokeWidth)
+      .attr('r', config.size.radius.large)
+      .attr('fill', config.colors.bright)
+      .attr('stroke', config.colors.axis)
+      .attr('stroke-width', 2);
+
+    sizeSymbol
+      .append('circle')
+      .attr('class', 'size-handle-inner-circle')
+      .attr('cx', config.size.radius.large + config.strokeWidth)
+      .attr('cy', config.size.radius.large + config.strokeWidth)
+      .attr('r', config.size.radius.small)
+      .attr('stroke', config.colors.highlight);
+    // <<< size symbol
+
+  };
+
+  return handleSymbols;
+};
+
+export default createHandleSymbols;

+ 41 - 0
src/js/d3/legend.js

@@ -0,0 +1,41 @@
+const createLegend = (options) => {
+
+  const yStart = options.graphHeight + options.margin.bottom * 0.7;
+  let xStart = 0;
+
+  const lineHeight = 18;
+
+  let legendGroup;
+
+  const legend = (selection) => {
+
+    legendGroup = selection.append('g');
+
+    let graphLegendGroup = legendGroup.selectAll(`mod${options.module}-legend`)
+      .data(options.entries)
+      .enter()
+      .append('g')
+      .attr('class', `mod${options.module}-legend`);
+
+    // graph texture sample
+    graphLegendGroup.append('line')
+      .attr('x1', 0)
+      .attr('y1', (d, i) => i * lineHeight + yStart - 5)
+      .attr('x2', 40)
+      .attr('y2', (d, i) => i * lineHeight + yStart - 5)
+      .attr('stroke-width', 2)
+      .attr('class', (d, i) => `mod${options.module}-line--${i + 1}`);
+
+    // graph descriptor
+    graphLegendGroup.append('text')
+      .attr('x', xStart + 55)
+      .attr('y', (d, i) => yStart + i * lineHeight)
+      .attr('fill', options.colors.units)
+      .style('font', '14px sans-serif')
+      .text(d => d.konkurrent);
+  };
+
+  return legend;
+};
+
+export default createLegend;

+ 46 - 0
src/js/d3/lineGraphs.js

@@ -0,0 +1,46 @@
+import * as d3 from 'd3';
+
+const createLineGraphs = (scales, options, dispatcher) => {
+
+  let graphs;
+
+  const compilePath = d3.line()
+    .x(d => scales.xScale(d.x))
+    .y(d => scales.yScale(d.y));
+
+
+  const lineGraphs = (selection) => {
+
+    graphs = selection
+      .append('g')
+      .attr('class', 'lineGraphs');
+
+    lineGraphs.update();
+
+    dispatcher.on('scaling.graph', () => {
+      lineGraphs.update(selection);
+    });
+  };
+
+  lineGraphs.update = () => {
+
+    graphs.exit().remove();
+
+    let linegraph = graphs.selectAll('.mod2-line');
+
+    linegraph.data(options.entries)
+      .enter()
+      .append('path')
+      .merge(linegraph)
+      .attr('class', (d, i) => `mod2-line  mod2-line--${d.konkurrent.toLowerCase()}  mod2-line--${i + 1}`)
+      .attr('stroke', options.colors.graph)
+      .attr('stroke-width', 2)
+      .attr('fill', 'none')
+      .attr('clip-path', 'url(#mask)')
+      .attr('d', d => compilePath(d.koordinaten));
+  };
+
+  return lineGraphs;
+};
+
+export default createLineGraphs;

+ 107 - 0
src/js/d3/main.js

@@ -0,0 +1,107 @@
+import * as d3 from 'd3';
+import addAxes from './axes';
+import addScales from './scales';
+import addDefs from './defs';
+import addPoints from './pointMarker';
+
+import createGrid from './grid';
+import createRange from './range';
+import createRangeController from './rangeController';
+import createLegend from './legend';
+import createLineGraphs from './lineGraphs';
+import options from '../config';
+
+
+// d3js module for module02: base module handling all submodules
+export default () => {
+
+  const dispatcher = d3.dispatch('scaling'); // inform grid and line graphs of scale event
+  let selectedRange;
+  let referenceRange;
+  let solutionRange;
+
+  // adapted size without margin
+  const graphWidth = options.width - options.margin.left - options.margin.right;
+  const graphHeight = options.height - options.margin.top - options.margin.bottom;
+
+  // merge options
+  Object.assign(options, { graphWidth, graphHeight });
+
+  // start
+  const draw = selection => {
+    // add svg
+    const svg = selection
+      .append('svg')
+      .attr('width', options.width)
+      .attr('height', options.height);
+
+    selection.append('defs');
+
+    // add group
+    const group = svg
+      .append('g')
+      .attr('transform', `translate(${options.margin.left}, ${options.margin.top})`);
+
+    // add defs for questionGroup 3
+    if (options.questionGroup === 3) addDefs(group, options);
+
+    // add scales and axes
+    const scales = addScales(options);
+    const axes = addAxes(group, scales, options);
+
+    // add grid
+    const grid = createGrid(scales, options, dispatcher);
+    group.call(grid);
+
+    if (options.questionGroup === 3) {
+
+      let rangeOptions = {
+        duration: options.duration,
+        graphHeight: options.graphHeight,
+        range: options.referenceRange
+      };
+
+      // Rerference range and its width in relation to initial scale range define scale factor
+      referenceRange = createRange(scales, rangeOptions);
+
+      // (User) selected range is initialised with the same parameters as the reference range
+      selectedRange = createRange(scales, rangeOptions);
+      group.call(selectedRange);
+
+      // Solution range is needed to compare the current selection and scale to the expected
+      rangeOptions.range = options.solutionRange;
+      solutionRange = createRange(scales, rangeOptions);
+    }
+
+    // add lines
+    const lineGraphs = createLineGraphs(scales, options, dispatcher);
+    group.call(lineGraphs);
+
+    const legend = createLegend(options);
+    group.call(legend);
+
+    // add points for question group 1
+    if (options.questionGroup === 1) addPoints(group, scales, options);
+
+    // add moveable and indicator elements for question group 3
+    if (options.questionGroup === 3) {
+
+      // Selection range controller controls all elements involved in rescaling / resizing the selection (incl y axis)
+      const selectionRangeController = createRangeController(
+        { scales, axes }, dispatcher, { referenceRange, selectedRange, solutionRange }
+      );
+      group.call(selectionRangeController);
+
+    }
+  };
+
+  // "setter"
+  draw.options = input => {
+    Object.assign(options, input);
+
+    return draw;
+  };
+
+  return draw;
+};
+

+ 47 - 0
src/js/d3/pointMarker.js

@@ -0,0 +1,47 @@
+import * as d3 from 'd3';
+
+// Visual feedback for answer / solution for question type 1
+export default (selection, scales, options) => {
+
+  // Get transformed coordinate
+  const transformCoordinate = (coordinate) => (
+    {
+      x: scales.xScale(coordinate.x),
+      y: scales.yScale(coordinate.y)
+    }
+  );
+
+  // Draw circles: always mark solution as correct ...
+  if (options.points.correct) {
+
+    const correct = transformCoordinate(options.points.correct);
+
+    selection
+      .append('circle')
+      .attr('r', 0)
+      .attr('cx', correct.x)
+      .attr('cy', correct.y)
+      .attr('fill', options.colors.correct)
+      .transition()
+      .duration(500)
+      .ease(d3.easeBackOut)
+      .attr('r', 6);
+
+    // ... if answer is incorrect, mark corresponding coordinate as incorrect
+    if (options.points.incorrect) {
+
+      const incorrect = transformCoordinate(options.points.incorrect);
+
+      selection
+        .append('circle')
+        .attr('r', 0)
+        .attr('cx', incorrect.x)
+        .attr('cy', incorrect.y)
+        .attr('fill', options.colors.error)
+        .transition()
+        .duration(500)
+        .ease(d3.easeBackOut)
+        .attr('r', 6);
+    }
+  }
+};

+ 140 - 0
src/js/d3/range.js

@@ -0,0 +1,140 @@
+// Represents a range on x (and potentially y) axis
+// Used to provide a consistent interface to the properties of all ranges involved:
+// - referece range: Reference for x scale factor (its own width and the initial graph's width)
+// - selected range: Ratio of its width that of the reference range define the current scale factor
+// - solution range: Provides a bounding box for the graph section in question
+const createRange = (scales, options) => {
+
+  let lowerBound = options.range.lowerBound;
+  let upperBound = options.range.upperBound;
+  let x1 = scales.xScale(lowerBound);
+  let x2 = scales.xScale(upperBound);
+  let y1;
+  let y2;
+  let width = x2 - x1;
+
+  let gradient = options.range.gradient;
+
+  // svg element
+  let node;
+
+  // "constructor"
+  const selectionRange = (selection) => {
+
+    // define linear gradient >>>>
+    const defs = selection.select('defs');
+    const colourGradient = defs.append('linearGradient')
+      .attr('id', 'gradient')
+      .attr('x1', 0)
+      .attr('x2', 0)
+      .attr('y1', 0)
+      .attr('y2', 1);
+
+    colourGradient.append('stop')
+      .attr('class', 'mod2-rect__markedarea--stop')
+      .attr('stop-opacity', '0')
+      .attr('offset', '0');
+
+    colourGradient.append('stop')
+      .attr('class', 'mod2-rect__markedarea--stop')
+      .attr('stop-opacity', '0.4')
+      .attr('offset', '50');
+
+    colourGradient.append('stop')
+      .attr('class', 'mod2-rect__markedarea--stop')
+      .attr('offset', '100');
+    // <<< linear gradient
+
+    node = selection.append('rect')
+      .attr('class', 'mod2-rect__markedarea')
+      .attr('height', options.graphHeight)
+      .attr('width', width)
+      .attr('transform', `translate(${x1}, 0)`);
+  };
+
+
+  // update / draw element
+  selectionRange.update = (withTransition = false) => {
+
+    let el = node;
+
+    if (withTransition) {
+      el = el.transition()
+        .duration(options.duration);
+    }
+
+    width = x2 - x1;
+
+    el.attr('width', width)
+      .attr('transform', `translate(${x1}, 0)`);
+  };
+
+
+  selectionRange.x1 = (val) => {
+    if (val === undefined) {
+      return x1;
+    }
+    x1 = val;
+    return selectionRange;
+  };
+
+  selectionRange.x2 = (val) => {
+    if (val === undefined) {
+      return x2;
+    }
+    x2 = val;
+    return selectionRange;
+  };
+
+  selectionRange.width = (val) => {
+    if (val === undefined) {
+      return width;
+    }
+    width = val;
+    return selectionRange;
+  };
+
+  selectionRange.lowerBound = (val) => {
+    if (val === undefined) {
+      return lowerBound;
+    }
+    lowerBound = val;
+    return selectionRange;
+  };
+
+  selectionRange.upperBound = (val) => {
+    if (val === undefined) {
+      return upperBound;
+    }
+    upperBound = val;
+    return selectionRange;
+  };
+
+  selectionRange.gradient = (val) => {
+    if (val === undefined) {
+      return gradient;
+    }
+    gradient = val;
+    return selectionRange;
+  };
+
+  selectionRange.y1 = (val) => {
+    if (val === undefined) {
+      return y1;
+    }
+    y1 = val;
+    return selectionRange;
+  };
+
+  selectionRange.y2 = (val) => {
+    if (val === undefined) {
+      return y2;
+    }
+    y2 = val;
+    return selectionRange;
+  };
+
+  return selectionRange;
+};
+
+export default createRange;

+ 461 - 0
src/js/d3/rangeController.js

@@ -0,0 +1,461 @@
+import * as d3 from 'd3';
+import createHandleSymbols from './handleSymbols';
+import createHandle from './handle';
+import createHandleConnector from './handleConnector';
+import createGradient from './gradient';
+import round from '../utilities/math';
+import options from '../config';
+
+/**
+ * Acts as a controller and general servant for all components that are part of the selection plus the vertical scale handle
+ * - symbol definition for drag handles
+ * - selected range rectangle
+ * - handles
+ *
+ * @param dispatcher: Custom event dispatcher (d3.dispatch)
+ * @param referenceRange: Initially selected range
+ * @param selectedRange: Range selected by user (initialised as referenceRange)
+ * @param solutionRange: This range specifies the solution to which the selectedRange is compared
+ */
+const createRangeController = ({ scales, axes }, dispatcher, { referenceRange, selectedRange, solutionRange }) => {
+
+  // d3 svg element
+  let selection;
+
+  // handles
+  let leftScaleHandle;
+  let rightScaleHandle;
+  let verticalScaleHandle;
+  let leftSizeHandle;
+  let rightSizeHandle;
+
+  let scaleHandleConnector;
+  let sizeHandleConnector;
+
+  let gradient;
+
+  let config = options.handles;
+  config.colors = options.colors;
+  config.duration = options.duration;
+
+  config.horizontal.bounds = {
+    min: 0,
+    max: options.graphWidth - config.maxDim
+  };
+
+  config.vertical.bounds = {
+    min: 0,
+    max: options.graphHeight - config.maxDim
+  };
+
+  // configuration for horizontal and vertical scale handles
+  const handles = [
+    {
+      // name: 'scale horizontal lower bound',
+      x: referenceRange.x1(),
+      y: options.graphHeight,
+      offset: {
+        x: -config.strokeWidth,
+        y: -config.minDim / 2 - config.strokeWidth
+      },
+      width: config.maxDim,
+      height: config.minDim,
+      cls: 'h',
+      dimension: 'x',
+      max: options.graphWidth, // dynamic
+      min: 0,
+      duration: options.duration,
+      strokeWidth: config.strokeWidth,
+      symbolId: config.symbolId.scale,
+      type: 'rescale',
+      bounds: config.horizontal.bounds,
+      transform: config.horizontal.transform
+    },
+    {
+      // name: 'scale horizontal upper bound',
+      x: referenceRange.x2(),
+      y: options.graphHeight,
+      offset: {
+        x: -config.maxDim - config.strokeWidth,
+        y: -config.minDim / 2 - config.strokeWidth
+      },
+      width: config.maxDim,
+      height: config.minDim,
+      cls: 'h',
+      dimension: 'x',
+      max: options.graphWidth,
+      min: 0,
+      duration: options.duration,
+      strokeWidth: config.strokeWidth,
+      symbolId: config.symbolId.scale,
+      type: 'rescale',
+      bounds: config.horizontal.bounds,
+      transform: config.horizontal.transform
+    },
+    {
+      // name: 'scale vertical',
+      x: 0,
+      y: (options.graphHeight) / 2 - config.maxDim * 1.2, // Caution: scaling by magic number!
+      offset: {
+        x: config.minDim / 2 + config.strokeWidth,
+        y: -config.strokeWidth
+      },
+      width: config.minDim,
+      height: config.maxDim,
+      cls: 'v',
+      dimension: 'y',
+      max: options.graphHeight,
+      min: 0,
+      duration: options.duration,
+      strokeWidth: config.strokeWidth,
+      symbolId: config.symbolId.scale,
+      type: 'rescale',
+      bounds: config.vertical.bounds,
+      transform: config.vertical.transform
+    },
+    {
+      // name: 'size horizontal lower bound',
+      x: referenceRange.x1(),
+      y: 0,
+      offset: {
+        x: -config.size.radius.large - config.strokeWidth,
+        y: -config.size.radius.large - config.strokeWidth
+      },
+      width: config.size.radius.large * 2,
+      height: config.size.radius.radius * 2,
+      cls: 'h',
+      dimension: 'x',
+      max: options.graphWidth, // dynamic
+      min: 0,
+      duration: options.duration,
+      strokeWidth: config.strokeWidth,
+      symbolId: config.symbolId.size,
+      type: 'resize',
+      bounds: config.horizontal.bounds,
+      transform: config.horizontal.transform
+    },
+    {
+      // name: 'size horizontal upper bound',
+      x: referenceRange.x2(),
+      y: 0,
+      offset: {
+        x: -config.size.radius.large - config.strokeWidth,
+        y: -config.size.radius.large - config.strokeWidth
+      },
+      width: config.size.radius.radius * 2,
+      height: config.size.radius.radius * 2,
+      cls: 'h',
+      dimension: 'x',
+      max: options.graphWidth,
+      min: 0,
+      duration: options.duration,
+      strokeWidth: config.strokeWidth,
+      symbolId: config.symbolId.size,
+      type: 'resize',
+      bounds: config.horizontal.bounds,
+      transform: config.horizontal.transform
+    }
+  ];
+
+  const setGradientCoordinates = () => {
+    const graph = options.entries.find(entry => (entry.konkurrent.toUpperCase() === options.konkurrent));
+    const x1 = scales.xScale(solutionRange.lowerBound());
+    const x2 = scales.xScale(solutionRange.upperBound());
+    const y1 = scales.yScale(graph.koordinaten.find(k => (k.x === solutionRange.lowerBound())).y);
+    const y2 = scales.yScale(graph.koordinaten.find(k => (k.x === solutionRange.upperBound())).y);
+
+    gradient.x1(x1);
+    gradient.y1(y1);
+    gradient.x2(x2);
+    gradient.y2(y2);
+
+    solutionRange.y1(Math.min(y1, y2)); // hackety hack
+    solutionRange.y2(Math.max(y1, y2));
+
+    return [x1, y1, x2, y2];
+  };
+
+  const calculateGradient = (x1, y1, x2, y2) => {
+    // take inverse y axis into account
+    let m = round((y1 - y2) / (x2 - x1), 1);
+    gradient.m(m);
+
+    return m;
+  };
+
+  const updateGradient = (_selection) => {
+
+    const [x1, y1, x2, y2] = setGradientCoordinates();
+    const m = calculateGradient(x1, y1, x2, y2);
+
+    let isWithinRanges;
+
+    let sx1 = round(selectedRange.x1());
+    let sx2 = round(selectedRange.x2());
+    let tx1 = round(solutionRange.x1());
+    let tx2 = round(solutionRange.x2());
+    let miny = round(solutionRange.y1());
+    let maxy = round(solutionRange.y2());
+
+    if (sx1 <= tx1 && sx2 >= tx2 && miny >= 0 && maxy <= options.graphHeight) {
+      if (m >= solutionRange.gradient().min && m <= solutionRange.gradient().max) {
+        gradient.state('correct');
+        isWithinRanges = true;
+      } else {
+        gradient.state('active');
+        isWithinRanges = false;
+      }
+    } else {
+      gradient.state('inactive');
+      isWithinRanges = false;
+    }
+
+    gradient.update(_selection);
+
+    options.setDataMethod(isWithinRanges); // propagate state to (p)react application
+  };
+
+
+  const rangeController = (_selection) => {
+
+    selection = _selection;
+
+    const handleSymbols = createHandleSymbols(config);
+    selection.call(handleSymbols);
+
+    gradient = createGradient(scales, options);
+    setGradientCoordinates(); // initialise gradient
+    selection.call(gradient);
+    updateGradient(selection);
+
+    // scale handles
+    leftScaleHandle = createHandle(scales, handles[0], rangeController);
+    rightScaleHandle = createHandle(scales, handles[1], rangeController);
+    // scale handle connector
+    scaleHandleConnector = createHandleConnector(config, leftScaleHandle, rightScaleHandle);
+    selection.call(scaleHandleConnector);
+    selection.call(leftScaleHandle);
+    selection.call(rightScaleHandle);
+
+    verticalScaleHandle = createHandle(scales, handles[2], rangeController);
+    selection.call(verticalScaleHandle);
+
+    // size handles
+    leftSizeHandle = createHandle(scales, handles[3], rangeController);
+    rightSizeHandle = createHandle(scales, handles[4], rangeController);
+    // size handle connector
+
+    config.connectorHeight = 1;
+    sizeHandleConnector = createHandleConnector(config, leftSizeHandle, rightSizeHandle);
+    selection.call(sizeHandleConnector);
+    selection.call(leftSizeHandle);
+    selection.call(rightSizeHandle);
+  };
+
+
+  // set coordinate and update handle's representation
+  const updateHandle = (handle, value, withTransition = false) => {
+    if (handle.dimension() === 'x') {
+      handle.x(value);
+    } else if (handle.dimension === 'y') {
+      handle.y(value);
+    }
+    handle.update(withTransition);
+  };
+
+  const updateRange = (x1, x2, withTransition = false) => {
+    selectedRange.x1(x1);
+    selectedRange.x2(x2);
+    selectedRange.update(withTransition);
+  };
+
+
+  rangeController.onDragStart = (handle) => {
+
+    // set new constraints (bounds) for horizontal handles
+    if (handle.dimension() === 'x') {
+
+      if (handle === leftScaleHandle) {
+        const cx2 = rightScaleHandle.x() - handle.width() * 2;
+        handle.max(cx2);
+      }
+      if (handle === rightScaleHandle) {
+        handle.min(leftScaleHandle.x() + handle.width() * 2);
+      }
+    }
+  };
+
+  // reference scale (for initial reference selection)
+  const baseScale = () => {
+    const xScale = d3.scalePoint()
+      .domain(options.entries[0].koordinaten.map(item => item.x))
+      .range([ 0, options.graphWidth ]);
+
+    return xScale;
+  };
+
+  const referenceScale = baseScale();
+
+  // Update handles and selections with transitions
+  rangeController.onResized = (handle) => {
+
+    let x1;
+    let x2;
+    let rx1;
+    let rx2;
+    let idx; // target index
+    let lower = selectedRange.lowerBound();
+    let upper = selectedRange.upperBound();
+
+    const xDomain = scales.xScale.domain();
+    const xRange = scales.xScale.range();
+
+    let coords = d3.range(xRange[0], xRange[1] + scales.xScale.step(), scales.xScale.step());
+
+    if (handle === leftSizeHandle) {
+      x1 = handle.x();
+      x2 = selectedRange.x2();
+      idx = d3.bisect(coords, x1); // find grid line index 'to the right'
+
+      // snap to next lower grid line if the distance to that grid line is smaller
+      // and the corresponding coordinate lies within view port
+      if (coords[idx] - scales.xScale.step() / 2 > x1 && coords[idx - 1] > 0) {
+        idx -= 1;
+      }
+
+      if (idx < 0) { // index out of bounds?
+        idx = 0;
+      }
+
+      rx1 = referenceScale(xDomain[idx]);
+      x1 = scales.xScale(xDomain[idx]);
+
+      updateHandle(leftSizeHandle, x1, true);
+      updateHandle(leftScaleHandle, x1, true);
+
+      lower = xDomain[idx];
+      selectedRange.lowerBound(lower);
+      referenceRange.lowerBound(lower);
+      referenceRange.x1(rx1);
+
+    } else if (handle === rightSizeHandle) {
+
+      x1 = selectedRange.x1();
+      x2 = handle.x();
+      idx = d3.bisect(coords, x2); // find grid line index 'to the right'
+
+      // snap to next lower grid line if the distance to that grid line is smaller
+      // and the corresponding coordinate lies within view port
+      if (coords[idx] - scales.xScale.step() / 2 > x2 || coords[idx] > options.graphWidth) {
+        idx -= 1;
+      }
+
+      if (idx > xDomain.length - 1) { // index out of bounds?
+        idx = xDomain.length - 1;
+      }
+
+      rx2 = referenceScale(xDomain[idx]);
+      x2 = scales.xScale(xDomain[idx]);
+
+      updateHandle(rightSizeHandle, x2, true);
+      updateHandle(rightScaleHandle, x2, true);
+
+      upper = xDomain[idx];
+      selectedRange.upperBound(upper);
+      referenceRange.upperBound(upper);
+
+      referenceRange.x2(rx2);
+    }
+
+    updateRange(x1, x2, true);
+    updateGradient(selection);
+
+    scaleHandleConnector.update(true);
+    sizeHandleConnector.update(true);
+  };
+
+  rangeController.onRescalingX = (handle) => {
+
+    let x1;
+    let x2;
+    let graphRangeWidth;
+    let rSelectionWidth;
+    let cSelectionWidth;
+    let scaleFactor;
+    let rangeOffset;
+
+    if (handle === leftScaleHandle) {
+      x1 = handle.x();
+      x2 = selectedRange.x2();
+      updateHandle(leftSizeHandle, x1);
+    } else if (handle === rightScaleHandle) {
+      x1 = selectedRange.x1();
+      x2 = handle.x();
+      updateHandle(rightSizeHandle, x2);
+    }
+
+    cSelectionWidth = x2 - x1;
+    rSelectionWidth = referenceRange.x2() - referenceRange.x1();
+
+    scaleFactor = cSelectionWidth / rSelectionWidth;
+
+    graphRangeWidth = scaleFactor * options.graphWidth;
+    rangeOffset = scaleFactor * referenceRange.x1() - x1;
+
+    scales.xScale.range([-rangeOffset, graphRangeWidth - rangeOffset]);
+    axes.xAxis.call(d3.axisBottom(scales.xScale));
+
+    updateRange(x1, x2);
+
+    solutionRange.x1(scales.xScale(solutionRange.lowerBound()));
+    solutionRange.x2(scales.xScale(solutionRange.upperBound()));
+
+    scaleHandleConnector.update();
+    sizeHandleConnector.update();
+
+    dispatcher.call('scaling'); // inform grid and line graphs of scale event
+    updateGradient(selection);
+  };
+
+
+  rangeController.onRescalingY = (handle) => {
+
+    const scalemax = options.y.max * 2;
+    const scalemin = options.y.max / 2;
+    // Scaling based on y position of scale handle. Calculation should be improved / fixed. Interaction works but does not feel right
+    const cScaleMax = (scalemax - scalemin) / options.graphHeight * handle.y() + scalemin;
+
+    const min = options.y.min;
+    const max = cScaleMax;
+
+    scales.yScale.domain([min, max]);
+    axes.yAxis.call(d3.axisLeft(scales.yScale));
+
+    dispatcher.call('scaling');
+    updateGradient(selection);
+  };
+
+  rangeController.onResizingX = (handle) => {
+
+    let x1;
+    let x2;
+
+    if (handle === leftSizeHandle) {
+      x1 = handle.x();
+      x2 = selectedRange.x2();
+      updateHandle(leftScaleHandle, x1);
+    } else if (handle === rightSizeHandle) {
+      x1 = selectedRange.x1();
+      x2 = handle.x();
+      updateHandle(rightScaleHandle, x2);
+    }
+    updateRange(x1, x2);
+
+    scaleHandleConnector.update();
+    sizeHandleConnector.update();
+  };
+
+  return rangeController;
+};
+
+export default createRangeController;

+ 23 - 0
src/js/d3/scales.js

@@ -0,0 +1,23 @@
+import * as d3 from 'd3';
+
+// d3js module for module02: add scales
+export default (options) => {
+  // add x scale (point scale)
+  const xScale = d3.scalePoint()
+    .range([ 0, options.graphWidth ])
+    .domain(options.entries[0].koordinaten.map(item => item.x));
+
+  // determine y minimum and maximum value
+  const yMax = d3.max(options.entries, entry => (d3.max(entry.koordinaten, d => d.y)));
+  let yMin = d3.min(options.entries, entry => (d3.min(entry.koordinaten, d => d.y)));
+
+  yMin = yMin < 0 ? Math.floor(yMin) : 0;
+  options.y = { min: yMin, max: yMax };
+
+  // add y scale (linear scale)
+  const yScale = d3.scaleLinear()
+    .domain([ yMin, yMax ])
+    .range([ options.graphHeight, 0 ]); // inverse output range to draw 'upside down'
+
+  return { xScale, yScale };
+};

+ 13 - 0
src/js/main-offline.jsx

@@ -0,0 +1,13 @@
+import fonts from './utilities/fonts';
+import touch from './utilities/enableTouch';
+import Module from './components/Index.jsx';
+import { h, render } from 'preact'; // eslint-disable-line no-unused-vars
+import 'svgxuse';
+
+if ('visibilityState' in document) {
+  fonts(); // load fonts
+  touch(); // handle no-touch class
+
+  // Render my PREACT App
+  render(<Module isOffline={true} />, document.querySelector('body')); // eslint-disable-line react/jsx-boolean-value
+}

+ 13 - 0
src/js/main.jsx

@@ -0,0 +1,13 @@
+import fonts from './utilities/fonts';
+import touch from './utilities/enableTouch';
+import Module from './components/Index.jsx';
+import { h, render } from 'preact'; // eslint-disable-line no-unused-vars
+import 'svgxuse';
+
+if ('visibilityState' in document) {
+  fonts(); // load fonts
+  touch(); // handle no-touch class
+
+  // Render my PREACT App
+  render(<Module isOffline={false} />, document.querySelector('body'));
+}

+ 98 - 0
src/js/utilities/api.js

@@ -0,0 +1,98 @@
+import config from '../config.js';
+
+// api methods for get, post and put operations
+// as well as `getToken`, `createUser` and `endSession`
+const api = {
+
+  // GET data from API
+  get: (route, payload) => {
+    const requestHeaders = new Headers();
+    let requestString = payload ? '?' : '';
+    const myInit = {
+      headers: requestHeaders,
+      method: 'get'
+    };
+
+    requestString += payload ? Object.keys(payload).map(
+      key => `${encodeURIComponent(key)}=${encodeURIComponent(payload[key])}`
+    ).join('&') : '';
+
+    requestHeaders.append('Accept', 'application/json');
+
+    return fetch(config.api.url + route + requestString, myInit)
+      .then(response => response.json())
+      .catch((error) => {
+        throw error;
+      });
+  },
+
+  // POST data to API
+  post: (route, payload, token) => {
+    const requestHeaders = new Headers();
+    const myInit = {
+      headers: requestHeaders,
+      method: 'post',
+      body: JSON.stringify(payload)
+    };
+
+    requestHeaders.append('Content-Type', 'application/json');
+
+    if (typeof token !== 'undefined') {
+      requestHeaders.append('Authorization', `Bearer ${token}`);
+    }
+
+    return fetch(config.api.url + route, myInit)
+      .then(response => response.json())
+      .catch((error) => {
+        throw error;
+      });
+  },
+
+  // PUT data to API
+  put: (route, payload, token) => {
+    const requestHeaders = new Headers();
+    const myInit = {
+      headers: requestHeaders,
+      method: 'put',
+      body: JSON.stringify(payload)
+    };
+
+    requestHeaders.append('Content-Type', 'application/json');
+
+    if (typeof token !== 'undefined') {
+      requestHeaders.append('Authorization', `Bearer ${token}`);
+    }
+
+    return fetch(config.api.url + route, myInit)
+      .catch((error) => {
+        console.error(error); // eslint-disable-line
+      });
+  },
+
+  // request token from API for current user
+  getToken: () => {
+    const requestHeaders = new Headers();
+    const myInit = {
+      headers: requestHeaders,
+      method: 'post',
+      body: `grant_type=password&username=${config.api.user}&password=${config.api.pwd}`
+    };
+
+    requestHeaders.append('Content-Type', 'application/x-www-form-urlencoded');
+
+    return fetch('https://www.adaptivetoolbox.net/risikoatlas/token', myInit)
+      .then(response => response.json())
+      .then(json => json.access_token)
+      .catch((error) => {
+        throw error;
+      });
+  }
+};
+
+// create new user
+api.createUser = token => api.post('User/Create', {}, token);
+
+// end user session
+api.endSession = (userId, token) => api.put(`User/EndSession/${userId}`, {}, token);
+
+export default api;

+ 6 - 0
src/js/utilities/enableTouch.js

@@ -0,0 +1,6 @@
+// remove no-touch class for touch devices
+export default () => {
+  if ('ontouchstart' in document.documentElement) {
+    document.documentElement.classList.remove('no-touch');
+  }
+};

+ 32 - 0
src/js/utilities/fonts.js

@@ -0,0 +1,32 @@
+import Observer from 'fontfaceobserver';
+import promisesPolyfill from 'es6-promise';
+
+import config from '../config.js';
+
+// preload fonts
+export default () => {
+  const fontObservers = [];
+
+  Object.keys(config.fonts).forEach((f) => {
+    const font = config.fonts[f];
+
+    fontObservers.push(
+      new Observer(
+        font.family,
+        {
+          weight: font.weight,
+          style: font.style
+        }
+      ).load()
+    );
+  });
+
+  if (fontObservers.length >= 1) {
+    promisesPolyfill.polyfill();
+
+    Promise.all(fontObservers)
+      .then(() => {
+        document.documentElement.classList.add('fonts-loaded');
+      });
+  }
+};

+ 76 - 0
src/js/utilities/formatter.js

@@ -0,0 +1,76 @@
+// Formatter for number representations
+import round from './math';
+
+// get a fixed number of digits
+const fixedDigits = (number, numberDigits) => {
+  const str = `${number}`;
+  let padding = '';
+
+  for (let i = 0; i < numberDigits; i += 1) {
+    padding += '0';
+  }
+
+  return padding.substring(0, padding.length - str.length) + str;
+};
+
+const needsSpacing = (val1, val2) => {
+  let toBeSpaced = 0;
+
+  // Add additional spacing when one value has decimal places and the other not
+  if ((val1 < 1 && val2 >= 1) || (val1 >= 1 && val2 < 1)) {
+    toBeSpaced = 1;
+  }
+
+  return toBeSpaced;
+};
+
+// - 0 <= x < 1:        1 decimal place
+// - 1 <= x <= 100.000: 0 decimal places
+const getPrecision = (val) => {
+  if (val < 1) {
+    return 10;
+  }
+  return 1;
+};
+
+// - 0 <= x < 1:        0 spaces
+// - 1 <= x <= 100.000: 2 spaces
+const getSpacing = (val) => {
+  if (val >= 1) {
+    return 2;
+  }
+  return 0;
+};
+
+// - 0 <= x < 1:        1 decimal place
+// - 1 <= x <= 100.000: 0 decimal places
+const getFormattedValue = (val) => {
+
+  const digits = Math.log(val) / Math.log(10);
+  let formattedValue;
+
+  if (digits < 0) {
+    formattedValue = Math.max(round(val, 1), 0.1);
+  } else {
+    formattedValue = round(val, 0);
+  }
+
+  return formattedValue;
+};
+
+const haveSameRepresentation = (value1, value2) => {
+
+  const p1 = getFormattedValue(value1);
+  const p2 = getFormattedValue(value2);
+
+  return p1 === p2 || (p1 < 0.1 && p2 < 0.1);
+};
+
+export {
+  fixedDigits,
+  needsSpacing,
+  getPrecision,
+  getSpacing,
+  getFormattedValue,
+  haveSameRepresentation
+};

+ 6 - 0
src/js/utilities/math.js

@@ -0,0 +1,6 @@
+const round = (val, d = 3) => {
+  let factor = Math.pow(10, d);
+  return Math.round(val * factor) / factor;
+};
+
+export default round;

+ 15 - 0
src/js/utilities/randomizer.js

@@ -0,0 +1,15 @@
+// generate a random key (string)
+const generateKey = (prefix) => {
+  let text = '';
+  const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+
+  for (let i = 0; i < 32; i += 1) {
+    text += possible.charAt(Math.floor(Math.random() * possible.length));
+  }
+  return `${prefix}-${text}`;
+};
+
+
+export {
+  generateKey
+};

+ 19 - 0
src/scss/base/_fonts.scss

@@ -0,0 +1,19 @@
+// custom @font-face rules are automatically generated from font-config
+// ======================================================================
+
+@if variable-exists(font-config) {
+  @each $font-id, $font-definition in $font-config {
+    @if map-get($font-definition, fontface) == true {
+      $fontfile: map-get($font-definition, file);
+
+      @font-face {
+        font-family: map-get($font-definition, family);
+        font-weight: map-get($font-definition, weight);
+        font-style: map-get($font-definition, style);
+        // feel free to add other font-formats here
+        // if you need to support older browsers
+        src: url('../fonts/#{$fontfile}.woff2') format('woff2'), url('../fonts/#{$fontfile}.woff') format('woff');
+      }
+    }
+  }
+}

+ 52 - 0
src/scss/base/_forms.scss

@@ -0,0 +1,52 @@
+// base styles for form elements, fieldsets, labels, inputs etc.
+// ======================================================================
+
+// default transparent background for all form elements
+button,
+input,
+select,
+textarea {
+  background-color: transparent;
+}
+
+// default styles for text-input forms fields
+[type='text'],
+[type='tel'],
+[type='email'],
+[type='search'],
+[type='number'],
+[type='password'],
+select,
+textarea {
+  @include spacing-inner(t 1/4, r 1/2, b 1/4, l 1/2);
+  @include border-color(border);
+  width: 100%;
+  border-style: solid;
+  border-width: 1px;
+  border-radius: 0;
+  appearance: none; // no rounded inputs etc.
+
+  @include attention {
+    @include border-color(main);
+  }
+}
+
+[type='radio'] {
+  @extend %visuallyhidden;
+}
+
+legend {
+  @include font-size(h5);
+  @include spacing(b 2);
+  width: 100%;
+  text-align: center;
+}
+
+[type='number'] {
+  // sass-lint:disable-block no-vendor-prefixes
+  &::-webkit-inner-spin-button,
+  &::-webkit-outer-spin-button {
+    appearance: none;
+    margin: 0;
+  }
+}

+ 48 - 0
src/scss/base/_headings.scss

@@ -0,0 +1,48 @@
+// default headings h1 - h6
+// ======================================================================
+
+h1 {
+  @include z-index(feet);
+  @include font-size(h1);
+  font-weight: 300;
+}
+
+h2 {
+  @include font-size(h2);
+
+  .number--huge + & {
+    @include spacing(b 0);
+    @include spacing-inner(l 1);
+  }
+}
+
+h3 {
+  @include font-size(h3);
+  font-weight: 400;
+
+  .number--huge + & {
+    @include spacing(b 0);
+    @include spacing-inner(l 1);
+  }
+
+  .wrapper__fakenews & {
+    @include font-size(h5);
+  }
+}
+
+h4 {
+  @include font-size(h4);
+  font-weight: 300;
+
+  .wrapper__ofwhat & {
+    @include spacing(b .5);
+  }
+}
+
+h5 {
+  @include font-size(h5);
+}
+
+h6 {
+  @include font-size(h6);
+}

+ 12 - 0
src/scss/base/_links.scss

@@ -0,0 +1,12 @@
+// default link styling
+// ======================================================================
+
+a {
+  text-decoration: none;
+
+  .no-touch & {
+    @include attention {
+      text-decoration: underline;
+    }
+  }
+}

+ 24 - 0
src/scss/base/_rhythm.scss

@@ -0,0 +1,24 @@
+// default vertical rhythm / margin-bottom spacing
+// ======================================================================
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+ul,
+ol,
+dl,
+blockquote,
+p,
+hr,
+table,
+fieldset,
+figure,
+pre,
+.rhythm,
+%rhythm {
+  @include spacing;
+}
+

+ 30 - 0
src/scss/base/_root.scss

@@ -0,0 +1,30 @@
+// base styles for html, body and other global elements
+// ======================================================================
+
+html {
+  @include color(default);
+  @include background-color(background);
+  font-size: $base-font-size; // as reference for rem
+}
+
+body {
+  @include font(default);
+  @include background-color(background);
+
+  @include mediaquery(print) {
+    * {
+      display: none;
+    }
+
+    &::after {
+      content: 'Please, do not waste paper by printing webpages.';
+    }
+  }
+}
+
+// text selection styles
+::selection {
+  @include background-color(main);
+  @include color(inverted);
+  text-shadow: none;
+}

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.