瀏覽代碼

Initial commit

Michael Zitzmann 5 年之前
當前提交
6d10fcc28b
共有 100 個文件被更改,包括 4807 次插入0 次删除
  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. 41 0
      doc/01_introduction.md
  9. 31 0
      doc/02_structure.md
  10. 71 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. 216 0
      doc/html/index.html
  18. 127 0
      gulpfile.babel.js
  19. 77 0
      package.json
  20. 9 0
      public/browserconfig.xml
  21. 二進制
      public/favicon.ico
  22. 5 0
      public/robots.txt
  23. 392 0
      readme.md
  24. 二進制
      src/fonts/roboto-light.woff
  25. 二進制
      src/fonts/roboto-light.woff2
  26. 二進制
      src/fonts/roboto-medium.woff
  27. 二進制
      src/fonts/roboto-medium.woff2
  28. 二進制
      src/fonts/roboto-regular.woff
  29. 二進制
      src/fonts/roboto-regular.woff2
  30. 二進制
      src/fonts/roboto-thin.woff
  31. 二進制
      src/fonts/roboto-thin.woff2
  32. 二進制
      src/fonts/robotomono-light.woff
  33. 二進制
      src/fonts/robotomono-light.woff2
  34. 31 0
      src/html/index.html
  35. 二進制
      src/img/android-chrome-192x192.png
  36. 二進制
      src/img/android-chrome-512x512.png
  37. 二進制
      src/img/apple-touch-icon.png
  38. 二進制
      src/img/favicon-16x16.png
  39. 二進制
      src/img/favicon-32x32.png
  40. 二進制
      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. 52 0
      src/js/components/AnswerScreen.jsx
  48. 18 0
      src/js/components/FinalScreen.jsx
  49. 275 0
      src/js/components/Index.jsx
  50. 42 0
      src/js/components/QuestionScreen.jsx
  51. 71 0
      src/js/components/ScoreScreen.jsx
  52. 18 0
      src/js/components/TitleScreen.jsx
  53. 41 0
      src/js/components/UserVotesScreen.jsx
  54. 86 0
      src/js/components/partials/AnswerItem.jsx
  55. 31 0
      src/js/components/partials/DonutGraphItem.jsx
  56. 35 0
      src/js/components/partials/ResponseOptionItem.jsx
  57. 54 0
      src/js/components/partials/VoteItem.jsx
  58. 37 0
      src/js/config.js
  59. 21 0
      src/js/content/module.json
  60. 352 0
      src/js/content/questions.json
  61. 94 0
      src/js/d3/axes.js
  62. 85 0
      src/js/d3/bar.js
  63. 81 0
      src/js/d3/donutchart.js
  64. 56 0
      src/js/d3/grid.js
  65. 43 0
      src/js/d3/increment.js
  66. 36 0
      src/js/d3/legend.js
  67. 37 0
      src/js/d3/symbols.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. 76 0
      src/js/utilities/formatter.js
  74. 6 0
      src/js/utilities/math.js
  75. 15 0
      src/js/utilities/randomizer.js
  76. 19 0
      src/scss/base/_fonts.scss
  77. 52 0
      src/scss/base/_forms.scss
  78. 44 0
      src/scss/base/_headings.scss
  79. 12 0
      src/scss/base/_links.scss
  80. 24 0
      src/scss/base/_rhythm.scss
  81. 34 0
      src/scss/base/_root.scss
  82. 17 0
      src/scss/config/_breakpoints.scss
  83. 32 0
      src/scss/config/_colors.scss
  84. 37 0
      src/scss/config/_defaults.scss
  85. 73 0
      src/scss/config/_fonts.scss
  86. 62 0
      src/scss/main.scss
  87. 21 0
      src/scss/modules/_animations.scss
  88. 115 0
      src/scss/modules/_answers.scss
  89. 29 0
      src/scss/modules/_boxes.scss
  90. 55 0
      src/scss/modules/_buttons.scss
  91. 32 0
      src/scss/modules/_content.scss
  92. 316 0
      src/scss/modules/_d3js.scss
  93. 10 0
      src/scss/modules/_debug.scss
  94. 11 0
      src/scss/modules/_footer.scss
  95. 92 0
      src/scss/modules/_forms.scss
  96. 45 0
      src/scss/modules/_grids.scss
  97. 33 0
      src/scss/modules/_header.scss
  98. 31 0
      src/scss/modules/_icons.scss
  99. 2 0
      src/scss/modules/_logos.scss
  100. 36 0
      src/scss/modules/_navs.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'
+};

+ 41 - 0
doc/01_introduction.md

@@ -0,0 +1,41 @@
+% Dokumentation
+% Tabea David <tabea.david@kf-interactive.com>; zitzmann@mpib-berlin.mpg.de
+
+<link rel="stylesheet" href="css/cssgithub-markdown.css" />
+<link rel="stylesheet" href="css/github-syntax-highlight.css" />
+
+## Inhalt
+
+- [Einführung](#einführung)
+- [Übersicht der Module](#übersicht-der-module)
+- [Verzeichnisstruktur](#verzeichnisstruktur)
+- [Modul 1: Risiken vergleichen](#modul-1-risiken-vergleichen)
+- [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. **Modul 1 – Risiken vergleichen** __\*__
+2. Modul 2 – Diagramme verstehen __\*__
+3. Modul 3 – Trends schätzen __\*__
+4. Modul 4 – Stichproben verstehen (original: [Rock 'n poll][rock-n-poll])
+5. Modul 5 – Relative Risiken verstehen __\*__
+6. Modul 6 – 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
+
+```

+ 71 - 0
doc/03_module.md

@@ -0,0 +1,71 @@
+## Modul 1: Risiken vergleichen
+
+Dieses Modul bietet die Möglichkeit, Risiken spielerisch miteinander zu vergleichen. Aus den präsentierten Paaren von Ereignissen muss jeweils dasjenige mit der höheren Eintrittswahrscheinlichkeit gewählt werden.
+Es wird unmittelbar angezeigt, ob die Schätzung richtig war. Die 'online'-Version der WebApp zeigt in den folgenden Ansichten, wie man im Vergleich zu anderen Benutzern abschneidet.
+
+### Javascript Verzeichnisstruktur
+
+Für einen besseren Überblick sind die Quell-Dateien unter `src/js` 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
+│   ├── AnswerScreen.jsx
+│   ├── FinalScreen.jsx
+│   ├── Index.jsx             // Web App Haupt-Komponente
+│   ├── QuestionScreen.jsx
+│   ├── ScoreScreen.jsx
+│   ├── TitleScreen.jsx
+│   ├── UserVotesScreen.jsx
+│   └── partials
+│       ├── AnswerItem.jsx
+│       ├── DonutGraphItem.jsx
+│       ├── HeaderLightItem.jsx
+│       ├── ResponseOptionItem.jsx
+│       └── VoteItem.jsx
+├── content                   // Definitionen der Inhalte
+│   ├── module.json           // Labels und Texte des User Interfaces
+│   └── questions.json        // Definitionen der Fragen
+├── d3                        // d3 Module
+│   ├── axes.js
+│   ├── donutchart.js
+│   ├── grid.js
+│   ├── increment.js
+│   ├── legend.js
+│   ├── line.js
+│   └── symbols.js
+└── utilities                 // Werkzeuge und Dienste
+    ├── api.js                // API für Lese- und Schreibzugriff auf die Datenbank
+    ├── enableTouch.js
+    ├── fonts.js
+    ├── formatter.js
+    └── randomizer.js
+```
+
+### Wie ändere ich Bezeichner und Datenbasis?
+
+#### *Offline* Version
+
+Die Inhalte der 'offline'-Version sind in den `json`-Dateien Dateien unter `src/js/content` definiert.
+
+#### Labels
+
+Titel, Texte und Labels sind in `module.json` definiert. Dort kann man z.B. den einleitenden Text und die Labels der Buttons ändern.
+
+#### Daten
+
+Die Datenbasis diese Moduls ist eine einfache Liste von Ereignissen. Ein Ereignis kann durch eine *ID* eindeutig identifiziert werden und besteht ansonsten aus der Bezeichnung und dem "Basisrisiko". Letzteres stellt die Häufigkeit des Ereignisses dar, bezogen auf die Referenzgröße 100 000:
+
+```
+  {
+    "id": 1,
+    "bezeichnung": "Tod durch Erklettern des Mount Everests",
+    "basisRisiko": 3500.00
+  }
+```
+
+#### *Online* Version
+
+<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

File diff suppressed because it is too large
+ 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;
+}

+ 216 - 0
doc/html/index.html

@@ -0,0 +1,216 @@
+<!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><strong>Module01 – Risiken vergleichen</strong> <strong>*</strong></li>
+<li>Module02 – Diagramme verstehen <strong>*</strong></li>
+<li>Module03 – Trends schätzen <strong>*</strong></li>
+<li>Module04 – Stichproben verstehen (original: <a href="http://rocknpoll.graphics/">Rock ’n poll</a>)</li>
+<li>Module05 – Relative Risiken verstehen <strong>*</strong></li>
+<li>Module06 – Wachstumsprozesse verstehen</li>
+</ol>
+<p>Alle mit einem <strong>*</strong> versehenen Module greifen über eine API auf eine Datenbank zu. Auf diese Weise rufen sie die benötigten Daten ab, und speichern andererseits Benutzerantworten, um Nutzern den Vergleich zu Anderen zu ermöglichen. Für jedes dieser Module existiert auch eine offline-Version, die ausschließlich auf lokale Daten zugreift.</p>
+<h2 id="verzeichnisstruktur">Verzeichnisstruktur</h2>
+<p>Im Wurzelverzeichnis liegen die für den Build Prozess notwendigen Konfigurationsdateien. Das Verzeichnis <code>doc/</code> enthält detailliertere Dokumentationen zu einzelnen Aspekten des Projekts. Alle für die WebApp benötigten Dateien werden in <code>public/</code> erstellt bzw. dorthin kopiert. Im <code>src/</code> Verzeichnis befinden sich alle Quelldateien, Bilder und Fonts. Der <code>tasks/</code> Ordner enthält Javascript-Dateien, die die Teilschritte des Build-Prozesses definieren.</p>
+<p>Die grobe Struktur sieht folgendermaßen aus:</p>
+<pre><code>
+├── .editorconfig        // Konfiguration für Texteditoren
+├── .babelrc             // Konfiguration von babel
+├── .eslintrc.yml        // Konfiguration des Javascript Linters
+├── .htmlhintrc          // Konfiguration des HTML Linters
+├── .sass-lint.yml       // Konfiguration des Sass Linters
+├── config.js            // Konfiguration des Build-Systems
+├── gulpfile.babel.js    // gulp Datei, verwendet Definitionen unter `tasks/`
+├── package.json         // npm Abhängigkeiten, Shortcuts für gulp tasks
+├── doc/                 // Dokumentation in markdown
+├── public/              // Zielverzeichnis für den Build-Prozess
+├── src/                 // Quellverzeichnis
+│   ├── fonts            // Font Dateien
+│   ├── html             // HTML &#39;Templates&#39;
+│   ├── img              // Bilder und Sprites
+│   ├── js               // Javascript Quelldateien
+│   └── scss             // Sass stylesheets
+└── tasks/               // Definitionen für den gulp Build-Prozess
+</code></pre>
+<h3 id="konventionen">Konventionen</h3>
+<p>Die Code Style Konventionen wurden von den ursprünglichen Entwicklern übernommen und nur an wenigen Stellen angepasst. Der Javascript Code ist in ES6 (bzw. ES2015) verfasst und als CSS-Preprocessor wird Sass mit der <code>scss</code> Syntax verwendet.</p>
+<p>Das Projekt verwendet <a href="http://editorconfig.org">editorconfig</a> für die Integration dieser Konventionen in Editoren, die entsprechende Datei heißt <code>.editorconfig</code>.</p>
+<p>Für die statische Überprüfung des Quellcodes werden folgende <em>Linter</em> verwendet:</p>
+<ul>
+<li>Javascript: <a href="https://eslint.org">eslint</a></li>
+<li>Sass: <a href="https://github.com/sasstools/sass-lint">sass-lint</a></li>
+<li>HTML: <a href="https://github.com/htmlhint/HTMLHint">HTMLHint</a></li>
+</ul>
+<p>Die zugehörigen Konfigurationsdateien befinden sich im Root-Verzeichnis, wie oben in der Auflistung angegeben.</p>
+<h2 id="modul-1-risiken-vergleichen">Modul 1: Risiken vergleichen</h2>
+<p>Dieses Modul bietet die Möglichkeit, Risiken spielerisch miteinander zu vergleichen. Aus den präsentierten Paaren von Ereignissen muss jeweils dasjenige mit der höheren Eintrittswahrscheinlichkeit gewählt werden. Es wird unmittelbar angezeigt, ob die Schätzung richtig war. Die ‘online’-Version der WebApp zeigt in den folgenden Ansichten, wie man im Vergleich zu anderen Benutzern abschneidet.</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
+│   ├── AnswerScreen.jsx
+│   ├── FinalScreen.jsx
+│   ├── Index.jsx             // Web App Haupt-Komponente
+│   ├── QuestionScreen.jsx
+│   ├── ScoreScreen.jsx
+│   ├── TitleScreen.jsx
+│   ├── UserVotesScreen.jsx
+│   └── partials
+│       ├── AnswerItem.jsx
+│       ├── DonutGraphItem.jsx
+│       ├── HeaderLightItem.jsx
+│       ├── ResponseOptionItem.jsx
+│       └── VoteItem.jsx
+├── content                   // Definitionen der Inhalte
+│   ├── module.json           // Labels und Texte des User Interfaces
+│   └── questions.json        // Definitionen der Fragen
+├── d3                        // d3 Module
+│   ├── axes.js
+│   ├── donutchart.js
+│   ├── grid.js
+│   ├── increment.js
+│   ├── legend.js
+│   ├── line.js
+│   └── symbols.js
+└── utilities                 // Werkzeuge und Dienste
+    ├── api.js
+    ├── enableTouch.js
+    ├── fonts.js
+    ├── formatter.js
+    └── randomizer.js</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-1">Konventionen</h3>
+<p>Der Javascript Code ist in ES6 (bzw. ES2015) verfasst. Als CSS-Preprocessor wird Sass mit der <code>scss</code> Syntax verwendet. Die Code Style Konventionen wurden von den ursprünglichen Entwicklern übernommen und nur an wenigen Stellen leicht angepasst.</p>
+<p>Erleichtert wird die Anwendung des definierten Programmierstils durch die Integration in Editoren mit Hilfe von <a href="http://editorconfig.org">editorconfig</a>. Die entsprechende Konfigurationsdatei ist im Wurzelverzeichnis als <code>.editorconfig</code> zu finden.</p>
+<p>Die statische Überprüfung des Javascript Quellcodes wird mit Hilfe von <a href="https://eslint.org">eslint</a> durchgeführt.Für Sass wird <a href="https://github.com/sasstools/sass-lint">sass-lint</a> verwendet und HTML wird mit Hilfe von <a href="https://github.com/htmlhint/HTMLHint">HTMLHint</a> überprüft.</p>
+<p>Die zugehörigen Konfigurationsdateien befinden sich im Wurzelverzeichnis, wie oben in der Auflistung angegeben. <a href="http://editorconfig.org">editorconfig</a>: http://editorconfig.org <a href="https://eslint.org">eslint</a>: https://eslint.org [gulp]: https://gulpjs.com/ <a href="https://github.com/htmlhint/HTMLHint">HTMLHint</a>: https://github.com/htmlhint/HTMLHint [nodejs]: https://nodejs.org [npm]: https://www.npmjs.com/ [pandoc]: https://pandoc.org <a href="https://github.com/sasstools/sass-lint">sass-lint</a>: 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-1">Verzeichnisstruktur</h3>
+<p>Die folgende Auflistung gibt einen groben Überblick über die Verzeichnisstruktur der Javascript Quelldateien in <code>src/js/</code>. Als Einstieg dient <code>main.jsx</code> bzw. <code>main-offline.jsx</code> für den “offline” Modus. <code>config.js</code> ist die zentrale Konfigurationsdatei der WebApp. In <code>components</code> liegen die Komponenten der <em>preact</em>-WebApp. Der Quellcode für die D3-Visualisierungen befindet sich unter <code>d3</code>.</p>
+<pre><code>├── main.jsx            // Einstiegspunkt für App in &quot;online&quot; Modus
+├── main-offline.jsx    // Einstiegspunkt für App in &quot;offline&quot; Modus
+├── config.js           // Konfiguration der WebApp
+├── components/         // Verzeichnis für (p)react Komponenten
+│   ├── Index.jsx       // Web App Haupt-Komponente
+│   └── partials/       // Vezeichnis für Teilkomponenten
+├── content/            // Verzeichnis für &quot;offline&quot; Inhalte
+├── d3/                 // d3 Module
+└── utilities/          // Verzeichnis für Hilfs-Bibliotheken und Werkzeuge</code></pre>
+<h2 id="sass">Sass</h2>
+<p>Zum Kompilieren von Sass zu CSS wird <code>gulp-sass</code> verwendet, das <code>node-sass</code> benutzt, welches wiederum auf <code>libsass</code> basiert. <code>node-sass</code> hat sich beim wiederholten gedankenlosen Aktualisieren von <code>nodejs</code> und / oder <code>npm</code> als notorischer Nerventöter herausgestellt, daher an dieser Stelle der <a href="https://github.com/sass/node-sass/blob/master/TROUBLESHOOTING.md">Verweis zur <em>Troubleshooting</em> Dokumentation</a> von <code>node-sass</code>. Meist reichte im Falle eines Problems aber ein <code>npm rebuild node-sass</code>.</p>
+<h3 id="struktur">Struktur</h3>
+<p>In der Datei <code>main.scss</code> werden alle Stile eingebunden, die in den <em>Partials</em> definiert werden, woraus die endgültige CSS-Datei generiert wird. Die Struktur des <code>src/scss</code> Verzeichnisses sieht folgendermaßen aus:</p>
+<pre><code>├── base     // Stile für HTML Elemente
+├── config   // Globale Variable
+├── modules  // Stile für Module
+└── tools    // Definierte *mixins* und Funktionen</code></pre>
+<h3 id="konventionen-techniken-und-tools">Konventionen, Techniken und Tools</h3>
+<p>Generell wird eine “mobile first” Strategie verfolgt. Als Standard-Einheit wird <code>rem</code> verwendet, auf deren Grundlage die Basis-Einheit definiert ist. Da sich alle Größen auf diese Einheit beziehen sollten, wird so das Skalieren des Layouts erleichtert.</p>
+<p>Sass wird in diesem Projekt mit der scss-Syntax verwendet. Stilistisch ist es in “oldschool BEM-Style” gehalten, Zitat der ursprünglichen Entwickler. Sie beziehen sich zudem auf bestimmte Guidelines:</p>
+<blockquote>
+<p>Hugo Giraudel wrote an awesome piece on everything you need to know about Sass, it’s called <a href="http://sass-guidelin.es/">Sass Guidelines</a> and you should really have a look at it. I agree with this guideline in almost all points, but I try to keep something more simple, and some things more strict, the linter will let you know :)</p>
+</blockquote>
+<p>ʕ̡̢̡ॢ•̫͡ॢ•ʔ̢̡̢</p>
+<p>Regeln mit Browser-spezifischen Präfixen (<em>vendor prefixes</em>) werden dem CSS automatisch durch <a href="https://github.com/postcss/autoprefixer">autoprefixer</a> hinzugefügt. Die Liste der zu unterstützenden Browser ist in <code>package.json</code> unter <code>browserslist</code> zu finden.</p>
+<h2 id="bilddateien">Bilddateien</h2>
+<p>Alle in diesem Projekt verwendeten Bilddateien befinden sich unter <code>src/img/</code>.</p>
+<p>Icons in Form von SVG-Dateien befinden sich im Unterordner <code>src/img/sprites</code> und werden im Build-Prozess mittels <code>gulp-svg-sprite</code> zu einem Sprite zusammengefasst. Sie wie folgt in HTML referenziert werden:</p>
+<pre><code>&lt;svg class=&quot;icon  icon--arrow-left&quot;&gt;
+  &lt;use xlink:href=&quot;assets/img/sprites.svg#icon--arrow-left&quot;/&gt;
+&lt;/svg&gt;</code></pre>
+<p>Stil-Definitionen für Icons sind unter <code>src/scss/modules/_icons.scss</code> zu finden. Für die Unterstützung von Fragmentbezeichnern (<em>fragment identifier</em>) in Internet Explorer wird <a href="https://github.com/Keyamoon/svgxuse">svgxuse</a> verwendet.</p>
+</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": "comparing-risks",
+  "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>

二進制
public/favicon.ico


+ 5 - 0
public/robots.txt

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

+ 392 - 0
readme.md

@@ -0,0 +1,392 @@
+`<link rel="stylesheet" href="css/cssgithub-markdown.css" />`{=html}
+`<link rel="stylesheet" href="css/github-syntax-highlight.css" />`{=html}
+
+Inhalt
+------
+
+-   [Einführung](#einführung)
+-   [Übersicht der Module](#übersicht-der-module)
+-   [Verzeichnisstruktur](#verzeichnisstruktur)
+-   [Modul 1: Risiken vergleichen](#modul-1-risiken-vergleichen)
+-   [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.  **Modul 1 -- Risiken vergleichen** **\***
+2.  Modul 2 -- Diagramme verstehen **\***
+3.  Modul 3 -- Trends schätzen **\***
+4.  Modul 4 -- Stichproben verstehen (original: [Rock 'n
+    poll](http://rocknpoll.graphics/))
+5.  Modul 5 -- Relative Risiken verstehen **\***
+6.  Modul 6 -- 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 1: Risiken vergleichen
+----------------------------
+
+Dieses Modul bietet die Möglichkeit, Risiken spielerisch miteinander zu
+vergleichen. Aus den präsentierten Paaren von Ereignissen muss jeweils
+dasjenige mit der höheren Eintrittswahrscheinlichkeit gewählt werden. Es
+wird unmittelbar angezeigt, ob die Schätzung richtig war. Die
+'online'-Version der WebApp zeigt in den folgenden Ansichten, wie man im
+Vergleich zu anderen Benutzern abschneidet.
+
+### Javascript Verzeichnisstruktur
+
+Für einen besseren Überblick sind die Quell-Dateien unter `src/js` 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
+    │   ├── AnswerScreen.jsx
+    │   ├── FinalScreen.jsx
+    │   ├── Index.jsx             // Web App Haupt-Komponente
+    │   ├── QuestionScreen.jsx
+    │   ├── ScoreScreen.jsx
+    │   ├── TitleScreen.jsx
+    │   ├── UserVotesScreen.jsx
+    │   └── partials
+    │       ├── AnswerItem.jsx
+    │       ├── DonutGraphItem.jsx
+    │       ├── HeaderLightItem.jsx
+    │       ├── ResponseOptionItem.jsx
+    │       └── VoteItem.jsx
+    ├── content                   // Definitionen der Inhalte
+    │   ├── module.json           // Labels und Texte des User Interfaces
+    │   └── questions.json        // Definitionen der Fragen
+    ├── d3                        // d3 Module
+    │   ├── axes.js
+    │   ├── donutchart.js
+    │   ├── grid.js
+    │   ├── increment.js
+    │   ├── legend.js
+    │   ├── line.js
+    │   └── symbols.js
+    └── utilities                 // Werkzeuge und Dienste
+        ├── api.js                // API für Lese- und Schreibzugriff auf die Datenbank
+        ├── enableTouch.js
+        ├── fonts.js
+        ├── formatter.js
+        └── randomizer.js
+
+### Wie ändere ich Bezeichner und Datenbasis?
+
+#### *Offline* Version
+
+Die Inhalte der 'offline'-Version sind in den `json`-Dateien Dateien
+unter `src/js/content` definiert.
+
+#### Labels
+
+Titel, Texte und Labels sind in `module.json` definiert. Dort kann man
+z.B. den einleitenden Text und die Labels der Buttons ändern.
+
+#### Daten
+
+Die Datenbasis diese Moduls ist eine einfache Liste von Ereignissen. Ein
+Ereignis kann durch eine *ID* eindeutig identifiziert werden und besteht
+ansonsten aus der Bezeichnung und dem "Basisrisiko". Letzteres stellt
+die Häufigkeit des Ereignisses dar, bezogen auf die Referenzgröße 100
+000:
+
+      {
+        "id": 1,
+        "bezeichnung": "Tod durch Erklettern des Mount Everests",
+        "basisRisiko": 3500.00
+      }
+
+#### *Online* Version
+
+`<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.

二進制
src/fonts/roboto-light.woff


二進制
src/fonts/roboto-light.woff2


二進制
src/fonts/roboto-medium.woff


二進制
src/fonts/roboto-medium.woff2


二進制
src/fonts/roboto-regular.woff


二進制
src/fonts/roboto-regular.woff2


二進制
src/fonts/roboto-thin.woff


二進制
src/fonts/roboto-thin.woff2


二進制
src/fonts/robotomono-light.woff


二進制
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>

二進制
src/img/android-chrome-192x192.png


二進制
src/img/android-chrome-512x512.png


二進制
src/img/apple-touch-icon.png


二進制
src/img/favicon-16x16.png


二進制
src/img/favicon-32x32.png


二進制
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>

+ 52 - 0
src/js/components/AnswerScreen.jsx

@@ -0,0 +1,52 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import { getFormattedValue, needsSpacing, getPrecision, getSpacing } from '../utilities/formatter';
+import AnswerItem from './partials/AnswerItem.jsx';
+
+// wrapper output for the current answer
+export default class AnswerScreen extends Component {
+
+  render () {
+    const navigateTo = this.props.isOffline ? 'score' : 'votes';
+    const isLast = this.props.answers.length >= this.props.numberQuestions;
+    const footerLabel = isLast ? this.props.endscore : this.props.interimscore;
+
+    const risks = [];
+    const fvals = [];
+    risks[0] = this.props.currentQuestionPair[0].basisRisiko;
+    risks[1] = this.props.currentQuestionPair[1].basisRisiko;
+    fvals[0] = getFormattedValue(risks[0]);
+    fvals[1] = getFormattedValue(risks[1]);
+    const shouldBeSpaced = needsSpacing(fvals[0], fvals[1]);
+
+    return (
+      <section className="wrapper  wrapper__answer">
+        <main className="wrapper__main  wrapper--centered">
+          {
+            this.props.currentQuestionPair.map((item, index) => {
+
+              const targetValue = fvals[index];
+              const correctChoice = item.basisRisiko === Math.max(risks[0], risks[1]);
+
+              return (
+                <AnswerItem key={ index }
+                  index={index}
+                  correctChoice={correctChoice}
+                  targetValue={ targetValue }
+                  precision={ getPrecision(targetValue) }
+                  needsSpacing={ shouldBeSpaced }
+                  spaces={ getSpacing(targetValue) }
+                  {...item} />
+              );
+            })
+          }
+        </main>
+        <footer className="footer footer--answer">
+          <a href="#" title={footerLabel} className="button--wide" onClick={ () => this.props.navigate(navigateTo) }>
+            { this.props.isOffline ? footerLabel : this.props.otherUsers }
+          </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
+
+// displays outro text and a link to restart the module
+const FinalScreen = props => ( // eslint-disable-line no-unused-vars
+  <section class="wrapper">
+    <header className="header">
+      <div className="number--square  number--square--huge"><span>{props.number}</span></div>
+      <h1 className="header__title">{props.title}</h1>
+    </header>
+    <main className="intro">{props.outrotext.split('\n').map(item => <span>{item}<br /></span>)}</main>
+    <footer className="footer footer--titlescreen">
+      { !props.isFetching ? <a href="#" title={props.restart} className="button--wide" onClick={() => props.navigate('titlescreen')}>
+        {props.restart}</a> : [] }
+    </footer>
+  </section>
+);
+
+export default FinalScreen;

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

@@ -0,0 +1,275 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+// content and config:
+import content from '../content/module.json';
+import questions from '../content/questions.json';
+import config from '../config.js';
+// services and helper:
+import { haveSameRepresentation } from '../utilities/formatter';
+import api from '../utilities/api';
+// screens and items:
+import TitleScreen from './TitleScreen.jsx';
+import QuestionScreen from './QuestionScreen.jsx';
+import AnswerScreen from './AnswerScreen.jsx';
+import UserVotesScreen from './UserVotesScreen.jsx';
+import ScoreScreen from './ScoreScreen.jsx';
+
+import * as d3 from 'd3';
+import createSymbols from '../d3/symbols';
+
+/**
+ * titlescreen -> (question -> answer -> votes -> score) * n
+ */
+export default class App extends Component {
+
+  // construct and initialize functions
+  constructor (props) {
+    super(props);
+
+    this.state = {
+      route: 'titlescreen',             // current screen
+      currentQuestionPair: null,        // current question pair to be answered
+      currentQuestionPairId: null,      // current question pair id
+      currentQuestionCorrect: null,     // is the currently set question the correct one?
+      currentQuestionAnswerIndex: null, // index in question pair of the currently set answer
+      answers: [],                      // list of all given answers
+      voteStats: [],                    // get votes from other users
+      isFetching: false,                // currently performing XHR?
+      userId: null,                     // current user id for posting
+      token: null,                      // current access token
+      isFlipped: false                  // flip conent order
+    };
+
+    if (window.location.hash) this.state.route = window.location.hash.replace('#', '');
+
+    // context binding
+    this.navigate = this.navigate.bind(this);
+    this.setAnswer = this.setAnswer.bind(this);
+    this.drawQuestionPair = this.drawQuestionPair.bind(this);
+    this.newQuestion = this.newQuestion.bind(this);
+    this.reset = this.reset.bind(this);
+    this.setup = this.setup.bind(this);
+    this.getVotes = this.getVotes.bind(this);
+    this.endUserSession = this.endUserSession.bind(this);
+  }
+
+  // initial setup
+  setup () {
+    if (this.props.isOffline) {
+      this.drawQuestionPair();
+    } else {
+      this.setState({ isFetching: true });
+
+      // get access token from api
+      api.getToken().then(accessToken => {
+        this.setState({ token: accessToken });
+
+        // create user
+        api.createUser(accessToken)
+          .then(user => {
+            this.setState({
+              userId: user.userId,
+              isFetching: false
+            });
+
+            // start with first question pair
+            this.drawQuestionPair();
+          });
+      });
+    }
+    // create svg symbols
+    let selection = d3.select('body');
+    let symbols = createSymbols();
+    selection.call(symbols);
+
+  }
+
+  // set navigation state
+  navigate (route) {
+    this.setState({ route });
+  }
+
+  // end user session
+  endUserSession () {
+    if (!this.props.isOffline) {
+      api.endSession(this.state.userId, this.state.token);
+    }
+  }
+
+  // show question pair
+  drawQuestionPair () {
+    let flip = 0;
+
+    if (this.props.isOffline) {
+
+      const firstIndex = Math.round(Math.random() * (questions.length - 1));
+      let secondIndex = Math.round(Math.random() * (questions.length - 1));
+
+      // Draw another 'question pair' if risk value respresentations are equal (true equality, rounded numbers)
+      while (haveSameRepresentation(questions[firstIndex].basisRisiko, questions[secondIndex].basisRisiko)) {
+        secondIndex = Math.round(Math.random() * (questions.length - 1));
+      }
+
+      this.setState({ currentQuestionPair: [ questions[firstIndex], questions[secondIndex] ] });
+    } else {
+      flip = Math.round(Math.random()); // randomly decide whether to flip answer positions
+      this.setState({ isFetching: true });
+      api.get(config.api.random).then(json => {
+        if (Object.prototype.hasOwnProperty.call(json, 'paar')) {
+
+          // Draw another 'question pair' if risk value respresentations are equal (true equality, rounded numbers)
+          if (haveSameRepresentation(json.paar[0].basisRisiko, json.paar[1].basisRisiko)) {
+            this.drawQuestionPair();
+          } else {
+
+            this.setState({
+              currentQuestionPairId: json.id,
+              isFetching: false,
+              currentQuestionPair: flip === 1 ? [ json.paar[1], json.paar[0] ] : json.paar,
+              isFlipped: flip === 1
+            });
+          }
+        }
+      });
+    }
+  }
+
+  // get user votes from API
+  getVotes () {
+    this.setState({ isFetching: true });
+    api.get(config.api.proportions, { pairId: this.state.currentQuestionPairId })
+      .then(json => {
+        const updatedQuestionPair = [ ...this.state.currentQuestionPair ];
+        const sumVotes = json.paar[0].anzahlStimmen + json.paar[1].anzahlStimmen;
+        const newVoteStats = [ ...this.state.voteStats ];
+
+        if (sumVotes === 0) { // prevent division by zero (although sumVotes should always be at least 1)
+          updatedQuestionPair[0].votesFraction = 0;
+          updatedQuestionPair[1].votesFraction = 0;
+          newVoteStats.push(0);
+        } else {
+          updatedQuestionPair[0].votesFraction = (this.state.isFlipped ? json.paar[1].anzahlStimmen : json.paar[0].anzahlStimmen) / sumVotes;
+          updatedQuestionPair[1].votesFraction = (this.state.isFlipped ? json.paar[0].anzahlStimmen : json.paar[1].anzahlStimmen) / sumVotes;
+          newVoteStats.push(
+            // if I am correct, subtract me from other correct users
+            (this.state.currentQuestionCorrect ? json.paar[0].anzahlStimmen - 1 : json.paar[0].anzahlStimmen) / sumVotes
+          );
+        }
+
+        this.setState({
+          currentQuestionPair: updatedQuestionPair,
+          isFetching: false,
+          voteStats: newVoteStats
+        });
+      });
+  }
+
+  // set and post answer from current user
+  setAnswer (answerIndex) {
+    const answerIsCorrect = this.state.currentQuestionPair[answerIndex].basisRisiko === Math.max(
+      this.state.currentQuestionPair[0].basisRisiko,
+      this.state.currentQuestionPair[1].basisRisiko); // is answer the one with the highest risk?
+
+    this.setState({
+      currentQuestionAnswerIndex: answerIndex,
+      currentQuestionCorrect: answerIsCorrect,
+      answers: [ ...this.state.answers, { isCorrect: answerIsCorrect } ]
+    });
+
+    if (!this.props.isOffline) {
+      api.post(config.api.create, {
+        userId: this.state.userId,
+        risikoPaarId: this.state.currentQuestionPairId,
+        correct: answerIsCorrect
+      }, this.state.token);
+    }
+  }
+
+  // get new question
+  newQuestion () {
+    this.drawQuestionPair();
+    this.setState({
+      route: 'question',
+      currentQuestionCorrect: null,
+      currentQuestionAnswerIndex: null
+    });
+  }
+
+  // reset everything to restart the module
+  reset () {
+    this.setState({
+      route: 'titlescreen',             // current screen
+      currentQuestionPair: null,        // current question pair to be answered
+      currentQuestionCorrect: null,     // is the currently set question the correct one?
+      currentQuestionAnswerIndex: null, // index in question pair of the currently set answer
+      answers: [],                      // list of all given answers
+      voteStats: [],
+      isFetching: false,                 // currently performing XHR?
+      currentQuestionPairId: null,
+      isFlipped: false
+    });
+    this.setup();
+  }
+
+  // LIFECYLCE
+  componentWillMount () {
+    this.setup();
+  }
+
+  // RENDER
+  render () {
+    let outputContent;
+
+    switch (this.state.route) {
+
+      case 'question':
+        outputContent = <QuestionScreen
+          {...content}
+          {...this.state}
+          {...config}
+          setAnswer={this.setAnswer}
+          navigate={this.navigate}
+          isFetching={this.state.isFetching} />;
+        break;
+
+      case 'answer':
+        outputContent = <AnswerScreen
+          {...content}
+          {...this.state}
+          {...config}
+          isOffline={this.props.isOffline}
+          navigate={this.navigate} />;
+        break;
+
+      case 'score':
+        outputContent = <ScoreScreen
+          {...content}
+          {...this.state}
+          {...config}
+          newQuestion={this.newQuestion}
+          isOffline={this.props.isOffline}
+          endUserSession={this.endUserSession}
+          reset={this.reset} />;
+        break;
+
+      case 'votes':
+        outputContent = <UserVotesScreen
+          {...content}
+          {...this.state}
+          {...config}
+          navigate={this.navigate}
+          getVotes={this.getVotes} />;
+        break;
+
+      case 'titlescreen':
+      default:
+        outputContent = <TitleScreen
+          {...content}
+          navigate={ this.navigate }
+          navigateTo='question'
+          isFetching={this.state.isFetching} />;
+    }
+
+    return outputContent;
+  }
+
+}

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

@@ -0,0 +1,42 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import ResponseOptionItem from './partials/ResponseOptionItem.jsx';
+
+export default class QuestionScreen extends Component {
+
+  // render question including response option items
+  render () {
+
+    return (
+      <section className="wrapper  wrapper__question">
+        <main className="wrapper__main">
+          { this.props.isFetching
+            ? <div>Loading question</div>
+            : <fieldset>
+              <legend>{this.props.title}</legend>
+              { this.props.currentQuestionPair.map((option, index) => (
+                <ResponseOptionItem
+                  key={index}
+                  index={index}
+                  {...option}
+                  correct={this.props.currentQuestionCorrect}
+                  isMyAnswer={this.props.currentQuestionAnswerIndex === index}
+                  setAnswer={ this.props.currentQuestionAnswerIndex === null
+                    ? this.props.setAnswer
+                    : () => {} } /> // eslint-disable-line no-empty-function
+              )
+              ) }
+            </fieldset>
+          }
+        </main>
+        { this.props.currentQuestionAnswerIndex !== null
+          ? <footer className="footer footer--question">
+            <a href="#" title="Start" className="button--wide" onClick={ () => this.props.navigate('answer') }>
+              {this.props.solution}
+            </a>
+          </footer>
+          : [] }
+      </section>
+    );
+  }
+
+}

+ 71 - 0
src/js/components/ScoreScreen.jsx

@@ -0,0 +1,71 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import DonutGraphItem from './partials/DonutGraphItem.jsx';
+
+// render the current score including animated donut graph
+export default class ScoreScreen extends Component {
+
+  // construct properties
+  constructor (props) {
+    super(props);
+    this.state = {
+      myScore: this.getMyScore(),
+      otherUsersScore: this.getOtherUsersScore()
+    };
+  }
+
+  // get score from current user
+  getMyScore () {
+    let numberCorrect = 0;
+
+    [ ...this.props.answers ].forEach(item => {
+      numberCorrect += item.isCorrect ? 1 : 0;
+    });
+    return numberCorrect / this.props.answers.length;
+  }
+
+  // get score from other users
+  getOtherUsersScore () {
+    let sum = 0;
+
+    [ ...this.props.voteStats ].forEach(stat => {
+      sum += stat;
+    });
+    return sum / this.props.voteStats.length;
+  }
+
+  // LIFECYLCE methods
+  componentDidMount () {
+    if (this.props.answers.length >= this.props.numberQuestions) this.props.endUserSession();
+  }
+
+  // ... and render it
+  render () {
+    const isLast = this.props.answers.length >= this.props.numberQuestions;
+
+    return (
+      <section className="wrapper  wrapper__score">
+        <main className="wrapper__main  wrapper--centered">
+          <div className="score-item">
+            <span className="text-min-mod1">
+              { isLast ? this.props.endscorelabel : this.props.interimscorelabel }
+            </span>
+            <span className="number--huge  number--huge--spaced">{Math.round(this.state.myScore * 100)}%</span> korrekt.
+            <DonutGraphItem endval={this.state.myScore} />
+          </div>
+          { !this.props.isOffline
+            ? <div className="score-item">
+              <span className="text-min-mod1">{this.props.othersscorelabel}</span>
+              <span className="number--huge  number--huge--spaced">{Math.round(this.state.otherUsersScore * 100)}%</span> korrekt.
+              <DonutGraphItem endval={this.state.otherUsersScore} />
+            </div> : [] }
+        </main>
+        <footer className="footer footer--question">
+          { !isLast
+            ? <a href="#" title="Start" className="button--wide" onClick={ this.props.newQuestion }>{this.props.next}</a>
+            : <a href="#" title="Start" className="button--wide" onClick={ this.props.reset }>Zum Start</a> }
+        </footer>
+      </section>
+    );
+  }
+
+}

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

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

+ 41 - 0
src/js/components/UserVotesScreen.jsx

@@ -0,0 +1,41 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import VoteItem from './partials/VoteItem.jsx';
+
+// outputs the current user vote result
+export default class UserVotesScreen extends Component {
+
+  // LIFECYLCE methods
+  componentWillMount () {
+    this.props.getVotes();
+  }
+
+  // render user vote result
+  render () {
+    return (
+      <section className="wrapper  wrapper__votes">
+        <main className="wrapper__main  wrapper--centered vote">
+          <h3 className="vote__headline">So wählten die User</h3>
+          { this.state.isFetching ? 'lädt Statistik' : this.props.currentQuestionPair.map(
+            item => {
+              const activeColor = item.basisRisiko === Math.max(
+                this.props.currentQuestionPair[0].basisRisiko,
+                this.props.currentQuestionPair[1].basisRisiko
+              ) ? '#7ed321' : '#D0021B';
+
+              return <VoteItem label={item.bezeichnung} votesFraction={item.votesFraction} activeColor={activeColor} />;
+            }
+          ) }
+        </main>
+        <footer className="footer footer--answer">
+          <a href="#"
+            title={ this.props.answers.length >= this.props.numberQuestions ? this.props.endscore : this.props.interimscore }
+            className="button--wide"
+            onClick={ () => this.props.navigate('score') }>
+            { this.props.answers.length >= this.props.numberQuestions ? this.props.endscore : this.props.interimscore }
+          </a>
+        </footer>
+      </section>
+    );
+  }
+
+}

+ 86 - 0
src/js/components/partials/AnswerItem.jsx

@@ -0,0 +1,86 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import d3Increment from '../../d3/increment';
+import * as d3 from 'd3';
+
+// outputs the user vote result
+export default class AnswerItem extends Component {
+
+  // construct properties
+  constructor (props) {
+    super(props);
+
+    const targetValue = this.props.targetValue;
+    const precision = this.props.precision;
+    const needsSpacing = this.props.needsSpacing;
+    const spaces = this.props.spaces;
+
+    this.options = {
+      tag: 'span',
+      classname: 'number--huge--increment',
+      targetValue,
+      precision,
+      needsSpacing,
+      spaces
+    };
+  }
+
+  // LIFECYCLE methods
+  componentDidMount () {
+    const countup = d3Increment().options(this.options);
+
+    d3.select(this.container).call(countup);
+  }
+
+  // special requirement to add spaces in numbers
+  // add them as non breaking space(s) to keep them equaly wide using a monospaced font
+  getSpaces () {
+    let suffix = '';
+
+    if (this.options.needsSpacing === 0) {
+      return suffix;
+    }
+
+    switch (this.options.spaces) {
+      case 1:
+        suffix = <span>&nbsp;</span>;
+        break;
+      case 2:
+        suffix = <span>&nbsp;&nbsp;</span>;
+        break;
+      case 3:
+        suffix = <span>&nbsp;&nbsp;&nbsp;</span>;
+        break;
+
+      default:
+        suffix = '';
+    }
+
+    return suffix;
+  }
+
+  // get output
+  render () {
+    const isCorrectChoice = this.props.correctChoice ? 'correct' : 'incorrect';
+    const prefix = this.props.basisRisiko < 0.1 ? '< ' : '';
+    const suffix = this.getSpaces();
+
+    return (
+      <dl className={`answer-item  answer-item--mono  answer-item--${isCorrectChoice}`}>
+        <dt className="answer-item__title">{this.props.bezeichnung}</dt>
+        <dd className="answer-item__value">
+
+          <span className="number--huge  number--huge--increment">
+            {prefix}
+            <span ref={ elem => (this.container = elem) }></span>
+            {suffix}
+          </span>
+
+          <span className="number--huge--after">von</span>
+          <span className="number--huge">100.000</span>
+          <span className="number--huge--after">Personen</span>
+        </dd>
+      </dl>
+    );
+  }
+
+}

+ 31 - 0
src/js/components/partials/DonutGraphItem.jsx

@@ -0,0 +1,31 @@
+import { render, h, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import donutchart from '../../d3/donutchart';
+import * as d3 from 'd3';
+
+// render animated donut graph to display the distribution
+export default class DonutGraphItem extends Component {
+
+  // set data and call d3js module to animate donut graph
+  setData () {
+    const donut = donutchart().options({ endval: this.props.endval });
+
+    d3.select(this.container).call(donut);
+  }
+
+  // LIFECYCLE methods
+  componentDidMount () {
+    this.setData();
+  }
+
+  componentWillReceiveProps () {
+    this.setData();
+  }
+
+  // output entry point for d3js module
+  render () {
+    return (
+      <span className="js-donut  score-item__donut" ref={ elem => (this.container = elem) }></span>
+    );
+  }
+
+}

+ 35 - 0
src/js/components/partials/ResponseOptionItem.jsx

@@ -0,0 +1,35 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+
+// render the response options for the current question
+export default class ResponseOptionItem extends Component {
+
+  render () {
+    let correctness = '';
+
+    // check whether there's an answer as well as answer state
+    if (this.props.isMyAnswer) correctness = this.props.correct ? 'correct' : 'incorrect';
+
+    // set class to mark answer state, default: add class to add padding left
+    // to center everything correctly and prevent jumping
+    const addClass = correctness ? `response-option__label--${correctness}` : 'response-option__label--initial';
+
+    return (
+      <div className="response-option">
+        <input type="radio" id={`answer${this.props.index}`} name="question" value={this.props.index} />
+        <label for={`answer${this.props.index}`}
+          className={`response-option__label  ${addClass}`}
+          onClick={() => this.props.setAnswer(this.props.index) }>
+
+          <span className="response-option__label__text">
+            { correctness ? <svg className={`icon  icon--${correctness}`}>
+              <use xlinkHref={`#icon--${correctness}`}></use>
+            </svg> : [] }
+            {this.props.bezeichnung}
+          </span>
+
+        </label>
+      </div>
+    );
+  }
+
+}

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

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

+ 37 - 0
src/js/config.js

@@ -0,0 +1,37 @@
+// basic config containing global api base URL
+export default {
+  numberQuestions: 10,
+  api: {
+    random: 'M1_Risikopaare/Random',
+    create: 'M1_RisikopaareUserData/Create',
+    proportions: 'M1_RisikopaareUserData/CountsByPairId/',
+    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'
+    }
+  }
+};

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

@@ -0,0 +1,21 @@
+{
+	"number": "01",
+	"title": "Was ist wahrscheinlicher innerhalb eines Jahres?",
+  "introtext": "Es ist von Vorteil zu wissen, wie wahrscheinlich verschiedene Risiken sind: Es hilft bei Entscheidungen, es hilft bei der Lebensplanung und es hilft, sich zu beruhigen. Sie lernen im Folgenden spielerisch, worüber Sie sich wirklich Gedanken machen sollten.",
+  "answerprefix": "Ihre Antwort",
+  "correct": "Richtig",
+  "incorrect": "Falsch",
+  "next": "Nächste Frage",
+  "start": "Start",
+  "solution": "Auflösung",
+  "otherUsers": "Alle anderen Nutzer",
+  "of": "von",
+  "people": "Personen",
+  "uservoteheadline": "So entschieden sich andere Nutzer",
+  "choose": "Bitte wählen Sie",
+  "interimscore": "Zwischenstand",
+  "endscore": "Endstand",
+  "interimscorelabel": "Ihr Zwischenstand:",
+  "endscorelabel": "Ihr Endstand:",
+  "othersscorelabel": "Alle anderen Nutzer:"
+}

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

@@ -0,0 +1,352 @@
+[
+  {
+    "id": 1,
+    "bezeichnung": "Tod durch Erklettern des Mount Everests",
+    "basisRisiko": 3500.00
+  },
+  {
+    "id": 2,
+    "bezeichnung": "Tod durch Bypass-Operation",
+    "basisRisiko": 1600.00
+  },
+  {
+    "id": 3,
+    "bezeichnung": "Tod durch Krankheit des Kreislaufsystems",
+    "basisRisiko": 418.55
+  },
+  {
+    "id": 4,
+    "bezeichnung": "Tod durch Krebserkrankung",
+    "basisRisiko": 256.17
+  },
+  {
+    "id": 5,
+    "bezeichnung": "Tod durch Herzkrankheit mit Durchblutungsstörung",
+    "basisRisiko": 150.02
+  },
+  {
+    "id": 6,
+    "bezeichnung": "Tod durch Herzkrankheit ohne Durchblutungsstörung",
+    "basisRisiko": 98.69
+  },
+  {
+    "id": 7,
+    "bezeichnung": "Tod durch Krankheit des Atmungssystems",
+    "basisRisiko": 72.56
+  },
+  {
+    "id": 8,
+    "bezeichnung": "Tod durch Blutgefäßerkrankung im Gehirn",
+    "basisRisiko": 68.38
+  },
+  {
+    "id": 9,
+    "bezeichnung": "Tod durch Herzattacke",
+    "basisRisiko": 62.03
+  },
+  {
+    "id": 10,
+    "bezeichnung": "Tod durch Bösartige Neubildung in den Atemwegen",
+    "basisRisiko": 55.18
+  },
+  {
+    "id": 11,
+    "bezeichnung": "Tod durch Krankheiten des Verdauungssystems",
+    "basisRisiko": 47.71
+  },
+  {
+    "id": 12,
+    "bezeichnung": "Tod durch Hochdruckkrankheit",
+    "basisRisiko": 46.04
+  },
+  {
+    "id": 13,
+    "bezeichnung": "Tod durch Psychische und Verhaltensstörungen",
+    "basisRisiko": 44.49
+  },
+  {
+    "id": 14,
+    "bezeichnung": "Tod durch Chronische Krankheiten der unteren Atemwege",
+    "basisRisiko": 37.14
+  },
+  {
+    "id": 15,
+    "bezeichnung": "Tod durch Trinken von Wein",
+    "basisRisiko": 36.50
+  },
+  {
+    "id": 16,
+    "bezeichnung": "Tod durch Fahren mit dem Motorrad",
+    "basisRisiko": 36.50
+  },
+  {
+    "id": 17,
+    "bezeichnung": "Tod durch Hormon-, Ernährungs- u. Stoffwechselkrankheiten",
+    "basisRisiko": 36.24
+  },
+  {
+    "id": 18,
+    "bezeichnung": "Tod durch Krankheiten der Nervensysteme und Sinnesorgane",
+    "basisRisiko": 32.05
+  },
+  {
+    "id": 19,
+    "bezeichnung": "Tod durch Unfälle mit Spätfolgen",
+    "basisRisiko": 28.20
+  },
+  {
+    "id": 20,
+    "bezeichnung": "Tod durch Diabetes Mellitus",
+    "basisRisiko": 28.03
+  },
+  {
+    "id": 21,
+    "bezeichnung": "Tod durch Krankheiten des Urogenitalsystems",
+    "basisRisiko": 27.54
+  },
+  {
+    "id": 22,
+    "bezeichnung": "Tod durch Zigaretten rauchen",
+    "basisRisiko": 25.55
+  },
+  {
+    "id": 23,
+    "bezeichnung": "Tod durch Infektiöse und parasitäre Krankheiten",
+    "basisRisiko": 22.27
+  },
+  {
+    "id": 24,
+    "bezeichnung": "Tod durch Krankheiten der Blutgefäße",
+    "basisRisiko": 22.13
+  },
+  {
+    "id": 25,
+    "bezeichnung": "Tod durch Krankheiten der Niere",
+    "basisRisiko": 20.81
+  },
+  {
+    "id": 26,
+    "bezeichnung": "Tod durch Schlaganfall",
+    "basisRisiko": 20.74
+  },
+  {
+    "id": 27,
+    "bezeichnung": "Tod durch Lungenentzündung",
+    "basisRisiko": 20.69
+  },
+  {
+    "id": 28,
+    "bezeichnung": "Tod durch Krankheiten der Leber",
+    "basisRisiko": 18.25
+  },
+  {
+    "id": 29,
+    "bezeichnung": "Tod durch Leberschädigungen",
+    "basisRisiko": 16.75
+  },
+  {
+    "id": 30,
+    "bezeichnung": "Tod durch Erkrankungen an Herzklappen und Herzwand",
+    "basisRisiko": 16.05
+  },
+  {
+    "id": 31,
+    "bezeichnung": "Tod durch Stürze",
+    "basisRisiko": 14.34
+  },
+  {
+    "id": 32,
+    "bezeichnung": "Tod durch Vorsätzliche Selbstbeschädigung",
+    "basisRisiko": 12.64
+  },
+  {
+    "id": 33,
+    "bezeichnung": "Tod durch Psychische und Verhaltensstörungen durch Alkohol",
+    "basisRisiko": 6.33
+  },
+  {
+    "id": 34,
+    "bezeichnung": "Tod durch Alkoholmissbrauch",
+    "basisRisiko": 6.26
+  },
+  {
+    "id": 35,
+    "bezeichnung": "Tod durch Transportmittelunfälle",
+    "basisRisiko": 4.61
+  },
+  {
+    "id": 36,
+    "bezeichnung": "Tod durch Krankheiten des Muskel-Skelett-Systems u. Bindegewebes",
+    "basisRisiko": 4.11
+  },
+  {
+    "id": 37,
+    "bezeichnung": "Tod durch Krankheiten des Blutes und der blutbildenden Organe",
+    "basisRisiko": 3.63
+  },
+  {
+    "id": 38,
+    "bezeichnung": "Tod durch Magengeschwüre",
+    "basisRisiko": 2.79
+  },
+  {
+    "id": 39,
+    "bezeichnung": "Tod durch Kanufahren für zwei Stunden",
+    "basisRisiko": 2.00
+  },
+  {
+    "id": 40,
+    "bezeichnung": "Tod durch Krankheiten der Haut und der Unterhaut",
+    "basisRisiko": 1.75
+  },
+  {
+    "id": 41,
+    "bezeichnung": "Tod durch Asthma",
+    "basisRisiko": 1.26
+  },
+  {
+    "id": 42,
+    "bezeichnung": "Tod durch Virenbedingte Leberentzündung",
+    "basisRisiko": 1.06
+  },
+  {
+    "id": 43,
+    "bezeichnung": "Tod durch Rheumatoide Arthritis",
+    "basisRisiko": 1.03
+  },
+  {
+    "id": 44,
+    "bezeichnung": "Tod durch substanzbedingte psychische und Verhaltensstörungen",
+    "basisRisiko": 1.01
+  },
+  {
+    "id": 45,
+    "bezeichnung": "Tod durch Drogenabhängigkeit",
+    "basisRisiko": 0.97
+  },
+  {
+    "id": 46,
+    "bezeichnung": "Tod durch Vergiftung durch schädliche Substanz",
+    "basisRisiko": 0.81
+  },
+  {
+    "id": 47,
+    "bezeichnung": "Tod durch Fliegen mit einem Flugzeug",
+    "basisRisiko": 0.72
+  },
+  {
+    "id": 48,
+    "bezeichnung": "Tod durch einen Fallschirmsprung",
+    "basisRisiko": 0.70
+  },
+  {
+    "id": 49,
+    "bezeichnung": "Tod durch Leben in Denver",
+    "basisRisiko": 0.60
+  },
+  {
+    "id": 50,
+    "bezeichnung": "Tod durch Leben mit einem Raucher",
+    "basisRisiko": 0.60
+  },
+  {
+    "id": 51,
+    "bezeichnung": "Tod durch einen Tauchgang",
+    "basisRisiko": 0.50
+  },
+  {
+    "id": 52,
+    "bezeichnung": "Tod durch Mord und Totschlag",
+    "basisRisiko": 0.49
+  },
+  {
+    "id": 53,
+    "bezeichnung": "Tod durch Ertrinken und Untergehen",
+    "basisRisiko": 0.48
+  },
+  {
+    "id": 54,
+    "bezeichnung": "Tod durch HIV-Erkrankung",
+    "basisRisiko": 0.48
+  },
+  {
+    "id": 55,
+    "bezeichnung": "Tod durch tätlichen Angriff",
+    "basisRisiko": 0.46
+  },
+  {
+    "id": 56,
+    "bezeichnung": "Tod durch Rauch, Feuer und Flammen",
+    "basisRisiko": 0.43
+  },
+  {
+    "id": 57,
+    "bezeichnung": "Tod durch Tuberkulose-Folgen",
+    "basisRisiko": 0.38
+  },
+  {
+    "id": 58,
+    "bezeichnung": "Tod durch Hirnhautentzündung",
+    "basisRisiko": 0.15
+  },
+  {
+    "id": 59,
+    "bezeichnung": "Tod durch Essen von 100 kohlegegrillten Steaks",
+    "basisRisiko": 0.10
+  },
+  {
+    "id": 60,
+    "bezeichnung": "Tod durch eine Röntgenuntersuchung",
+    "basisRisiko": 0.10
+  },
+  {
+    "id": 61,
+    "bezeichnung": "Tod durch Essen von 40 Löffeln Erdnussbutter",
+    "basisRisiko": 0.10
+  },
+  {
+    "id": 62,
+    "bezeichnung": "Tod durch Trinken von natürlichem Wasser in Miami",
+    "basisRisiko": 0.10
+  },
+  {
+    "id": 63,
+    "bezeichnung": "Tod durch Flugzeugabsturz",
+    "basisRisiko": 0.10
+  },
+  {
+    "id": 64,
+    "bezeichnung": "Tod durch Besuch einer Kohlengrube für drei Stunden",
+    "basisRisiko": 0.10
+  },
+  {
+    "id": 65,
+    "bezeichnung": "Tod durch Fahren mit dem Zug",
+    "basisRisiko": 0.10
+  },
+  {
+    "id": 66,
+    "bezeichnung": "Tod durch eine Pille Ecstasy",
+    "basisRisiko": 0.10
+  },
+  {
+    "id": 67,
+    "bezeichnung": "Tod durch Grippe",
+    "basisRisiko": 0.10
+  },
+  {
+    "id": 68,
+    "bezeichnung": "Tod durch Meningokokkeninfektion",
+    "basisRisiko": 0.04
+  },
+  {
+    "id": 69,
+    "bezeichnung": "Tod durch Terroristenangriff",
+    "basisRisiko": 0.01
+  },
+  {
+    "id": 70,
+    "bezeichnung": "Tod durch das Leben innerhalb von 32 km um ein Atomkraftwerk",
+    "basisRisiko": 0.01
+  }
+]

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

@@ -0,0 +1,94 @@
+import * as d3 from 'd3';
+
+// 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 };
+};

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

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

+ 81 - 0
src/js/d3/donutchart.js

@@ -0,0 +1,81 @@
+import * as d3 from 'd3';
+
+export default () => {
+  // defaults
+  const options = {
+    donutWidth: 15,
+    endval: 0.66,
+    width: 80,
+    height: 80,
+    duration: 1500,
+    colors: [ '#7ED321', '#E3E3E3' ]
+  };
+
+  // start
+  const donut = selection => {
+    const datasetBase = [
+      { color: 1, val: 100 }
+    ];
+
+    const dataset = [
+      { color: 0, val: options.endval * 100 },
+      { color: 1, val: 100 - (options.endval * 100) }
+    ];
+
+    const radius = Math.min(options.width, options.height) / 2;
+
+    const svg = selection
+      .append('svg')
+      .attr('width', options.width)
+      .attr('height', options.height);
+
+    const arc = d3.arc()
+      .innerRadius(radius - options.donutWidth)
+      .outerRadius(radius);
+
+    const pie = d3.pie()
+      .value(d => d.val)
+      .sort(null);
+
+    // base donut without animation 100%
+    const groupBase = svg.append('g')
+      .attr('transform', `translate(${options.width / 2}, ${options.height / 2})`);
+
+    groupBase.selectAll('path')
+      .data(pie(datasetBase))
+      .enter()
+      .append('path')
+      .attr('d', arc)
+      .attr('fill', d => options.colors[d.data.color]);
+
+    // fill x% from donut, animate!
+    const group = svg.append('g')
+      .attr('transform', `translate(${options.width / 2}, ${options.height / 2})`);
+
+    group.selectAll('path')
+      .data(pie(dataset))
+      .enter()
+      .append('path')
+      .attr('fill', d => options.colors[d.data.color])
+      .transition()
+      .delay((d, i) => ((100 - d.value) * options.duration / 100 * i))
+      .duration(d => (options.duration * d.value / 100))
+      .attrTween('d', d => {
+        const i = d3.interpolate(d.startAngle + 0.1, d.endAngle);
+
+        return t => {
+          d.endAngle = i(t);
+          return arc(d);
+        };
+      });
+  };
+
+  // "setter"
+  donut.options = input => {
+    Object.assign(options, input);
+
+    return donut;
+  };
+
+  return donut;
+};

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

@@ -0,0 +1,56 @@
+import * as d3 from 'd3';
+
+const createGrid = (scales, options, dispatcher) => {
+
+  const addGridlinesX = () => d3.axisBottom(scales.xScale).ticks(5);
+  const addGridlinesY = () => d3.axisLeft(scales.yScale).ticks(5);
+
+  const draw = (selection) => {
+
+    const gridGroup = selection.selectAll('g.grid');
+    const gridy = gridGroup.selectAll('.grid--y');
+    const gridx = gridGroup.selectAll('g.grid--x');
+
+    // create x axis grid lines
+    gridx.call(
+      addGridlinesX()
+        .tickSize(-options.heightWithoutMargin)
+        .tickFormat('')
+    );
+
+    // create y axis grid lines
+    gridy.call(
+      addGridlinesY()
+        .tickSize(-options.widthWithoutMargin)
+        .tickFormat('')
+    );
+  };
+
+  // constructor
+  const grid = (selection) => {
+
+    dispatcher.on('dragging', () => {
+      draw(selection);
+    });
+
+    const gridGroup = selection.append('g')
+      .attr('class', 'grid')
+      .attr('clip-path', 'url(#mask)');
+
+    // create group for x grid lines
+    gridGroup.append('g')
+      .attr('class', 'grid--x')
+      .attr('transform', `translate(0, ${options.heightWithoutMargin})`);
+
+    // create group for y grid lines
+    gridGroup.append('g')
+      .attr('class', `grid--y  mod${options.module}__grid--y`);
+
+    // call draw to actually draw grid lines
+    draw(selection);
+  };
+
+  return grid;
+};
+
+export default createGrid;

+ 43 - 0
src/js/d3/increment.js

@@ -0,0 +1,43 @@
+import * as d3 from 'd3';
+
+export default () => {
+  // defaults
+  const options = {
+    startValue: 0,
+    targetValue: 0.10,
+    precision: 100,
+    duration: 500,
+    classname: 'd3-increment',
+    tag: 'p'
+  };
+
+  // start
+  const countup = selection => {
+    selection.selectAll(`${options.classname}`)
+      .data([ options.targetValue ])
+      .enter()
+      .append(options.tag)
+      .text(options.startValue)
+      .transition()
+      .duration(options.duration)
+      .tween('text', (targetValue, index, curObj) => {
+        const i = d3.interpolate(curObj[index].textContent, targetValue);
+
+        return t => {
+          const val = Math.round(i(t) * options.precision) / options.precision;
+          const ops = { style: 'decimal' };
+
+          curObj[index].textContent = val.toLocaleString('de-DE', ops);
+        };
+      });
+  };
+
+  // "setter"
+  countup.options = input => {
+    Object.assign(options, input);
+
+    return countup;
+  };
+
+  return countup;
+};

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

@@ -0,0 +1,36 @@
+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}`);
+};

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

@@ -0,0 +1,37 @@
+const createSymbols = () => {
+
+  // constructor
+  const symbols = (selection) => {
+
+    const svg = selection.append('svg')
+      .attr('class', 'symbols')
+      .attr('xmlns', 'http://www.w3.org/2000/svg')
+      .attr('xmlns:xlink', 'http://www.w3.org/1999/xlink');
+
+    const checked = svg.append('symbol')
+      .attr('viewBox', '0 0 16 13')
+      .attr('id', 'icon--correct');
+
+    checked.append('path')
+      .attr('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');  // eslint-disable-line max-len
+
+    const crossed = svg.append('symbol')
+      .attr('viewBox', '0 0 16 13')
+      .attr('id', 'icon--incorrect');
+
+    crossed.append('path')
+      .attr('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');  // eslint-disable-line max-len
+
+    const triangle = svg.append('symbol')
+      .attr('viewBox', '0 0 7 4')
+      .attr('id', 'icon--triangle');
+
+    triangle.append('path')
+      .attr('d', 'M3.5 4L0 0h7L3.5 4z');
+
+  };
+
+  return symbols;
+};
+
+export default createSymbols;

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

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,44 @@
+// 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);
+  }
+}
+
+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;
+}
+

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

@@ -0,0 +1,34 @@
+// base styles for html, body and other global elements
+// ======================================================================
+
+html,
+body {
+  @include background-color(background);
+  height: 100%;
+}
+
+html {
+  @include color(default);
+  font-size: $base-font-size; // as reference for rem
+}
+
+body {
+  @include font(default);
+
+  @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);
+    }
+  }
+}

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

@@ -0,0 +1,316 @@
+svg {
+  &.symbols {
+    display: none;
+  }
+
+  text {
+    user-select: none;
+
+    &::selection {
+      background: none;
+    }
+  }
+}
+
+.chart {
+  position: relative;
+
+  &--centered {
+    @extend %centered;
+  }
+}
+
+.mod2 {
+  &-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;
+        }
+      }
+    }
+  }
+
+  @each $line in a b c {
+    // sass-lint:disable-block force-element-nesting
+    .correct--#{$line} &-line--#{$line} path {
+      stroke: map-get($colors, correct);
+    }
+
+    .incorrect--#{$line} &-line--#{$line} path {
+      stroke: map-get($colors, error);
+    }
+  }
+
+  &-drag {
+    cursor: move;
+
+    &--active {
+      path {
+        stroke: map-get($colors, main);
+      }
+    }
+  }
+
+  &-rect {
+    &__markedarea {
+      fill: url('#gradient'); // defined in svg
+      stroke: lighten(map-get($colors, default), 30%);
+      stroke-width: 1px;
+      stroke-dasharray: 4px;
+
+      &--stop {
+        stop-color: lighten(map-get($colors, default), 30%);
+      }
+    }
+
+    &__connector {
+      fill: url('#gradient2'); // defined in svg
+    }
+  }
+
+  // first y line is similar to x-axis
+  &__grid--y {
+    g:first-of-type {
+      line {
+        stroke: map-get($colors, axis);
+        stroke-width: 2;
+      }
+    }
+  }
+
+  &-gradientangle {
+    transition: opacity .4s ease-in;
+
+    &--correct {
+      line,
+      rect {
+        stroke: map-get($colors, correct);
+      }
+
+      circle,
+      text {
+        fill: map-get($colors, correct);
+      }
+    }
+
+    &--incorrect {
+      line,
+      rect {
+        stroke: map-get($colors, error);
+      }
+
+      circle,
+      text {
+        fill: map-get($colors, error);
+      }
+    }
+
+    &--inactive {
+      opacity: 0;
+    }
+  }
+
+  &-dragshift--active {
+    circle {
+      fill: map-get($colors, error);
+      cursor: pointer;
+    }
+  }
+}
+
+.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;
+  }
+}

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

@@ -0,0 +1,33 @@
+// 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 {
+    @include spacing(v 2);
+
+    &--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;
+    }
+  }
+}

Some files were not shown because too many files changed in this diff