Explorar el Código

Initial commit

Michael Zitzmann hace 5 años
commit
e5a410d973
Se han modificado 100 ficheros con 4709 adiciones y 0 borrados
  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. 144 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. 2 0
      doc/html/css/github-markdown.css
  16. 121 0
      doc/html/css/github-syntax-highlight.css
  17. 215 0
      doc/html/index.html
  18. 127 0
      gulpfile.babel.js
  19. 77 0
      package.json
  20. 9 0
      public/browserconfig.xml
  21. BIN
      public/favicon.ico
  22. 5 0
      public/robots.txt
  23. 492 0
      readme.md
  24. BIN
      src/fonts/roboto-light.woff
  25. BIN
      src/fonts/roboto-light.woff2
  26. BIN
      src/fonts/roboto-medium.woff
  27. BIN
      src/fonts/roboto-medium.woff2
  28. BIN
      src/fonts/roboto-regular.woff
  29. BIN
      src/fonts/roboto-regular.woff2
  30. BIN
      src/fonts/roboto-thin.woff
  31. BIN
      src/fonts/roboto-thin.woff2
  32. BIN
      src/fonts/robotomono-light.woff
  33. BIN
      src/fonts/robotomono-light.woff2
  34. 31 0
      src/html/index.html
  35. BIN
      src/img/android-chrome-192x192.png
  36. BIN
      src/img/android-chrome-512x512.png
  37. BIN
      src/img/apple-touch-icon.png
  38. BIN
      src/img/favicon-16x16.png
  39. BIN
      src/img/favicon-32x32.png
  40. BIN
      src/img/mstile-150x150.png
  41. 3 0
      src/img/safari-pinned-tab.svg
  42. 1 0
      src/img/sprites.svg
  43. 3 0
      src/img/sprites/correct.svg
  44. 3 0
      src/img/sprites/incorrect.svg
  45. 15 0
      src/img/sprites/sprites.yaml
  46. 1 0
      src/img/sprites/triangle.svg
  47. 80 0
      src/js/components/ChartScreen.jsx
  48. 18 0
      src/js/components/FinalScreen.jsx
  49. 215 0
      src/js/components/Index.jsx
  50. 18 0
      src/js/components/TitleScreen.jsx
  51. 67 0
      src/js/components/partials/GraphItem.jsx
  52. 12 0
      src/js/components/partials/HeaderLightItem.jsx
  53. 46 0
      src/js/config.js
  54. 260 0
      src/js/content/group-a.json
  55. 10 0
      src/js/content/module.json
  56. 3 0
      src/js/content/offline.js
  57. 46 0
      src/js/content/questions.json
  58. 96 0
      src/js/d3/axes.js
  59. 210 0
      src/js/d3/draw.js
  60. 27 0
      src/js/d3/grid.js
  61. 37 0
      src/js/d3/legend.js
  62. 31 0
      src/js/d3/line.js
  63. 135 0
      src/js/d3/main.js
  64. 6 0
      src/js/d3/message.js
  65. 8 0
      src/js/d3/point.js
  66. 26 0
      src/js/d3/scales.js
  67. 32 0
      src/js/d3/userline.js
  68. 13 0
      src/js/main-offline.jsx
  69. 13 0
      src/js/main.jsx
  70. 98 0
      src/js/utilities/api.js
  71. 6 0
      src/js/utilities/enableTouch.js
  72. 32 0
      src/js/utilities/fonts.js
  73. 6 0
      src/js/utilities/math.js
  74. 19 0
      src/scss/base/_fonts.scss
  75. 52 0
      src/scss/base/_forms.scss
  76. 48 0
      src/scss/base/_headings.scss
  77. 12 0
      src/scss/base/_links.scss
  78. 24 0
      src/scss/base/_rhythm.scss
  79. 30 0
      src/scss/base/_root.scss
  80. 17 0
      src/scss/config/_breakpoints.scss
  81. 32 0
      src/scss/config/_colors.scss
  82. 37 0
      src/scss/config/_defaults.scss
  83. 73 0
      src/scss/config/_fonts.scss
  84. 62 0
      src/scss/main.scss
  85. 21 0
      src/scss/modules/_animations.scss
  86. 115 0
      src/scss/modules/_answers.scss
  87. 29 0
      src/scss/modules/_boxes.scss
  88. 55 0
      src/scss/modules/_buttons.scss
  89. 32 0
      src/scss/modules/_content.scss
  90. 208 0
      src/scss/modules/_d3js.scss
  91. 10 0
      src/scss/modules/_debug.scss
  92. 11 0
      src/scss/modules/_footer.scss
  93. 92 0
      src/scss/modules/_forms.scss
  94. 45 0
      src/scss/modules/_grids.scss
  95. 35 0
      src/scss/modules/_header.scss
  96. 31 0
      src/scss/modules/_icons.scss
  97. 2 0
      src/scss/modules/_logos.scss
  98. 36 0
      src/scss/modules/_navs.scss
  99. 92 0
      src/scss/modules/_questions.scss
  100. 20 0
      src/scss/modules/_score.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
+      - 145
+    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': 0,
+        '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 3: Trends schätzen](#modul-3-trends-schaetzen)
+- [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
+
+```

+ 144 - 0
doc/03_module.md

@@ -0,0 +1,144 @@
+## Modul 3: Trends schätzen
+
+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
+│   ├── ChartScreen.jsx
+│   ├── FinalScreen.jsx
+│   ├── Index.jsx
+│   ├── TitleScreen.jsx
+│   └── partials
+│       ├── GraphItem.jsx     // Komponente für d3 Visualisierungen
+│       └── HeaderLightItem.jsx
+├── content
+│   ├── group-a.json          // Daten zur Aufgabenstellung: Graphen-Definitionen
+│   ├── group-b.json          // Daten zur Aufgabenstellung: Graphen-Definitionen
+│   ├── module.json           // Labels und Texten des User Interfaces
+│   └── offline.js            // Definition der Reihenfolge der Fragen
+│   └── questions.json
+├── d3
+│   ├── axes.js               // Achsendefinitionen
+│   ├── draw.js               // Interaktives Zeichnen von Graphen
+│   ├── grid.js               // Statisches Hintergrundraster
+│   ├── legend.js             // Legende eines Graphen
+│   ├── line.js               // Darstellung eines Graphen
+│   ├── main.js               // d3 Haupt-Modul
+│   ├── message.js            // Benachrichtigung, wenn der Graph nicht über das komplette Intervall gezeichnet wurde
+│   ├── point.js              // Hervorgehobener Orientierungspunkt in interaktiver Ansicht
+│   ├── scales.js             // Definition der d3 Skalen für x- und y-Achsen
+│   └── userline.js           // Statische Darstellung des interaktiv gezeichneten Graphen in Vergleichsansichten
+└── utilities
+    ├── api.js                // API für Lese- und Schreibzugriff auf die Datenbank
+    ├── enableTouch.js
+    ├── math.js
+    └── fonts.js
+```
+
+### Wie ändere ich Inhalte und Daten?
+
+Alle Inhalte und Daten der 'offline'-Version sind in den `json`-Dateien Dateien unter `src/js/content` definiert. Die Datei `questions.json` ist aber sowohl für die 'offline' als auch für die 'online' Version zwingend notwendig. Dort sind hauptächlich Labels der Fragen, aber auch Metadaten definiert. Für das Hinzufügen oder Ändern von Fragen sind sowohl die Texte und Labels der Frage (`questions.json`) und die Definition der Daten der Frage (`group-<x>.json`) anzupassen. Die Definition der Reihenfolge der Fragen (`offline.js`) ist nur für den 'offline' Modus relevant, im 'online' Modus wird die Reihenfolge von der Datenbank / API bestimmt.
+
+#### 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 den Text der Fehlermeldung bei einem nur teilweise gezeichneten Graphen.
+
+    {
+      "title": "Wie stark hängen der Wert von Immobilien und die Einkommensentwicklung zusammen?",
+      "introtext": "In der Diskussion um die Einkommensentwicklung im Kontext steigender Mieten, …",
+      "outrotext": "Sie haben gleich zwei historische Entwicklungen verinnerlicht. …",
+      "next": "Weiter",
+      "start": "Start",
+      "restart": "Neustart",
+      "error": "Achtung, da fehlt noch was!",
+      "done": "Wenn Sie fertig sind, klicken Sie bitte auf weiter!"
+    }
+
+##### Labels der WebApp
+
+Die Texte und Labels und teilweise die Daten zu den Fragen sind in `src/js/content/questions.json` definiert:
+
+    {
+      "source": "Quelle: Mack, A., and E. Martínez-García. 2011. …",
+      "texts": [
+        {
+          "text": "Schätzen Sie zunächst bitte, …",
+          "instruction": "Bitte fahren Sie über …",
+          "hint": "Einen Punkt haben wir Ihnen …",
+          "answer1": "Nicht schlecht! …",
+          "answer2": "Sie sehen jetzt im Vergleich …"
+        },
+        {
+          "text": "Den Anstieg der Immobilien behalten wir. …",
+          "instruction": "Bitte fahren Sie wieder …",
+          "hint": "Einen Punkt haben wir Ihnen …",
+          "answer1": "Nicht schlecht! …",
+          "answer2": "Sie sehen jetzt …"
+        }
+      ],
+      "fixedPoints": [
+        {
+          "jahr": 2005,
+          "index": 100
+        },
+        {
+          "jahr": 2005,
+          "einkommen": 100
+        }
+      ],
+      "yKeys": [
+        "index",
+        "einkommen"
+      ],
+      "yAxisUnit": [
+        "Immobilienindex",
+        "Einkommen"
+      ],
+      "legendLabels": [
+        "Immobilienpreis",
+        "Einkommen",
+        "Durchschnitt"
+      ],
+      "xKey": "jahr",
+      "xAxisUnit": "Jahr"
+    }
+
+Eine Frage kann aus mehreren Teilen bestehen, in diesem Fall sind es zwei Teile, die durch die Objekte im Array `texts` definiert werden. Im Fall der 'offline' Version werden die Texte der Felder `answer2` nicht verwendet.
+`yKeys` bezieht sich auf den Datensatz und welcher Key dort für den jeweiligen Aufgbenteil verwendet werden soll. [*Diese Idee ist hundsmiserabel und man hätte das Ganze den Entwicklern um die Ohren hauen müssen, die Struktur und die Namensgebung der Labels ist fürchterlich, aber durch die entsprechende Implementierung in der Datenbank / API zumindest zunächst betoniert.*]
+
+Besser ist die Tatsache, dass die Achsenbeschriftungen für die Aufgabenteile (`yAxisUnit`, `xAxisUnit`) und die Labels der Legende (`legendlabels`) angepasst werden können.
+
+Das Objekt `fixedPoints` enthält die Koordinaten des Hilfspunktes, der in der interaktiven Ansicht angezeigt wird.
+
+#### Daten
+
+##### offline
+
+Die Datei `group-a.json` in `src/js/content` enthält Daten der folgenden Struktur:
+
+    {
+      "einkommen": 65.21,
+      "id": 1,
+      "index": 108.58,
+      "jahr": 1975
+    }
+
+Dieses Objekt beschreibt für einen x-Wert ("jahr": 1975) die y-Werte für **zwei** verschiedene Graphen. Diese Behandlung der Daten schreit nach einer Überarbeitung des Codes, da hier Daten und (Re)präsentation unnötigerweise und auf komplizierte Art miteinander verwurschelt werden.
+Wie im vorhergehenden Abschnitt beschrieben, werden in `question.json` die *Keys* und *Labels* definiert, die für einen Frageabschnitt verwendet werden sollen. In diesem Beispiel bezieht sich `xKey` auf `"jahr"`, `yKeys` je nach Frageabschnitt auf `"index"` bzw. `"einkommen".
+
+##### online
+
+<API-Dokumentation>
+

+ 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

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 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;
+}

+ 215 - 0
doc/html/index.html

@@ -0,0 +1,215 @@
+<!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="conventions">Conventions</h2>
+<h3 id="coding-style-linting">Coding Style &amp; Linting</h3>
+<p>If your editor of choice supports <a href="http://editorconfig.org">editorconfig</a>, you should already be fine, this project provides an .editorconfig-file that sets all conventions.</p>
+<p>If not: 2 Spaces for <em>everything</em>, UTF-8 everything, Unix-Style linebreaks, no trailing spaces except for Markdown and text-files.</p>
+<p>The JavaScript-, HTML- and Scss-linters will be enabled by default with the settings from above, so if you try to move away from those conventions: you will be warned :) The JavaScript- and Scss-linters will try to enforce a specific coding style regarding whitespace, naming stuff, usage of variables etc. to make your code consistent, so please try to stick to it.</p>
+<p><a href="#linting">More on configuring and using the linters</a></p>
+<h3 id="filenames">Filenames</h3>
+<p>Files should always be in lowercase, use dashes or double-dashes where applicable. No underscores, except as a starting character for sass-partials.</p>
+<p>+++</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                 // 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 &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 [<code>nodejs</code>] unter Verwendung von [<code>npm</code>] 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>. [<code>gulp</code>] wird dabei als Task-Manager dieses Projekts global installiert.</p>
+<p>Für das Erstellen der Dokumentation aus den einzelnen <em>Markdown</em>-Dateien, die im Verzeichnis <code>doc/</code> liegen, wird <code>[pandoc]</code> verwendet. Dieses ist für viele Betriebssysteme und Distributionen verfügbar, muss aber gesondert installiert werden.</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="erstellen-der-dokumentation">Erstellen der Dokumentation</h3>
+<p>Die Dokumentation in einzelne <em>Markdown</em>-Dateien aufgeteilt, die im Verzeichnis <code>doc/</code> liegen. Zum Erstellen einer zusammenhängender Dokumentation sind folgende <code>npm</code> Scripts definiert, die auf <code>pandoc</code> basieren:</p>
+<pre><code>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</code></pre>
+<h3 id="konventionen">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 [editorconfig]. 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 [eslint] durchgeführt.Für Sass wird [sass-lint] verwendet und HTML wird mit Hilfe von [HTMLHint] überprüft.</p>
+<p>Die zugehörigen Konfigurationsdateien befinden sich im Wurzelverzeichnis, 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</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">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
+};

+ 77 - 0
package.json

@@ -0,0 +1,77 @@
+{
+  "name": "estimating-trends",
+  "version": "1.0.0",
+  "author": "Tabea David <tabea.david@kf-interactive.com>, zitzmann@mpib-berlin.mpg.de",
+  "browserslist": [
+    "last 2 versions",
+    "ie >= 11"
+  ],
+  "devDependencies": {
+    "@babel/core": "^7.10.1",
+    "@babel/plugin-proposal-object-rest-spread": "^7.10.1",
+    "@babel/plugin-transform-object-assign": "^7.10.1",
+    "@babel/plugin-transform-react-jsx": "^7.10.1",
+    "@babel/preset-env": "^7.10.1",
+    "@babel/preset-react": "^7.10.1",
+    "@babel/register": "^7.10.1",
+    "@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.11.2",
+    "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:

+ 492 - 0
readme.md

@@ -0,0 +1,492 @@
+Inhalt
+------
+
+-   [Einführung](#einführung)
+-   [Übersicht der Module](#übersicht-der-module)
+-   [Verzeichnisstruktur](#verzeichnisstruktur)
+-   [Modul 3: Trends schätzen](#modul-3-trends-schaetzen)
+-   [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 3: Trends schätzen
+------------------------
+
+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
+    │   ├── ChartScreen.jsx
+    │   ├── FinalScreen.jsx
+    │   ├── Index.jsx
+    │   ├── TitleScreen.jsx
+    │   └── partials
+    │       ├── GraphItem.jsx     // Komponente für d3 Visualisierungen
+    │       └── HeaderLightItem.jsx
+    ├── content
+    │   ├── group-a.json          // Daten zur Aufgabenstellung: Graphen-Definitionen
+    │   ├── group-b.json          // Daten zur Aufgabenstellung: Graphen-Definitionen
+    │   ├── module.json           // Labels und Texten des User Interfaces
+    │   └── offline.js            // Definition der Reihenfolge der Fragen
+    │   └── questions.json
+    ├── d3
+    │   ├── axes.js               // Achsendefinitionen
+    │   ├── draw.js               // Interaktives Zeichnen von Graphen
+    │   ├── grid.js               // Statisches Hintergrundraster
+    │   ├── legend.js             // Legende eines Graphen
+    │   ├── line.js               // Darstellung eines Graphen
+    │   ├── main.js               // d3 Haupt-Modul
+    │   ├── message.js            // Benachrichtigung, wenn der Graph nicht über das komplette Intervall gezeichnet wurde
+    │   ├── point.js              // Hervorgehobener Orientierungspunkt in interaktiver Ansicht
+    │   ├── scales.js             // Definition der d3 Skalen für x- und y-Achsen
+    │   └── userline.js           // Statische Darstellung des interaktiv gezeichneten Graphen in Vergleichsansichten
+    └── utilities
+        ├── api.js                // API für Lese- und Schreibzugriff auf die Datenbank
+        ├── enableTouch.js
+        ├── math.js
+        └── fonts.js
+
+### Wie ändere ich Inhalte und Daten?
+
+Alle Inhalte und Daten der 'offline'-Version sind in den `json`-Dateien
+Dateien unter `src/js/content` definiert. Die Datei `questions.json` ist
+aber sowohl für die 'offline' als auch für die 'online' Version zwingend
+notwendig. Dort sind hauptächlich Labels der Fragen, aber auch Metadaten
+definiert. Für das Hinzufügen oder Ändern von Fragen sind sowohl die
+Texte und Labels der Frage (`questions.json`) und die Definition der
+Daten der Frage (`group-<x>.json`) anzupassen. Die Definition der
+Reihenfolge der Fragen (`offline.js`) ist nur für den 'offline' Modus
+relevant, im 'online' Modus wird die Reihenfolge von der Datenbank / API
+bestimmt.
+
+#### 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 den Text der Fehlermeldung bei einem nur teilweise
+gezeichneten Graphen.
+
+    {
+      "title": "Wie stark hängen der Wert von Immobilien und die Einkommensentwicklung zusammen?",
+      "introtext": "In der Diskussion um die Einkommensentwicklung im Kontext steigender Mieten, …",
+      "outrotext": "Sie haben gleich zwei historische Entwicklungen verinnerlicht. …",
+      "next": "Weiter",
+      "start": "Start",
+      "restart": "Neustart",
+      "error": "Achtung, da fehlt noch was!",
+      "done": "Wenn Sie fertig sind, klicken Sie bitte auf weiter!"
+    }
+
+##### Labels der WebApp
+
+Die Texte und Labels und teilweise die Daten zu den Fragen sind in
+`src/js/content/questions.json` definiert:
+
+    {
+      "source": "Quelle: Mack, A., and E. Martínez-García. 2011. …",
+      "texts": [
+        {
+          "text": "Schätzen Sie zunächst bitte, …",
+          "instruction": "Bitte fahren Sie über …",
+          "hint": "Einen Punkt haben wir Ihnen …",
+          "answer1": "Nicht schlecht! …",
+          "answer2": "Sie sehen jetzt im Vergleich …"
+        },
+        {
+          "text": "Den Anstieg der Immobilien behalten wir. …",
+          "instruction": "Bitte fahren Sie wieder …",
+          "hint": "Einen Punkt haben wir Ihnen …",
+          "answer1": "Nicht schlecht! …",
+          "answer2": "Sie sehen jetzt …"
+        }
+      ],
+      "fixedPoints": [
+        {
+          "jahr": 2005,
+          "index": 100
+        },
+        {
+          "jahr": 2005,
+          "einkommen": 100
+        }
+      ],
+      "yKeys": [
+        "index",
+        "einkommen"
+      ],
+      "yAxisUnit": [
+        "Immobilienindex",
+        "Einkommen"
+      ],
+      "legendLabels": [
+        "Immobilienpreis",
+        "Einkommen",
+        "Durchschnitt"
+      ],
+      "xKey": "jahr",
+      "xAxisUnit": "Jahr"
+    }
+
+Eine Frage kann aus mehreren Teilen bestehen, in diesem Fall sind es
+zwei Teile, die durch die Objekte im Array `texts` definiert werden. Im
+Fall der 'offline' Version werden die Texte der Felder `answer2` nicht
+verwendet. `yKeys` bezieht sich auf den Datensatz und welcher Key dort
+für den jeweiligen Aufgbenteil verwendet werden soll. \[*Diese Idee ist
+hundsmiserabel und man hätte das Ganze den Entwicklern um die Ohren
+hauen müssen, die Struktur und die Namensgebung der Labels ist
+fürchterlich, aber durch die entsprechende Implementierung in der
+Datenbank / API zumindest zunächst betoniert.*\]
+
+Besser ist die Tatsache, dass die Achsenbeschriftungen für die
+Aufgabenteile (`yAxisUnit`, `xAxisUnit`) und die Labels der Legende
+(`legendlabels`) angepasst werden können.
+
+Das Objekt `fixedPoints` enthält die Koordinaten des Hilfspunktes, der
+in der interaktiven Ansicht angezeigt wird.
+
+#### Daten
+
+##### offline
+
+Die Datei `group-a.json` in `src/js/content` enthält Daten der folgenden
+Struktur:
+
+    {
+      "einkommen": 65.21,
+      "id": 1,
+      "index": 108.58,
+      "jahr": 1975
+    }
+
+Dieses Objekt beschreibt für einen x-Wert ("jahr": 1975) die y-Werte für
+**zwei** verschiedene Graphen. Diese Behandlung der Daten schreit nach
+einer Überarbeitung des Codes, da hier Daten und (Re)präsentation
+unnötigerweise und auf komplizierte Art miteinander verwurschelt werden.
+Wie im vorhergehenden Abschnitt beschrieben, werden in `question.json`
+die *Keys* und *Labels* definiert, die für einen Frageabschnitt
+verwendet werden sollen. In diesem Beispiel bezieht sich `xKey` auf
+`"jahr"`, `yKeys` je nach Frageabschnitt auf `"index"` bzw.
+\`"einkommen".
+
+##### online
+
+`<API-Dokumentation>`{=html}
+
+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.

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 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"/></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 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"/></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>

+ 80 - 0
src/js/components/ChartScreen.jsx

@@ -0,0 +1,80 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import HeaderLightItem from './partials/HeaderLightItem.jsx';
+import GraphItem from './partials/GraphItem.jsx';
+
+// displays the current Chart Screen depending on mode
+export default class ChartScreen extends Component {
+
+  // RENDER
+  render () {
+    const question = this.props.questions[this.props.currentQuestion];
+    let textKey;
+
+    // get text depending on mode
+    switch (this.props.mode) {
+      case 'presentation':
+        textKey = 'answer1';
+        break;
+
+      case 'comparison':
+        textKey = 'answer2';
+        break;
+
+      default:
+        textKey = 'text';
+        break;
+    }
+
+    // ... combine and render it
+    return (
+      <section className="wrapper  wrapper__question">
+        <HeaderLightItem { ...this.props } />
+
+        <main className="wrapper__main  wrapper--centered  wrapper__main--question">
+          <div className="question  question__manipula">
+            <p className="question__title">
+              {question.texts[this.props.currentStep][textKey].split('\n').map(item => <span>{item}<br /></span>)}
+            </p>
+
+            { this.props.mode === 'interaction'
+              ? <p className="question__instruction">{question.texts[this.props.currentStep].instruction}</p> : []
+            }
+
+            { question.texts[this.props.currentStep].hint && this.props.mode === 'interaction'
+              ? <p className="question__hint">
+                {question.texts[this.props.currentStep].hint.split('\n').map(item => <span>{item}<br /></span>)}
+              </p>
+              : []
+            }
+          </div>
+
+          <div className="question__message">
+            <div className="question__message--error" aria-hidden="true">{this.props.error}</div>
+            <div className="question__message--done" aria-hidden="true">{this.props.done}</div>
+          </div>
+
+          <GraphItem
+            interactionData={this.props.interactionData}
+            comparisonData={this.props.comparisonData}
+            currentStep={this.props.currentStep}
+            currentQuestion={this.props.currentQuestion}
+            data={this.props.data}
+            mode={this.props.mode}
+            questions={this.props.questions}
+            setData={this.props.setData} />
+
+          <div className="question__source">{question.source}</div>
+        </main>
+
+        {this.props.mode !== 'interaction' || this.props.interactionData.length > 0
+          ? <footer className="footer footer--chart">
+            <a href="#" title={this.props.next} className="button--wide" onClick={() => this.props.toNextStep()}>
+              {this.props.next}
+            </a>
+          </footer>
+          : []
+        }
+      </section>
+    );
+  }
+}

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

@@ -0,0 +1,18 @@
+import { h, render } from 'preact'; // eslint-disable-line no-unused-vars
+
+// shared 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">
+      <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;

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

@@ -0,0 +1,215 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+// Content
+import content from '../content/module.json';
+import questions from '../content/questions.json';
+import data from '../content/offline';
+// Configuration
+import config from '../config';
+// Services
+import api from '../utilities/api';
+// Views
+import TitleScreen from './TitleScreen.jsx';
+import FinalScreen from './FinalScreen.jsx';
+import ChartScreen from './ChartScreen.jsx';
+
+/**
+ * titlescreen -> chartscreen (*n) -> finalscreen
+ */
+export default class App extends Component {
+
+  // Initialize functions
+  constructor (props) {
+    super(props);
+    this.state = {
+      route: 'titlescreen',       // Current screen
+      currentQuestion: 0,         // Current question
+      currentStep: 0,             // Current graph task
+      chartMode: 'interaction',   // Mode for graph 'interaction', 'presentation' or  'comparison'
+      questions: [],              // Wrapper for all questions
+      data: [],                   // Global data wrapper
+      interactionData: [],        // Wrapper for interaction data
+      comparisonData: [],         // Wrapper for comparison data
+      isFetching: false,          // Currently performing XHR?
+      userId: null,               // Current user id for posting
+      token: null                 // Current access token
+    };
+
+    // Context binding
+    this.navigate = this.navigate.bind(this);
+    this.toNextStep = this.toNextStep.bind(this);
+    this.setup = this.setup.bind(this);
+    this.setData = this.setData.bind(this);
+  }
+
+  // Initial setup, get accesToken and userId
+  setup () {
+    this.setState({ questions });
+    if (this.props.isOffline) {
+      this.setState({ data });
+    } 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();
+          });
+      });
+    }
+  }
+
+  // Navigate to specific route
+  navigate (route) {
+    if (route !== 'titlescreen') {
+      this.setState({ route });
+    } else {
+      location.reload();
+    }
+  }
+
+  // Set data
+  setData (newData) {
+    this.setState({ interactionData: newData });
+  }
+
+  // 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 () {
+    let newData = [ ...this.state.data ];
+    api.get(config.api.lists.a)
+      .then(json => {
+        newData[0] = json;
+      // })
+      // .then(() => api.get(config.api.lists.b))
+      // .then(json => {
+      //   newData[1] = json;
+        this.setState({ data: newData });
+      })
+      .catch(error => {
+        console.error(error); // eslint-disable-line
+      });
+  }
+
+  // Update state depending on chart mode and current question
+  toNextStep () {
+    const nextQuestion = this.state.currentQuestion < this.state.questions.length - 1;
+    const nextStep = this.state.currentStep < this.state.questions[this.state.currentQuestion].texts.length - 1;
+
+    if (this.state.chartMode === 'interaction') { // If in interaction mode, go to presentation mode
+
+      if (!this.props.isOffline) {
+        api.post(
+          this.state.currentQuestion === 0 ? config.api.creates.a : config.api.creates.b,
+          {
+            userId: this.state.userId,
+            kontext: this.state.questions[this.state.currentQuestion].yKeys[this.state.currentStep],
+            punkte: this.state.interactionData
+          },
+          this.state.token
+        )
+          .then(() => {
+            this.setState({ chartMode: 'presentation' });
+          });
+      } else {
+        this.setState({ chartMode: 'presentation' });
+      }
+
+    } else if (this.state.chartMode === 'presentation' && !this.props.isOffline) { // If 'online' and in presentation mode, go to comparison mode
+
+      api.get(
+        this.state.currentQuestion === 0 ? config.api.proportions.a : config.api.proportions.b,
+        { kontext: this.state.questions[this.state.currentQuestion].yKeys[this.state.currentStep] })
+        .then(json => {
+          this.setState({ chartMode: 'comparison', comparisonData: json });
+        });
+
+    } else if (nextStep) {
+
+      this.setState({
+        chartMode: 'interaction',
+        currentStep: this.state.currentStep + 1,
+        interactionData: [],
+        comparisonData: []
+      });
+
+    } else if (nextQuestion) {
+
+      this.setState({
+        chartMode: 'interaction',
+        currentQuestion: this.state.currentQuestion + 1,
+        currentStep: 0,
+        interactionData: [],
+        comparisonData: []
+      });
+
+    } else {
+      // Go to finalscreen
+      this.setState({ route: 'finalscreen' });
+    }
+  }
+
+  // LIFECYCLE
+  componentDidMount () {
+    this.setup();
+  }
+
+  // RENDER
+  render () {
+    let outputContent;
+
+    switch (this.state.route) {
+
+      case 'question':
+        outputContent = <ChartScreen
+          {...content}
+          data={this.state.data}
+          comparisonData={this.state.comparisonData}
+          interactionData={this.state.mode === 'interaction' ? [] : this.state.interactionData}
+          mode={this.state.chartMode}
+          route={this.state.route}
+          questions={this.state.questions}
+          currentQuestion={this.state.currentQuestion}
+          currentStep={this.state.currentStep}
+          setData={this.setData}
+          toNextStep={this.toNextStep} />;
+        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;
+  }
+
+}

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

@@ -0,0 +1,18 @@
+import { h, render } from 'preact'; // eslint-disable-line no-unused-vars
+
+// shared 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">
+      <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;

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

@@ -0,0 +1,67 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import createGraph 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
+  updateData (replace) {
+    const entries = this.props.data[this.props.currentQuestion];
+    const question = this.props.questions[this.props.currentQuestion];
+
+    const graph = createGraph().options({
+      entries,
+      userEntries: this.props.interactionData,
+
+      comparisonData: this.props.comparisonData,
+      xKey: question.xKey,
+      xAxisUnit: question.xAxisUnit,
+      yKey: question.yKeys[this.props.currentStep],
+      yKeys: question.yKeys,
+      yAxisUnit: question.yAxisUnit[this.props.currentStep],
+      legendLabels: question.legendLabels,
+      fixedPoint: question.fixedPoints[this.props.currentStep],
+      mode: this.props.mode,
+      currentStep: this.props.currentStep,
+      currentQuestion: this.props.currentQuestion,
+      setDataMethod: this.props.setData
+    });
+
+    if (replace === true) {
+      this.container.innerHTML = ''; // clear container
+      d3.select(this.container).call(graph);
+    }
+
+  }
+
+  // LIFECYCLE methods
+  shouldComponentUpdate (nextProps) {
+    if (this.props.mode === 'interaction' && nextProps.mode === 'interaction') {
+      return false;
+    }
+    return true;
+  }
+
+  componentDidMount () {
+    this.updateData(true);
+  }
+
+  componentDidUpdate () {
+    this.updateData(true);
+  }
+
+  componentWillReceiveProps () {
+    if (this.props.mode !== 'interaction') {
+      this.updateData();
+    }
+  }
+
+  // output entry point for d3js module
+  render () {
+    return (
+      <div className="chart" ref={ elem => (this.container = elem) }></div>
+    );
+  }
+
+}

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

@@ -0,0 +1,12 @@
+import { h, render } from 'preact'; // eslint-disable-line no-unused-vars
+
+// shared 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>
+    <h1 className="header__title--light">{props.title}</h1>
+  </header>
+);
+
+export default HeaderLightItem;

+ 46 - 0
src/js/config.js

@@ -0,0 +1,46 @@
+// basic config containing global api base URL
+export default {
+  numberQuestions: 12,
+  api: {
+    lists: {
+      a: 'M3a_Immobilien/Liste',
+      b: 'M3b_Zufriedenheit/Liste'
+    },
+    creates: {
+      a: 'M3a_ImmobilienUserData/Create',
+      b: 'M3b_ZufriedenheitUserData/Create'
+    },
+    proportions: {
+      a: 'M3a_ImmobilienUserData/Points',
+      b: 'M3b_ZufriedenheitUserData/Points'
+    },
+    url: 'https://www.adaptivetoolbox.net/risikoatlas/api/'
+  },
+  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'
+    }
+  }
+};

+ 260 - 0
src/js/content/group-a.json

@@ -0,0 +1,260 @@
+[
+  {
+    "einkommen": 65.21,
+    "id": 1,
+    "index": 108.58,
+    "jahr": 1975
+  },
+  {
+    "einkommen": 66.8,
+    "id": 2,
+    "index": 109.89,
+    "jahr": 1976
+  },
+  {
+    "einkommen": 68.26,
+    "id": 3,
+    "index": 113.74,
+    "jahr": 1977
+  },
+  {
+    "einkommen": 71.0,
+    "id": 4,
+    "index": 121.48,
+    "jahr": 1978
+  },
+  {
+    "einkommen": 73.35,
+    "id": 5,
+    "index": 126.79,
+    "jahr": 1979
+  },
+  {
+    "einkommen": 73.59,
+    "id": 6,
+    "index": 123.7,
+    "jahr": 1980
+  },
+  {
+    "einkommen": 72.57,
+    "id": 7,
+    "index": 125.97,
+    "jahr": 1981
+  },
+  {
+    "einkommen": 70.73,
+    "id": 8,
+    "index": 125.95,
+    "jahr": 1982
+  },
+  {
+    "einkommen": 68.87,
+    "id": 9,
+    "index": 122.93,
+    "jahr": 1983
+  },
+  {
+    "einkommen": 69.34,
+    "id": 10,
+    "index": 117.11,
+    "jahr": 1984
+  },
+  {
+    "einkommen": 70.45,
+    "id": 11,
+    "index": 111.32,
+    "jahr": 1985
+  },
+  {
+    "einkommen": 73.11,
+    "id": 12,
+    "index": 110.77,
+    "jahr": 1986
+  },
+  {
+    "einkommen": 76.6,
+    "id": 13,
+    "index": 111.12,
+    "jahr": 1987
+  },
+  {
+    "einkommen": 79.07,
+    "id": 14,
+    "index": 111.31,
+    "jahr": 1988
+  },
+  {
+    "einkommen": 79.74,
+    "id": 15,
+    "index": 110.82,
+    "jahr": 1989
+  },
+  {
+    "einkommen": 82.76,
+    "id": 16,
+    "index": 109.51,
+    "jahr": 1990
+  },
+  {
+    "einkommen": 86.1,
+    "id": 17,
+    "index": 110.77,
+    "jahr": 1991
+  },
+  {
+    "einkommen": 88.12,
+    "id": 18,
+    "index": 110.26,
+    "jahr": 1992
+  },
+  {
+    "einkommen": 87.54,
+    "id": 19,
+    "index": 110.55,
+    "jahr": 1993
+  },
+  {
+    "einkommen": 88.97,
+    "id": 20,
+    "index": 112.49,
+    "jahr": 1994
+  },
+  {
+    "einkommen": 90.06,
+    "id": 21,
+    "index": 114.71,
+    "jahr": 1995
+  },
+  {
+    "einkommen": 90.93,
+    "id": 22,
+    "index": 114.01,
+    "jahr": 1996
+  },
+  {
+    "einkommen": 90.6,
+    "id": 23,
+    "index": 111.06,
+    "jahr": 1997
+  },
+  {
+    "einkommen": 91.73,
+    "id": 24,
+    "index": 108.7,
+    "jahr": 1998
+  },
+  {
+    "einkommen": 93.33,
+    "id": 25,
+    "index": 108.58,
+    "jahr": 1999
+  },
+  {
+    "einkommen": 95.78,
+    "id": 26,
+    "index": 108.91,
+    "jahr": 2000
+  },
+  {
+    "einkommen": 97.77,
+    "id": 27,
+    "index": 108.87,
+    "jahr": 2001
+  },
+  {
+    "einkommen": 96.68,
+    "id": 28,
+    "index": 105.08,
+    "jahr": 2002
+  },
+  {
+    "einkommen": 97.8,
+    "id": 29,
+    "index": 103.0,
+    "jahr": 2003
+  },
+  {
+    "einkommen": 99.06,
+    "id": 30,
+    "index": 102.43,
+    "jahr": 2004
+  },
+  {
+    "einkommen": 100.0,
+    "id": 31,
+    "index": 100.0,
+    "jahr": 2005
+  },
+  {
+    "einkommen": 101.04,
+    "id": 32,
+    "index": 99.23,
+    "jahr": 2006
+  },
+  {
+    "einkommen": 102.67,
+    "id": 33,
+    "index": 95.55,
+    "jahr": 2007
+  },
+  {
+    "einkommen": 103.53,
+    "id": 34,
+    "index": 94.84,
+    "jahr": 2008
+  },
+  {
+    "einkommen": 103.73,
+    "id": 35,
+    "index": 94.09,
+    "jahr": 2009
+  },
+  {
+    "einkommen": 104.18,
+    "id": 36,
+    "index": 94.05,
+    "jahr": 2010
+  },
+  {
+    "einkommen": 105.76,
+    "id": 37,
+    "index": 94.19,
+    "jahr": 2011
+  },
+  {
+    "einkommen": 107.8,
+    "id": 38,
+    "index": 95.08,
+    "jahr": 2012
+  },
+  {
+    "einkommen": 106.54,
+    "id": 39,
+    "index": 98.03,
+    "jahr": 2013
+  },
+  {
+    "einkommen": 107.57,
+    "id": 40,
+    "index": 99.75,
+    "jahr": 2014
+  },
+  {
+    "einkommen": 109.42,
+    "id": 41,
+    "index": 103.28,
+    "jahr": 2015
+  },
+  {
+    "einkommen": 109.71,
+    "id": 42,
+    "index": 108.15,
+    "jahr": 2016
+  },
+  {
+    "einkommen": 111.99,
+    "id": 43,
+    "index": 111.96,
+    "jahr": 2017
+  }
+]

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

@@ -0,0 +1,10 @@
+{
+	"title": "Wie stark hängen der Wert von Immobilien und die Einkommensentwicklung zusammen?",
+  "introtext": "In der Diskussion um die Einkommensentwicklung im Kontext steigender Mieten, Grundstücks- und Immobilienpreise in Deutschland wird das zunehmende Auseinanderdriften kritisiert.\nDoch ist das so, historisch betrachtet? \nBitte schätzen Sie selbst und machen Sie sich mit Immobilienpreis- und Einkommensindex vertraut. Beide Indizes sind korrigiert um den Wert der Währung (inflationsbereinigt).\nDas Jahr 2005 geben wir mit 100% als Bezugspunkt für beide Indizes vor. Los geht’s!",
+  "outrotext": "Sie haben gleich zwei historische Entwicklungen verinnerlicht. Erst in den letzten Jahren haben die Immobilienpreise historisch gesehen wieder stark angezogen und ein länger andauerndes Missverhältnis zum Einkommen ist vorstellbar.",
+  "next": "Weiter",
+  "start": "Start",
+  "restart": "Neustart",
+  "error": "Achtung, da fehlt noch was!",
+  "done": "Wenn Sie fertig sind, klicken Sie bitte auf weiter!"
+}

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

@@ -0,0 +1,3 @@
+import dataA from './group-a.json';
+
+export default [ dataA ];

+ 46 - 0
src/js/content/questions.json

@@ -0,0 +1,46 @@
+[
+  {
+    "source": "Quelle: Mack, A., and E. Martínez-García. 2011. \"A Cross-Country Quarterly Database of Real House Prices: A Methodological Note.\" Globalization and Monetary Policy Institute Working Paper No. 99, Federal Reserve Bank of Dallas.",
+    "texts": [
+      {
+        "text": "Schätzen Sie zunächst bitte, wie sich die Immobilienpreise über die letzten 40 Jahre in Deutschland entwickelt haben.",
+        "instruction": "Bitte fahren Sie über die Fläche des Diagramms, um eine Linie zu zeichnen!",
+        "hint": "Einen Punkt haben wir Ihnen zur Orientierung vorgegeben. Dieser Immobilienpreis-Index von 100 liefert den Bezug für die anderen Jahre.",
+        "answer1": "Nicht schlecht!\n\n Sie sehen jetzt wie die Preise tatsächlich gestiegen sind.",
+        "answer2": "Sie sehen jetzt im Vergleich die Schätzungen aller anderen Nutzer.\n\n Klicken Sie auf weiter, um nun die Einkommensentwicklung vergleichend hinzuziehen!"
+      },
+      {
+        "text": "Den Anstieg der Immobilien behalten wir. Schätzen Sie nun bitte zusätzlich, wie sich das Einkommen über die letzten 40 Jahre entwickelt hat.",
+        "instruction": "Bitte fahren Sie wieder über das Diagramm, um eine Linie zu zeichnen!",
+        "hint": "Einen Punkt haben wir Ihnen zur Orientierung vorgegeben. Dieser Einkommensindex von 100 liefert den Bezug für die anderen Jahre.",
+        "answer1": "Nicht schlecht!\n\n Sie sehen, dass in den letzten Jahren das Einkommen mit den Immobilienpreisen nicht mithält.",
+        "answer2": "Sie sehen jetzt im Vergleich die Schätzungen aller anderen Nutzer."
+      }
+    ],
+    "fixedPoints": [
+      {
+        "jahr": 2005,
+        "index": 100
+      },
+      {
+        "jahr": 2005,
+        "einkommen": 100
+      }
+    ],
+    "yKeys": [
+      "index",
+      "einkommen"
+    ],
+    "yAxisUnit": [
+      "Immobilienindex",
+      "Einkommen"
+    ],
+    "legendLabels": [
+      "Immobilienpreis",
+      "Einkommen",
+      "Durchschnitt"
+    ],
+    "xKey": "jahr",
+    "xAxisUnit": "Jahr"
+  }
+]

+ 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.widthWithoutMargin / 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.heightWithoutMargin / 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.heightWithoutMargin})`)
+    .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 };
+};

+ 210 - 0
src/js/d3/draw.js

@@ -0,0 +1,210 @@
+import * as d3 from 'd3';
+import hideMessage from './message';
+import round from '../utilities/math';
+
+// d3js module for module03: draw line, add points to readjust them
+export default (selection, scales, options) => {
+  // defaults
+  let isDrawMode = false;
+  const lineClass = 'mod3-userline';
+  const circleClass = 'mod3-circle';
+  const line = d3.line().curve(d3.curveCardinal);
+  const points = [];
+  const xRange = scales.xScale.range();
+
+  // start drawing
+  const draw = () => {
+    selection.select(`.${lineClass}`).attr('d', line); // update d path from line
+    selection.selectAll(`.${circleClass}`).remove();
+
+    // add points
+    selection.selectAll(`.${circleClass}`)
+      .data(points)
+      .enter()
+      .append('circle')
+      .attr('class', circleClass)
+      .attr('transform', d => `translate(${d})`)
+      .attr('r', options.pointRadius);
+
+    if (d3.event) {
+      d3.event.preventDefault();
+      d3.event.stopPropagation();
+    }
+  };
+
+  // calculate point coordinates
+  const calcPoint = (point, domain = false) => {
+    let finPoint = {};
+    const xDomain = scales.xScale.domain();
+
+    // increase max range value, add distance between two points to match last point
+    const xRangePoints = d3.range(xRange[0], xRange[1] + scales.xScale.step(), scales.xScale.step());
+
+    // remove half of the distance between two points to get a correct mean value
+    const pointX = point[0] - scales.xScale.step() / 2;
+    const xPos = xDomain[d3.bisect(xRangePoints, pointX)];
+
+    if (domain) {
+      finPoint = { x: xPos, y: scales.yScale.invert(point[1]) };
+    } else {
+      finPoint = {
+        x: Math.max(0, Math.min(options.widthWithoutMargin, scales.xScale(xPos))),
+        y: Math.max(0, Math.min(options.heightWithoutMargin, Math.round(point[1])))
+      };
+    }
+
+    return finPoint;
+  };
+
+  // sort points
+  const sortPoints = () => {
+    points.sort((a, b) => {
+      // Compare the 2 dates
+      if (a[0] < b[0]) return -1;
+      if (a[0] > b[0]) return 1;
+      return 0;
+    });
+  };
+
+  // get points and draw them
+  const calcPoints = () => {
+    if (isDrawMode) {
+      let index = null;
+      const point = calcPoint(d3.mouse(selection.node()));
+      const match = points.find((p, i) => {
+        index = i;
+        return p[0] === point.x;
+      });
+
+      // only draw valid points
+      if (point.x >= 0 && point.y >= 0) {
+        if (match) {
+          points[index][1] = point.y; // update existing point
+        } else {
+          points.push([ point.x, point.y ]); // add point
+          sortPoints();
+        }
+
+        draw();
+      }
+    }
+  };
+
+  // toggle draw mode
+  const setDrawMode = (boo = true) => (isDrawMode = boo);
+  const activateDrawMode = () => setDrawMode();
+  const deactivateDrawMode = () => setDrawMode(false);
+
+  // calculate missing points
+  // if user has just clicked multiple times without dragging
+  const calcMissingPoints = path => {
+    for (let i = 0; i < path.getTotalLength(); i += scales.xScale.step()) {
+      const svgPoint = path.getPointAtLength(i);
+      const point = calcPoint([ svgPoint.x, svgPoint.y ]);
+      const match = points.find(p => round(p[0]) === round(point.x));
+
+      if (!match) points.push([ point.x, point.y ]); // add point
+      sortPoints();
+    }
+  };
+
+  // mark missing area for a certain direction
+  const markMissingArea = (direction = 'left') => {
+    const rect = selection.append('rect')
+      .attr('class', `mod3-rect--missing  mod3-rect--${direction}`)
+      .attr('height', options.heightWithoutMargin);
+
+    return rect;
+  };
+
+  // handle missing areas
+  const markMissingAreas = path => {
+    // get first and last point from line
+    const min = path.getPointAtLength(0);
+    const max = path.getPointAtLength(path.getTotalLength());
+    const rects = {
+      left: selection.select('.mod3-rect--left'),
+      right: selection.select('.mod3-rect--right')
+    };
+
+    min.x = round(min.x, 0);
+    max.x = round(max.x, 0);
+
+    // mark area to the left, 0 - min.x
+    if (min.x > 0) {
+      if (rects.left.empty()) rects.left = markMissingArea();
+      rects.left
+        .transition()
+        .duration(500)
+        .attr('width', min.x);
+    } else if (!rects.left.empty()) {
+      rects.left.remove();
+    }
+
+    // mark area to the right, max.x - scalemax
+    if (max.x < xRange[1]) {
+      if (rects.right.empty()) {
+        rects.right = markMissingArea('right');
+        rects.right.attr('transform', `translate(${max.x}, 0)`);
+      }
+      rects.right
+        .transition()
+        .duration(500)
+        .attr('width', options.widthWithoutMargin - max.x)
+        .attr('transform', `translate(${max.x}, 0)`);
+    } else if (!rects.right.empty()) {
+      rects.right.remove();
+    }
+  };
+
+  // calcualte real points and handle message visibility
+  const calcRealPoints = () => {
+    const rects = selection.selectAll('.mod3-rect--missing');
+
+    if (rects.empty() && points.length) { // no missing areas, valid path
+      const realPoints = [];
+
+      points.forEach(point => (realPoints.push(calcPoint(point, true))));
+      options.setDataMethod(realPoints);
+      hideMessage('error', true); // hide error message
+      hideMessage('done', false); // show done message
+    } else {
+      hideMessage(); // show error message
+    }
+  };
+
+  // finish drawing: calculate missing points, mark missing areas and calculate real points
+  const finishDrawing = () => {
+    const path = selection.select(`.${lineClass}`).node();
+
+    deactivateDrawMode();
+    if (points.length && points.length > 1) {
+      calcMissingPoints(path);
+      markMissingAreas(path);
+      calcRealPoints();
+    }
+  };
+
+  // add mouse and touch handlers
+  selection.select('.mod3-rect')
+    .on('mousedown touchstart', () => {
+      activateDrawMode();
+      calcPoints();
+    })
+    .on('mousemove touchmove', calcPoints);
+
+  // add path and call draw method
+  selection.append('path')
+    .datum(points)
+    .attr('class', lineClass)
+    .call(draw)
+    .on('mousedown touchstart', activateDrawMode)
+    .on('mousemove touchmove', calcPoints);
+
+  // handle finish drawing
+  d3.select(window)
+    .on('mouseup touchend', finishDrawing);
+
+  // set correct focus
+  selection.node().focus();
+};

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

@@ -0,0 +1,27 @@
+import * as d3 from 'd3';
+
+// d3js module: add grid
+export default (selection, scales, options) => {
+  const addGridlinesX = () => d3.axisBottom(scales.xScale).ticks(5);
+  const addGridlinesY = () => d3.axisLeft(scales.yScale).ticks(15);
+
+  // add the X gridlines
+  selection.append('g')
+    .attr('class', 'grid  grid--x')
+    .attr('transform', `translate(0, ${options.heightWithoutMargin})`)
+    .call(
+      addGridlinesX()
+        .tickSize(-options.heightWithoutMargin)
+        .tickFormat('')
+    );
+
+  // add the Y gridlines
+  selection.append('g')
+    .attr('class', `grid  grid--y  mod${options.module}__grid--y`)
+    .call(
+      addGridlinesY()
+        .tickSize(-options.widthWithoutMargin)
+        .tickFormat('')
+    );
+
+};

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

@@ -0,0 +1,37 @@
+// d3js module: add legend
+export default (selection, options, label) => {
+  const yStart = options.heightWithoutMargin + options.margin.bottom * 0.8;
+  let xStart = 0;
+
+  // get start value
+  if (options.i > 1 && selection.node().previousSibling) {
+    const boxWrapper = selection.node().previousSibling.querySelector(`.mod${options.module}-legend`);
+
+    if (boxWrapper) {
+      const box = boxWrapper.getBBox();
+
+      xStart = box.x + box.width + 20;
+    }
+  }
+
+  // append selection wrapper
+  const group = selection.append('g')
+    .attr('class', `mod${options.module}-legend`);
+
+  // append indicator line
+  group.append('line')
+    .attr('x1', xStart)
+    .attr('y1', yStart - 5)
+    .attr('x2', xStart + 50)
+    .attr('y2', yStart - 5)
+    .attr('stroke-width', 2)
+    .attr('stroke', options.colors.units);
+
+  // append text / description
+  group.append('text')
+    .attr('x', xStart + 55)
+    .attr('y', yStart)
+    .attr('fill', options.colors.units)
+    .style('font', '14px sans-serif')
+    .text(`${label}`);
+};

+ 31 - 0
src/js/d3/line.js

@@ -0,0 +1,31 @@
+import * as d3 from 'd3';
+import addLegendItem from './legend';
+
+// d3js module for module03: add line
+export default (selection, scales, options) => {
+  // add line
+  const valueline = d3.line()
+    .curve(d3.curveCardinal)
+    .x(d => scales.xScale(d[options.xKey]))
+    .y(d => scales.yScale(d[options.yKey]));
+
+  const currClass = `mod${options.module}-line--${options.i}`;
+
+  // append group
+  const group = selection
+    .selectAll(`.${currClass}`)
+    .data([ options.entries ])
+    .enter()
+    .append('g')
+    .attr('class', `mod${options.module}-line  ${currClass} ${options.isCurrent ? `mod${options.module}-line--active` : ''}`);
+
+  // append path
+  group.append('path')
+    .attr('stroke', options.colors.graph)
+    .attr('stroke-width', 2)
+    .attr('fill', 'none')
+    .attr('d', d => valueline(d));
+
+  // add legend for current line
+  addLegendItem(group, options, options.legendLabels[options.i - 1]);
+};

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

@@ -0,0 +1,135 @@
+import addAxes from './axes';
+import addScales from './scales';
+import addFixedPoint from './point';
+import addLine from './line';
+import addUserLine from './userline';
+import addGrid from './grid';
+import drawLine from './draw';
+import hideMessage from './message';
+
+// d3js module for module03: linegraph base
+export default () => {
+  // defaults
+  const options = {
+    entries: {},
+    userEntries: [],
+    comparisonData: [],
+    width: 850,
+    height: 400,
+    xAxisUnit: 'x axis',
+    yAxisUnit: 'y axis',
+    xKey: 'x',
+    yKey: 'y',
+    yKeys: [ 'y' ],
+    margin: { top: 20, right: 10, bottom: 100, left: 60 },
+    fixedPoint: { x: 10, y: 10 },
+    pointRadius: 4,
+    mode: 'interaction',
+    currentStep: 0,
+    currentQuestion: 0,
+    module: 3,
+    colors: { axis: '#9b9b9b', units: '#616161', graph: '#616161' },
+    setDataMethod: () => {} // eslint-disable-line no-empty-function
+  };
+
+  // adapted size without margin
+  const widthWithoutMargin = options.width - options.margin.left - options.margin.right;
+  const heightWithoutMargin = options.height - options.margin.top - options.margin.bottom;
+
+  // merge options
+  Object.assign(options, { widthWithoutMargin, heightWithoutMargin });
+
+  // start
+  const graph = selection => {
+    // append svg
+    const svg = selection
+      .append('svg')
+      .attr('class', 'mod3-chart')
+      .attr('width', options.width)
+      .attr('height', options.height);
+
+    // append group
+    const group = svg
+      .append('g')
+      .attr('transform', `translate(${options.margin.left}, ${options.margin.top})`);
+
+    // append reactangle
+    group.append('rect')
+      .attr('class', 'mod3-rect')
+      .attr('width', options.widthWithoutMargin)
+      .attr('height', options.heightWithoutMargin);
+
+    // add scales
+    const scales = addScales(options);
+
+    // add grid, axes and fixed point
+    addGrid(group, scales, options);
+    addAxes(group, scales, options);
+    addFixedPoint(group, scales, options);
+
+    // step 2, before step 1!!!
+    if (options.currentStep > 0) {
+      for (let i = 0; i < options.currentStep; i += 1) {
+        addLine(group, scales, Object.assign({}, options, { yKey: options.yKeys[i], i: i + 1, isCurrent: false }));
+      }
+    }
+
+    // Draw lines: Realities
+    // step 1
+    if (options.mode !== 'interaction') {
+      addLine(group, scales, Object.assign({}, options, { i: options.currentStep + 1, isCurrent: true }));
+      hideMessage('error', true); // hide error message
+      hideMessage('done', true); // hide done message
+    } else {
+      drawLine(group, scales, options);
+    }
+
+    // Draw comparison data
+    if (options.mode === 'comparison') {
+      // Create object grouped by x value
+      const res = options.comparisonData.reduce((o, c) => {
+        o[c.x] = o[c.x] || [];
+        o[c.x].push(c.y);
+        return o;
+      }, Object.create(null));
+
+      let thesolution = [];
+
+      // Create array of objects for each x value with corresponding average y values
+      Object.keys(res).forEach((x) => {
+        let sum = res[x].reduce((s, v) => s + v, 0);
+        let avg = sum / res[x].length;
+        thesolution.push({ x: parseInt(x), y: avg });
+      });
+
+      thesolution.sort((a, b) => a.x - b.x);
+
+      // Hack for incorrect first data point, needs to be fixed in the database when the db ui is finished
+      if (thesolution[0].x === 2 && thesolution[0].y === 5) {
+        thesolution.shift();
+      }
+
+      // options.colors.graph = '#5533aa';
+      addLine(group, scales, Object.assign({}, options, {
+        entries: thesolution,
+        xKey: 'x',
+        yKey: 'y',
+        i: options.legendLabels.length,
+        cn: 'average'
+      }));
+    }
+
+    // Draw userline
+    if (options.userEntries.length && options.mode !== 'interaction') {
+      addUserLine(group, scales, Object.assign({}, options, { xKey: 'x', yKey: 'y', i: options.currentStep + 2, isCurrent: false }));
+    }
+  };
+
+  // "setter"
+  graph.options = input => {
+    Object.assign(options, input);
+    return graph;
+  };
+
+  return graph;
+};

+ 6 - 0
src/js/d3/message.js

@@ -0,0 +1,6 @@
+// d3js module: add message, handle visibility
+export default (key = 'error', val = false) => {
+  const div = document.querySelector(`.question__message--${key}`);
+
+  if (div) div.setAttribute('aria-hidden', val);
+};

+ 8 - 0
src/js/d3/point.js

@@ -0,0 +1,8 @@
+// d3js module for module03: draw fixed point
+export default (selection, scales, options) => {
+  selection.append('circle')
+    .attr('class', 'mod3-circle--assistance')
+    .attr('r', options.pointRadius)
+    .attr('cx', scales.xScale(options.fixedPoint[options.xKey]))
+    .attr('cy', scales.yScale(options.fixedPoint[options.yKey]));
+};

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

@@ -0,0 +1,26 @@
+import * as d3 from 'd3';
+
+// d3js module for module03: add scales
+export default (options) => {
+  // add x scale (point scale)
+  const xScale = d3.scalePoint()
+    .range([ 0, options.widthWithoutMargin ])
+    .domain(options.entries.map(entry => entry[options.xKey]));
+
+  // improve this using array reduce!
+  const yMatch = entry => {
+    const arr = [];
+
+    options.yKeys.forEach(key => (arr.push(entry[key])));
+    return arr;
+  };
+
+  // add y scale (linear scale)
+  const yMin = d3.min(options.entries, entry => (Math.min(...yMatch(entry))));
+  const yMax = d3.max(options.entries, entry => (Math.max(...yMatch(entry))));
+  const yScale = d3.scaleLinear()
+    .range([ options.heightWithoutMargin, 0 ])
+    .domain([ yMin - yMax / 12, yMax + yMax / 12 ]);
+
+  return { xScale, yScale };
+};

+ 32 - 0
src/js/d3/userline.js

@@ -0,0 +1,32 @@
+import * as d3 from 'd3';
+import addLegendItem from './legend';
+
+// d3js module for module03: add painted user line
+export default (selection, scales, options) => {
+
+  // get line coordinates
+  const valueline = d3.line()
+    .curve(d3.curveCardinal)
+    .x(d => scales.xScale(d[options.xKey]))
+    .y(d => scales.yScale(d[options.yKey]));
+
+  const currClass = `mod${options.module}-line--user`;
+
+  // append group for current line
+  const group = selection
+    .selectAll(`.${currClass}`)
+    .data([ options.userEntries ])
+    .enter()
+    .append('g')
+    .attr('class', `mod${options.module}-line  ${currClass} ${options.isCurrent ? `mod${options.module}-line--active` : ''}`);
+
+  // append line
+  group.append('path')
+    .attr('stroke', options.colors.graph)
+    .attr('stroke-width', 2)
+    .attr('fill', 'none')
+    .attr('d', d => valueline(d));
+
+  // add legend for current line
+  addLegendItem(group, options, 'Ihre Schätzung');
+};

+ 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=jens.becker%40kf-interactive.com&password=P4ssw0rd%21'
+    };
+
+    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');
+      });
+  }
+};

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

+ 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;
+}

+ 17 - 0
src/scss/config/_breakpoints.scss

@@ -0,0 +1,17 @@
+// breakpoints used throughout the project - use with mixin and js-module
+// ======================================================================
+
+// please only use px and em to define breakpoints. px-values will
+// automatically be converted to em-mediaqueries in the mixin and
+// the provieded javascript-module
+
+$breakpoints: (
+  s: 480px,
+  m: 640px,
+  l: 720px,
+  xl: 960px,
+  xxl: 1100px,
+  xxxl: 1400px,
+  // to use this, set the second parameter of the mixin to true
+  custom: '(max-width: 30em)'
+);

+ 32 - 0
src/scss/config/_colors.scss

@@ -0,0 +1,32 @@
+// the colors map variables used throughout the project
+// ======================================================================
+
+// define your colors here, and feel free to use 'real' names, just
+// like below --> color-red: #fff; but never use those colors in your
+// project, only use the map + mixins, where you assing the colors in
+
+$color-grey: #616161;
+$color-grey-light: #f0f0f0;
+$color-grey-another: #9b9b9b;
+$color-white: rgb(255, 255, 255);
+$color-red: #d0021b;
+$color-red-soft: #ffb8c1;
+$color-green: #7ed321;
+$color-light: rgba(0, 0, 0, 0.08);
+$color-blue: #90cfeb;
+
+$colors: (
+  default: $color-grey,
+  main: $color-red,
+  sub: $color-blue,
+  error: $color-red,
+  correct: $color-green,
+  inverted: $color-white,
+  border: $color-grey,
+  background: $color-white,
+  button: $color-grey-light,
+  lightbackground: $color-grey-light,
+  grid: $color-light,
+  axis: $color-grey-another,
+  soft: $color-red-soft
+);

+ 37 - 0
src/scss/config/_defaults.scss

@@ -0,0 +1,37 @@
+// base unit definition and width/heights for other stuff
+// ======================================================================
+
+// base unit - default font-size * line-height in rem and half/double
+$base-line-height: map-get(map-get($font-config, default), line-height);
+$base-font-size: map-get($font-sizes, default);
+
+$base-unit-in-px: $base-font-size * $base-line-height;
+$base-unit: 1rem * $base-line-height;
+$base-half: $base-unit / 2;
+$base-double: $base-unit * 2;
+
+
+// globally used width and height declarations
+// ======================================================================
+
+// same as icon + margin
+$width-icon: $base-unit * 1.2 + $base-half;
+
+
+// overrides and customization
+// ======================================================================
+// if you want to customize any of the values maps and values that
+// are set by default in the later to be included tools, mixins and
+// function (such as z-indexes) please add them here, before including
+// the actual mixins, for example, override the z-indexes map from
+// frckl-tools/_z-index.scss
+
+// $z-indexes: (
+//   whale: 1000,
+//   elephant: 900,
+//   tiger: 800,
+//   dog: 700,
+//   cat: 700,
+//   mouse: 500,
+//   worm: -1
+// );

+ 73 - 0
src/scss/config/_fonts.scss

@@ -0,0 +1,73 @@
+// font families and their fallbacks for the whole page
+// ======================================================================
+
+// you the parameters height (as in line-height) and
+// fontface are optional, but you *should* set the default line
+// height for the default font.
+// if you want to use font-face fonts, define them in this map
+// and set the fontface-parameter to true and provide the filename
+// for the font-definition to be included (without ../fonts or .woff)
+
+$font-config: (
+  default: (
+    family: 'Roboto',
+    fallback: 'Arial, sans-serif',
+    weight: 300,
+    style: normal,
+    line-height: 1.5,
+    fontface: true,
+    file: 'roboto-light'
+  ),
+  thin: (
+    family: 'Roboto',
+    fallback: 'Arial, sans-serif',
+    weight: 200,
+    style: normal,
+    line-height: 1.5,
+    fontface: true,
+    file: 'roboto-thin'
+  ),
+  bold: (
+    family: 'Roboto',
+    fallback: 'Arial, sans-serif',
+    weight: 600,
+    style: normal,
+    line-height: 1.5,
+    fontface: true,
+    file: 'roboto-medium'
+  ),
+  regular: (
+    family: 'Roboto',
+    fallback: 'Arial, sans-serif',
+    weight: 400,
+    style: normal,
+    line-height: 1.5,
+    fontface: true,
+    file: 'roboto-regular'
+  ),
+  mono: (
+    family: 'Roboto Mono',
+    fallback: 'monospace',
+    weight: 300,
+    style: normal,
+    line-height: 1.5,
+    fontface: true,
+    file: 'robotomono-light'
+  )
+);
+
+// global font sizes scss-map variable - use with mixin
+// ======================================================================
+
+$font-sizes: (
+  huge: 64px,
+  h1: 36px,
+  h2: 32px,
+  h3: 24px,
+  h4: 20px,
+  h5: 18px,
+  h6: 16px,
+  small: 14px,
+  smaller: 12px,
+  default: 16px
+);

+ 62 - 0
src/scss/main.scss

@@ -0,0 +1,62 @@
+// main.scss - in here we just include all the other partials
+// ======================================================================
+
+// first we include global variable-maps from the config
+@import 'config/colors';
+@import 'config/fonts';
+@import 'config/breakpoints';
+
+// include the sizes after the above maps, use defaults from font-config
+// to generate the default base-units and other dimensions
+@import 'config/defaults';
+
+// then we include all functions and mixins
+@import '../../node_modules/frckl-tools/all-tools';
+@import 'tools/centered';
+
+// next up: third party generic styles + resets
+@import '../../node_modules/normalize.css/normalize';
+@import '../../node_modules/frckl-reset/reset';
+
+// base styles - for raw html-elements, no classes
+@import 'base/root';
+@import 'base/fonts';
+@import 'base/rhythm';
+@import 'base/forms';
+@import 'base/headings';
+@import 'base/links';
+
+// throw in all other 3rdparty styles here this way we can override
+// vendor-styles in our modules, if we really need to
+// @import 'vendor/lightbox';
+
+// next up: all your modules. be careful with order/cascading here
+// have a look at the modules folder for already existing ones
+@import 'modules/animations';
+// @import 'modules/boxes';
+@import 'modules/buttons';
+@import 'modules/forms';
+// @import 'modules/grids';
+@import 'modules/icons';
+@import 'modules/logos';
+@import 'modules/navs';
+@import 'modules/typography';
+@import 'modules/answers';
+@import 'modules/score';
+@import 'modules/questions';
+@import 'modules/d3js';
+@import 'modules/votes';
+
+// layout modules, but i treat them as modules :)
+@import 'modules/wrapper';
+@import 'modules/header';
+@import 'modules/content';
+@import 'modules/footer';
+@import 'modules/debug';
+
+// generically used styles and that are able to override modules
+// if you have similar styles that override modules, throw them
+// in the folder 'overrides'
+@import '../../node_modules/frckl-helpers/clearfix';
+@import '../../node_modules/frckl-helpers/hidden';
+// @import 'overrides/your-override';

+ 21 - 0
src/scss/modules/_animations.scss

@@ -0,0 +1,21 @@
+// some basic keyframe animations
+// ======================================================================
+
+// default animation, only animate opacity + transform, because
+// those are cheap to animate and dont trigger any heavy layout recalc
+.animate,
+%animate {
+  transition: opacity 0.4s ease-in, transform 0.4s ease-in;
+}
+
+// define you keyframe animations here and use them anywhere else
+// rotates an element once
+// @keyframes rotation {
+//   from {
+//     transform: rotate(0deg);
+//   }
+
+//   to {
+//     transform: rotate(360deg);
+//   }
+// }

+ 115 - 0
src/scss/modules/_answers.scss

@@ -0,0 +1,115 @@
+.answer-item {
+  @include spacing(b 2);
+
+  &__title {
+    @include font-size(h5);
+    @include spacing(b .75);
+  }
+
+  &__value {
+    @include font-size(small);
+    text-transform: uppercase;
+  }
+}
+
+.question__options__item {
+  .question__fakenews & {
+    @include spacing;
+
+    .checkbox-label {
+      font-weight: 500;
+    }
+  }
+
+  &--marked {
+    @include color(error);
+
+    .checkbox-label::before {
+      @include border-color(error);
+      border-width: 1px;
+      border-style: solid;
+    }
+
+    .checkbox-label::after {
+      @include absolute(t 0, l 0);
+      @include font-size(h3);
+      line-height: 1;
+      content: '\2715';
+      display: block;
+      width: 20px;
+      height: 20px;
+      text-align: center;
+    }
+  }
+
+  &--correct {
+    @include color(correct);
+
+    .checkbox-label::before {
+      @include border-color(correct);
+      border-width: 1px;
+      border-style: solid;
+    }
+  }
+
+  &--checked {
+    .checkbox-label::before {
+      @include border-color(correct);
+      border-width: 1px;
+      border-style: solid;
+    }
+
+    .checkbox-label::after {
+      @include absolute(t 0, l 0);
+      @include font-size(h3);
+      @include color(correct);
+      line-height: 1;
+      content: '\2715';
+      display: block;
+      width: 20px;
+      height: 20px;
+      text-align: center;
+    }
+  }
+
+  &--selected {
+    .checkbox-label::after {
+      @include absolute(t 0, l 0);
+      @include font-size(h3);
+      @include color(default);
+      line-height: 1;
+      content: '\2715';
+      display: block;
+      width: 20px;
+      height: 20px;
+      text-align: center;
+    }
+  }
+
+  &--incorrect {
+    @include color(error);
+  }
+}
+
+// mod2
+.question__answer__item {
+  @include spacing(t 1);
+  text-align: center;
+
+  &--correct {
+    @include color(correct);
+  }
+
+  &--incorrect {
+    @include color(error);
+  }
+
+  span {
+    @include font-size(h5);
+  }
+}
+
+.checkbox-feedback {
+  @include color(default);
+  @include spacing-inner(l 1.3);
+}

+ 29 - 0
src/scss/modules/_boxes.scss

@@ -0,0 +1,29 @@
+// box ratio / add custom boxes with a fixed (responsive) ratio
+// ======================================================================
+
+// boxes with a fixed ratio use like:
+// <div class="box  box--16-9">
+//   <div class="box__content"></div>
+// </div>
+
+.box {
+  @include block(block);
+
+  &::before {
+    @include block(pseudo);
+    @include ratio-box;
+  }
+}
+
+.box__content {
+  @include center(cover);
+}
+
+// and now - the various box sizes
+.box--2-1 {
+  @include ratio-box(2, 1);
+}
+
+.box--4-3 {
+  @include ratio-box(4, 3);
+}

+ 55 - 0
src/scss/modules/_buttons.scss

@@ -0,0 +1,55 @@
+// buttons
+// ======================================================================
+
+// on the how and why of @extend, read:
+// http://csswizardry.com/2014/11/when-to-use-extend-when-to-use-a-mixin/
+
+// default button styles for every button
+%button {
+  display: inline-block;
+  cursor: pointer;
+}
+
+.button {
+  @extend %button;
+  @include background-color(button);
+  @include color(main);
+  @include spacing;
+  @include spacing-inner(h 1.75, v .5);
+  border: 0;
+  font-weight: 400;
+}
+
+%button--wide,
+.button--wide {
+  @include background-color(button);
+  @include color(main);
+  @include font-size(h3);
+  @include spacing-inner(a 1.5);
+  display: block;
+  width: 100%;
+  text-align: center;
+
+  &--first {
+    @include spacing(r .05);
+  }
+
+  &--between {
+    @include spacing(h .05);
+  }
+
+  &--last {
+    @include spacing(l .05);
+  }
+
+  &--cond {
+    opacity: 0;
+    cursor: initial;
+
+    &.isvisible {
+      opacity: 1;
+      transition: opacity .4s ease-in;
+      cursor: pointer;
+    }
+  }
+}

+ 32 - 0
src/scss/modules/_content.scss

@@ -0,0 +1,32 @@
+// content styles
+// ======================================================================
+.intro {
+  @include center;
+  max-width: $base-double * 10;
+  // text-align: center;
+}
+
+.spacingr {
+  @include spacing(r 4);
+}
+
+.content {
+  &--above {
+    // mod06
+    height: $base-unit * 5;
+    overflow: hidden;
+  }
+}
+
+.legend {
+  display: flex;
+  flex-direction: column;
+
+  &-item {
+    @include font-size(smaller);
+
+    &--unit {
+      @include spacing(t .2);
+    }
+  }
+}

+ 208 - 0
src/scss/modules/_d3js.scss

@@ -0,0 +1,208 @@
+svg { text {
+    user-select: none;
+
+    &::selection {
+      background: none;
+    }
+  }
+}
+
+.chart {
+  position: relative;
+
+  &--centered {
+    @extend %centered;
+  }
+}
+
+.mod3 {
+  &__x-axis--1 {
+    g:nth-of-type(3n+2),
+    g:nth-of-type(3n+3) {
+      text {
+        display: none;
+      }
+    }
+  }
+
+  &-chart {
+    @include z-index(knees);
+    position: relative;
+  }
+
+  &-line {
+    // sass-lint:disable-block force-element-nesting
+    @for $i from 1 through 4 {
+      &--#{$i} line,
+      &--#{$i} path {
+        stroke-width: 2;
+
+        @if $i > 1 {
+          stroke-dasharray: 6 * ($i - 2) + 2;
+        }
+      }
+    }
+
+    &--active line,
+    &--active path {
+      stroke: map-get($colors, correct);
+      stroke-dasharray: 0;
+    }
+
+    &--user line,
+    &--user path {
+      stroke: map-get($colors, main);
+      stroke-width: 2;
+    }
+  }
+
+  &-canvas {
+    @include z-index(feet);
+    background: transparent;
+    filter: blur(8px);
+  }
+
+  &-circle {
+    fill: map-get($colors, main);
+    pointer-events: none;
+    user-select: none;
+
+    &--assistance {
+      fill: map-get($colors, main);
+      pointer-events: none;
+      user-select: none;
+    }
+  }
+
+  &-rect {
+    fill: map-get($colors, lightbackground);
+    fill-opacity: .5;
+    pointer-events: all;
+
+    &--missing {
+      fill-opacity: .1;
+      fill: map-get($colors, main);
+      pointer-events: none;
+      user-select: none;
+    }
+  }
+
+  &-userline {
+    fill: none;
+    stroke: map-get($colors, main);
+    stroke-width: 2px;
+    stroke-linejoin: round;
+    stroke-linecap: round;
+  }
+}
+
+.mod5 {
+  &__chart {
+    @include absolute(t 150px);
+  }
+
+  &-text {
+    fill: map-get($colors, default);
+    font-weight: 300;
+    font-family: Roboto, sans-serif;
+
+    &--formula {
+      @include font-size(h5);
+      font-weight: 400;
+    }
+  }
+
+  &-headline {
+    @include font-size(h1);
+    fill: map-get($colors, main);
+    font-weight: 300;
+  }
+
+  &-arrow {
+    &__head {
+      fill: map-get($colors, main);
+    }
+
+    &__line {
+      stroke: map-get($colors, main);
+    }
+  }
+}
+
+.mod6 {
+  &-text {
+    @include font-size(small);
+    fill: map-get($colors, default);
+    font-weight: 300;
+    font-family: Roboto, sans-serif;
+
+    &--right {
+      text-align: right;
+    }
+  }
+
+  &-drag {
+    cursor: move;
+
+    &--active {
+      path {
+        stroke: map-get($colors, main);
+      }
+    }
+  }
+
+  &-amount {
+    fill: map-get($colors, main);
+    font-weight: 400;
+    font-family: Roboto, sans-serif;
+  }
+
+  &-axis--text {
+    @include font-size(smaller);
+    font-family: Roboto, sans-serif;
+  }
+
+  &-indicator--text {
+    font-family: Roboto, sans-serif;
+    fill: map-get($colors, main);
+    font-weight: 400;
+  }
+
+  &-tick {
+    &--marked {
+      text {
+        fill: map-get($colors, soft);
+        font-weight: 400;
+      }
+
+      line {
+        stroke: map-get($colors, soft);
+      }
+    }
+
+    &--active {
+      text {
+        @include font-size(h5);
+        fill: map-get($colors, main);
+      }
+    }
+  }
+
+  &-rate {
+    fill: map-get($colors, sub);
+    font-weight: 500;
+  }
+}
+
+.grid {
+  pointer-events: none;
+  user-select: none;
+
+  line {
+    stroke: map-get($colors, grid);
+  }
+
+  path {
+    stroke-width: 0;
+  }
+}

+ 10 - 0
src/scss/modules/_debug.scss

@@ -0,0 +1,10 @@
+.debug {
+  &__output {
+    @include spacing-inner(a 1);
+    @include spacing(t 1);
+    @include background-color(lightbackground);
+    @include font-size(small);
+    @include center;
+    width: 33%;
+  }
+}

+ 11 - 0
src/scss/modules/_footer.scss

@@ -0,0 +1,11 @@
+// footer styles
+// ======================================================================
+
+.footer {
+  @include spacing(t 2);
+  width: 100%;
+
+  &--flex {
+    display: flex;
+  }
+}

+ 92 - 0
src/scss/modules/_forms.scss

@@ -0,0 +1,92 @@
+// forms - very custom form styling (hide honeypots etc.)
+// ======================================================================
+.checkbox {
+  &-input {
+    @extend %visuallyhidden;
+  }
+
+  &-label {
+    @include spacing-inner(l 1.3);
+    position: relative;
+    cursor: pointer;
+
+    .question__ofwhat & {
+      @include font-size(small);
+    }
+
+    &::before {
+      @include absolute(t .1, l 0);
+      @include border-color(default);
+      border-style: solid;
+      border-width: 1px;
+      display: block;
+      content: '';
+      width: 20px;
+      height: 20px;
+    }
+  }
+}
+
+.response-option {
+  @include spacing(b .1);
+
+  &__label {
+    @extend %button--wide;
+    @include color(default);
+
+    &__text {
+      @include spacing(r $width-icon);
+      display: inline-block;
+    }
+
+    &--initial {
+      .response-option__label__text {
+        @include spacing(l $width-icon);
+      }
+    }
+
+    &--correct {
+      @include color(correct);
+    }
+
+    &--incorrect {
+      @include color(error);
+    }
+  }
+}
+
+.form-item {
+  @include spacing(b 3);
+  text-align: center;
+
+  &--mt {
+    @include spacing(t 3);
+  }
+
+  &__label {
+    @include spacing(b .75);
+  }
+
+  &__input {
+    @include center;
+    position: relative;
+    max-width: 300px;
+
+    &--currency {
+      @include absolute(r .4, t .3);
+      @include color(axis);
+      @include font-size(small);
+    }
+  }
+
+  &__select {
+    @include spacing(h .5);
+    width: 120px;
+
+    &-wrapper {
+      position: relative;
+    }
+  }
+}
+
+

+ 45 - 0
src/scss/modules/_grids.scss

@@ -0,0 +1,45 @@
+// a very basic grid system
+// ======================================================================
+
+// use like this:
+// <div class="grid">
+//  <div class="grid__column"> Your content </div>
+//  <div class="grid__column"> Your content </div>
+// </div>
+
+// The grids by default try to put everything stacked on each other
+// on sizes below the l-breakpoint, and columns after that
+// you can throw grids into each other to create custom layouts
+
+.grid {
+  @include spacing(l -1);
+  @include spacing-inner(a 0);
+  display: flex;
+  flex-direction: column;
+  flex-wrap: wrap;
+  justify-content: space-between;
+  list-style: none; // if applied on a list, remove list-styles
+
+  // try to fit everything into a row on larger displays, in
+  // effect making columns out of every grid__column
+  @include mediaquery(l) {
+    flex-direction: row;
+  }
+}
+
+.grid__column {
+  @include spacing-inner(l 1);
+  flex: 1;
+}
+
+// custom grid, this one starts 'columnizing' at ~600px width
+// for 2 elements, at around 900px for 3 elements etc. pp.
+// ======================================================================
+
+.grid--custom {
+  flex-direction: row;
+
+  > .grid__column {
+    flex: 1 0 300px;
+  }
+}

+ 35 - 0
src/scss/modules/_header.scss

@@ -0,0 +1,35 @@
+// header styles
+// ======================================================================
+.header {
+  @include spacing-inner(t 3, h 4);
+  text-align: center;
+
+  &--light {
+    @include spacing-inner(t 1, h 1);
+    @include center;
+    max-width: map-get($breakpoints, xl);
+    width: 100%;
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+  }
+
+  &__title {
+    margin-left: auto;
+    margin-right: auto;
+    max-width: 65rem;
+
+    &--light {
+      @include spacing(v 0, l 1);
+      @include font-size(h5);
+    }
+  }
+
+  &__state {
+    @include spacing(r auto);
+  }
+
+  .number--square {
+    visibility: hidden;
+  }
+}

+ 31 - 0
src/scss/modules/_icons.scss

@@ -0,0 +1,31 @@
+// svg icons and custom icon styles
+// ======================================================================
+
+// default icon styles - inline, same color, 1em/1em
+.icon {
+  position: relative;
+  top: -0.0625em;
+  display: inline-block;
+  width: 1em;
+  height: 1em;
+  fill: currentColor;
+
+  &--correct,
+  &--incorrect {
+    @include spacing(r .5);
+    @include spacing-inner(a .2);
+    border-color: inherit;
+    border-style: solid;
+    border-width: 2px;
+    border-radius: 100%;
+    width: $base-unit * 1.2;
+    height: $base-unit * 1.2;
+  }
+
+  &--triangle {
+    @include absolute(r .8, t .7);
+    pointer-events: none;
+    width: 9px;
+    height: 6px;
+  }
+}

+ 2 - 0
src/scss/modules/_logos.scss

@@ -0,0 +1,2 @@
+// logo styles
+// ======================================================================

+ 36 - 0
src/scss/modules/_navs.scss

@@ -0,0 +1,36 @@
+// navigation styles
+// ======================================================================
+.nav {
+  &__index {
+    @include spacing(t 3);
+
+    li {
+      @include relative;
+      @include spacing-inner(l 4, r 2, v 1);
+      @include color(default);
+
+      @include attention {
+        @include color(main);
+        @include background-color(button);
+      }
+    }
+
+    &__item {
+      max-width: map-get($breakpoints, xxl);
+      color: inherit;
+      display: flex;
+      justify-content: space-between;
+
+      @include attention {
+        text-decoration: none;
+      }
+    }
+
+    &__title {
+      @include font-size(h3);
+      color: inherit;
+      display: inline-block;
+      align-self: center;
+    }
+  }
+}

+ 92 - 0
src/scss/modules/_questions.scss

@@ -0,0 +1,92 @@
+.question {
+  @include spacing;
+
+  &__fakenews {
+    @include spacing(b 0);
+  }
+
+  &__manipula {
+    min-height: 135px;
+  }
+
+  &__title {
+    @include font-size(h4);
+    @include spacing;
+    text-align: center;
+  }
+
+  &__intro {
+    @include font-size(small);
+    @include spacing;
+    text-align: center;
+  }
+
+  &__instruction {
+    @include spacing(b .25);
+    font-weight: 400;
+    text-align: center;
+  }
+
+  &__hint {
+    @include font-size(small);
+    text-align: center;
+  }
+
+  &__options {
+    @include center;
+    display: flex;
+    justify-content: center;
+
+    &__item {
+      @include spacing-inner(h .5);
+
+      .question__ofwhat & {
+        width: 25%;
+      }
+
+      .question__compound & {
+        @include spacing-inner(h 1);
+      }
+
+      .question__fakenews & {
+        @include spacing-inner(l 0);
+      }
+    }
+  }
+
+  &__source {
+    @include font-size(small);
+    text-align: center;
+  }
+
+  &__message {
+    @include font-size(small);
+    @include color(main);
+    max-width: 842px; // same as svg
+    text-align: right;
+    height: 21px;
+
+    > div {
+      &[aria-hidden='true'] {
+        display: none;
+      }
+    }
+  }
+
+  &__ofwhat--no-mb {
+    @include spacing(b 0);
+
+    p {
+      @include spacing(b 0);
+    }
+  }
+
+  &__compound__setting {
+    p {
+      &:nth-of-type(1),
+      &:nth-last-of-type(1) {
+        @include spacing(b 2);
+      }
+    }
+  }
+}

+ 20 - 0
src/scss/modules/_score.scss

@@ -0,0 +1,20 @@
+.score-item {
+  @include spacing;
+  @include font-size(h3);
+
+  &__donut {
+    @include spacing(l 3);
+    display: inline-block;
+    vertical-align: $base-unit * .8;
+  }
+}
+
+.stats {
+  &__manipula {
+    @extend %centered;
+  }
+
+  &__title {
+    @include font-size(h3);
+  }
+}

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio