Ver código fonte

Initial commit

Michael Zitzmann 5 anos atrás
commit
f3c4285448
100 arquivos alterados com 4880 adições e 0 exclusões
  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. 100 0
      Session.vim
  8. 104 0
      config.js
  9. 38 0
      doc/01_introduction.md
  10. 31 0
      doc/02_structure.md
  11. 115 0
      doc/03_module.md
  12. 101 0
      doc/04_development.md
  13. 32 0
      doc/05_javascript.md
  14. 30 0
      doc/06_styles.md
  15. 15 0
      doc/07_images.md
  16. 2 0
      doc/html/css/github-markdown.css
  17. 121 0
      doc/html/css/github-syntax-highlight.css
  18. 233 0
      doc/html/index.html
  19. 127 0
      gulpfile.babel.js
  20. 77 0
      package.json
  21. 9 0
      public/browserconfig.xml
  22. BIN
      public/favicon.ico
  23. 5 0
      public/robots.txt
  24. 434 0
      readme.md
  25. BIN
      src/fonts/roboto-light.woff
  26. BIN
      src/fonts/roboto-light.woff2
  27. BIN
      src/fonts/roboto-medium.woff
  28. BIN
      src/fonts/roboto-medium.woff2
  29. BIN
      src/fonts/roboto-regular.woff
  30. BIN
      src/fonts/roboto-regular.woff2
  31. BIN
      src/fonts/roboto-thin.woff
  32. BIN
      src/fonts/roboto-thin.woff2
  33. BIN
      src/fonts/robotomono-light.woff
  34. BIN
      src/fonts/robotomono-light.woff2
  35. 31 0
      src/html/index.html
  36. BIN
      src/img/android-chrome-192x192.png
  37. BIN
      src/img/android-chrome-512x512.png
  38. BIN
      src/img/apple-touch-icon.png
  39. BIN
      src/img/favicon-16x16.png
  40. BIN
      src/img/favicon-32x32.png
  41. BIN
      src/img/mstile-150x150.png
  42. 3 0
      src/img/safari-pinned-tab.svg
  43. 1 0
      src/img/sprites.svg
  44. 3 0
      src/img/sprites/correct.svg
  45. 3 0
      src/img/sprites/incorrect.svg
  46. 15 0
      src/img/sprites/sprites.yaml
  47. 1 0
      src/img/sprites/triangle.svg
  48. 22 0
      src/js/components/FinalScreen.jsx
  49. 251 0
      src/js/components/Index.jsx
  50. 96 0
      src/js/components/QuestionScreen.jsx
  51. 19 0
      src/js/components/TitleScreen.jsx
  52. 76 0
      src/js/components/partials/BubbleChartItem.jsx
  53. 15 0
      src/js/components/partials/HeaderLightItem.jsx
  54. 13 0
      src/js/components/partials/IntroItem.jsx
  55. 55 0
      src/js/components/partials/QuestionItem.jsx
  56. 25 0
      src/js/components/partials/ScoreItem.jsx
  57. 55 0
      src/js/components/partials/VoteItem.jsx
  58. 41 0
      src/js/config.js
  59. 24 0
      src/js/content/module.json
  60. 115 0
      src/js/content/questions.json
  61. 84 0
      src/js/d3/bar.js
  62. 17 0
      src/js/d3/bubblechart-decolor.js
  63. 101 0
      src/js/d3/bubblechart-divide-gradual.js
  64. 113 0
      src/js/d3/bubblechart-divide.js
  65. 116 0
      src/js/d3/bubblechart-reduce.js
  66. 90 0
      src/js/d3/bubblechart.js
  67. 16 0
      src/js/d3/defs.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. 83 0
      src/js/utilities/formatter.js
  74. 17 0
      src/js/utilities/randomizer.js
  75. 19 0
      src/scss/base/_fonts.scss
  76. 52 0
      src/scss/base/_forms.scss
  77. 48 0
      src/scss/base/_headings.scss
  78. 12 0
      src/scss/base/_links.scss
  79. 24 0
      src/scss/base/_rhythm.scss
  80. 34 0
      src/scss/base/_root.scss
  81. 17 0
      src/scss/config/_breakpoints.scss
  82. 32 0
      src/scss/config/_colors.scss
  83. 37 0
      src/scss/config/_defaults.scss
  84. 73 0
      src/scss/config/_fonts.scss
  85. 62 0
      src/scss/main.scss
  86. 21 0
      src/scss/modules/_animations.scss
  87. 115 0
      src/scss/modules/_answers.scss
  88. 29 0
      src/scss/modules/_boxes.scss
  89. 55 0
      src/scss/modules/_buttons.scss
  90. 32 0
      src/scss/modules/_content.scss
  91. 311 0
      src/scss/modules/_d3js.scss
  92. 10 0
      src/scss/modules/_debug.scss
  93. 11 0
      src/scss/modules/_footer.scss
  94. 92 0
      src/scss/modules/_forms.scss
  95. 45 0
      src/scss/modules/_grids.scss
  96. 33 0
      src/scss/modules/_header.scss
  97. 31 0
      src/scss/modules/_icons.scss
  98. 2 0
      src/scss/modules/_logos.scss
  99. 36 0
      src/scss/modules/_navs.scss
  100. 92 0
      src/scss/modules/_questions.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

+ 100 - 0
Session.vim

@@ -0,0 +1,100 @@
+let SessionLoad = 1
+let s:so_save = &so | let s:siso_save = &siso | set so=0 siso=0
+let v:this_session=expand("<sfile>:p")
+silent only
+cd ~/code/risk-atlas/kompetenzmodule
+if expand('%') == '' && !&modified && line('$') <= 1 && getline(1) == ''
+  let s:wipebuf = bufnr('%')
+endif
+set shortmess=aoO
+badd +1 src/js/modules/d3-mod2-area.js
+badd +1 src/js/modules/d3-mod2-base.js
+badd +1 src/js/modules/d3-mod2-defs.js
+badd +1 src/js/modules/d3-mod2-draghandles.js
+badd +1 src/js/modules/d3-mod2-dragshifts.js
+badd +1 src/js/modules/d3-mod2-gradientangle.js
+badd +1 src/js/modules/d3-mod2-handles.js
+badd +1 src/js/modules/d3-mod2-line.js
+badd +1 src/js/modules/d3-mod2-points.js
+badd +1 src/js/modules/d3-mod2-quartshifts.js
+badd +1 src/js/modules/d3-mod2-redraw.js
+badd +1 src/js/modules/d3-mod2-scales.js
+badd +1 src/js/modules/d3-mod-grid.js
+argglobal
+silent! argdel *
+$argadd src/js/modules/d3-mod2-area.js
+$argadd src/js/modules/d3-mod2-base.js
+$argadd src/js/modules/d3-mod2-defs.js
+$argadd src/js/modules/d3-mod2-draghandles.js
+$argadd src/js/modules/d3-mod2-dragshifts.js
+$argadd src/js/modules/d3-mod2-gradientangle.js
+$argadd src/js/modules/d3-mod2-handles.js
+$argadd src/js/modules/d3-mod2-line.js
+$argadd src/js/modules/d3-mod2-points.js
+$argadd src/js/modules/d3-mod2-quartshifts.js
+$argadd src/js/modules/d3-mod2-redraw.js
+$argadd src/js/modules/d3-mod2-scales.js
+edit src/js/modules/d3-mod2-base.js
+set splitbelow splitright
+wincmd _ | wincmd |
+vsplit
+1wincmd h
+wincmd w
+set nosplitbelow
+set nosplitright
+wincmd t
+set winminheight=1 winminwidth=1 winheight=1 winwidth=1
+exe 'vert 1resize ' . ((&columns * 87 + 87) / 174)
+exe 'vert 2resize ' . ((&columns * 86 + 87) / 174)
+argglobal
+if bufexists('src/js/modules/d3-mod2-base.js') | buffer src/js/modules/d3-mod2-base.js | else | edit src/js/modules/d3-mod2-base.js | endif
+setlocal fdm=manual
+setlocal fde=0
+setlocal fmr={{{,}}}
+setlocal fdi=#
+setlocal fdl=0
+setlocal fml=1
+setlocal fdn=20
+setlocal fen
+silent! normal! zE
+let s:l = 1 - ((0 * winheight(0) + 24) / 49)
+if s:l < 1 | let s:l = 1 | endif
+exe s:l
+normal! zt
+1
+normal! 0
+wincmd w
+argglobal
+if bufexists('src/js/modules/d3-mod-grid.js') | buffer src/js/modules/d3-mod-grid.js | else | edit src/js/modules/d3-mod-grid.js | endif
+setlocal fdm=manual
+setlocal fde=0
+setlocal fmr={{{,}}}
+setlocal fdi=#
+setlocal fdl=0
+setlocal fml=1
+setlocal fdn=20
+setlocal fen
+silent! normal! zE
+let s:l = 1 - ((0 * winheight(0) + 24) / 49)
+if s:l < 1 | let s:l = 1 | endif
+exe s:l
+normal! zt
+1
+normal! 0
+wincmd w
+exe 'vert 1resize ' . ((&columns * 87 + 87) / 174)
+exe 'vert 2resize ' . ((&columns * 86 + 87) / 174)
+tabnext 1
+if exists('s:wipebuf') && getbufvar(s:wipebuf, '&buftype') isnot# 'terminal'
+  silent exe 'bwipe ' . s:wipebuf
+endif
+unlet! s:wipebuf
+set winheight=1 winwidth=20 winminheight=1 winminwidth=1 shortmess=filnxtToOFc
+let s:sx = expand("<sfile>:p:r")."x.vim"
+if file_readable(s:sx)
+  exe "source " . fnameescape(s:sx)
+endif
+let &so = s:so_save | let &siso = s:siso_save
+doautoall SessionLoadPost
+unlet SessionLoad
+" vim: set ft=vim :

+ 104 - 0
config.js

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

+ 38 - 0
doc/01_introduction.md

@@ -0,0 +1,38 @@
+% Dokumentation
+% Tabea David <tabea.david@kf-interactive.com>; zitzmann@mpib-berlin.mpg.de
+
+## Inhalt
+
+- [Einführung](#einführung)
+- [Übersicht der Module](#übersicht-der-module)
+- [Verzeichnisstruktur](#verzeichnisstruktur)
+- [Modul 5: Relative Risiken verstehen](#modul-5-relative-risiken-verstehen)
+- [Entwicklungsumgebung](#build-tool-chain)
+- [Javascript](#javascript)
+- [Sass](#sass)
+- [Bilddateien](#bilddateien)
+- [Entwicklungshistorie](#entwicklungshistorie)
+
+## Einführung
+
+Das Projekt [RisikoAtlas] hat die Förderung der Risikokompetenz zum Ziel. Zu diesem Zweck wurden am Harding-Zentrum für Risikokompetenz am Max-Planck-Institut für Bildungsforschung digitale Werkzeuge entwickelt, die auf wissenschaftlichen Erkenntnissen beruhen. Neben interaktiven Visualisierungen evidenzbasierter Risikokommunikation, einer App zur Entscheidungsunterstützung und einer Browser-Erweiterung als Leseassistenz sind Lernvisualisierungen zur Verbesserung der Risikokompetenz Teil des Projektes.
+Das vorliegende Modul ist eines dieser sechs Lernmodule.
+
+Die Lernmodule wurden ursprünglich entwickelt von [kf interactive][kf-interactive].
+
+## Übersicht der Module
+
+Die folgende Liste gibt einen Überblick über die Ziele der Module:
+
+1. 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
+
+```

+ 115 - 0
doc/03_module.md

@@ -0,0 +1,115 @@
+## Modul 5: Relative Risiken verstehen
+
+Mit diesem Modul soll das Verständnis von relativen Risiken geschult werden.  
+Dazu werden den Nutzern Multiple-Choice-Fragen gestellt, die durch animierte Icon Arrays illustriert werden. Die Auflösung wird gegebenenfalls (im 'online' Modus) mit dem Vergleich zu anderen Nutzern präsentiert.
+
+### Javascript Verzeichnisstruktur
+
+Für einen besseren Überblick sind die Quell-Dateien mit kurzen Beschreibungen aufgelistet:
+
+```
+├── main.jsx                      // Einstiegspunkt für App *mit* Verwendung der API ("online mode")
+├── main-offline.jsx              // Einstiegspunkt für App *ohne* Verwendung der API ("offline mode")
+├── config.js                     // Globale Konfiguration der WebApp
+├── components
+│   ├── Index.jsx                 // Web App Haupt-Komponente
+│   ├── FinalScreen.jsx           // Abschließende Ansicht
+│   ├── QuestionScreen.jsx
+│   ├── TitleScreen.jsx           // Einleitende Ansicht
+│   └── partials
+│       ├── BubbleChartItem.jsx   // d3 Visualisierungen
+│       ├── HeaderLightItem.jsx
+│       ├── IntroItem.jsx         // Einführung zu den Fragen
+│       ├── QuestionItem.jsx      // Benutzereingaben für Fragen
+│       ├── ScoreItem.jsx         // Auflösung / Erklärung zu den Fragen
+│       └── VoteItem.jsx          // Ergebnisse anderer Nutzer
+├── content
+│   ├── module.json               // Definition der Labels und Texte des User Interfaces
+│   └── questions.json            // Definition der Multiple Choice Fragen
+├── d3
+│   ├── bar.js                    // Balkendiagramm für Vergleich mit anderen Nutzern
+│   ├── bubblechart-decolor.js
+│   ├── bubblechart-divide-gradual.js
+│   ├── bubblechart-divide.js
+│   ├── bubblechart-reduce.js
+│   ├── bubblechart.js
+│   └── defs.js
+├── services
+└── utilities
+    ├── api.js                    // API zum Lese- und Schreib-Zugriff auf die Datenbank
+    ├── enableTouch.js
+    ├── fonts.js
+    ├── formatter.js
+    └── randomizer.js
+```
+
+### Wie ändere ich Inhalte und Daten?
+
+
+#### Labels
+
+In `module.json` werden die umgebenden Texte definiert:
+
+
+    {
+      "title": "Prozent von was??",
+      "introtext": "Täglicher Wurstkonsum erhöht Darmkrebsrisiko um 18 %.\nDamit steht dieses Risiko auf der gleichen Gefahrenstufe der Weltgesundheitsorganisation WHO wie Asbest und Rauchen.",
+      "introContinue": "Worum geht es hier?",
+      "next": "Erklärung",
+      "start": "Start",
+      "restart": "Neustart",
+      "percent": "18 %",
+      "prediction": "mehr Darmkrebsrisiko bei erhöhtem Wurstkonsum",
+      "conclusion": "Was haben Sie gelernt?",
+      "finalTitle": "Fragen Sie sich immer, bei jeder Prozentangabe",
+      "finalSlogan": "„% von was“?",
+      "finalText": "Was ist „was“ genau?\nWie häufig kommt etwas normalerweise sowieso vor?\nDann lebt sich‘s viel entspannter.",
+      "reference": {
+        "source": "Quelle",
+        "author": "WHO",
+        "year": "(2015)",
+        "title": "Links between processed meat and colorectal cancer.",
+        "url": "http://www.who.int/mediacentre/news/statements/2015/processed-meat-cancer/en/"
+      },
+      "sausageEater": "Wurstesser",
+      "mainstreamEater": "Durchschnittsesser",
+      "usersRight": "Wie viele der anderen Nutzer lagen richtig?"
+    }
+
+Hier können Titel, Einleitungstext und Zusammenfassung angepasst werden. Auch die Labels der Buttons sind dort festgelegt.
+
+#### Fragen
+
+In `src/js/content/question.json` werden die Fragen definiert:
+
+    {
+      "intro": "Haben Sie sich dazu auch mal folgende Frage gestellt?",
+      "title": "Welche Wurst?",
+      "answers": [
+        {
+          "id": 1,
+          "antwort": "Verarbeitetes Fleisch",
+          "korrekt": true
+        },
+        {
+          "id": 2,
+          "antwort": "Rotes Fleisch",
+          "korrekt": false
+        },
+        {
+          "id": 3,
+          "antwort": "Geräuchertes Fleisch",
+          "korrekt": false
+        },
+        {
+          "id": 4,
+          "antwort": "Weißes Fleisch",
+          "korrekt": false
+        }
+      ],
+      "description": "Verarbeitetes Fleisch ist Fleisch, das durch Verarbeitungsprozesse wie Salzen, Räuchern, Reifen bzw. Fermentieren oder andere Verfahren verändert wurde, um den Geschmack zu verbessern oder es haltbar zu machen. Meistens sind Schweine-, Rind- oder Geflügelfleisch, aber auch Innereien enthalten. Wurstwaren, Schinken, Hackfleischprodukte und Fleischkonserven sind verarbeitetes Fleisch.",
+      "onext": "Worum geht es hier?",
+      "next": "Nur wie viel hiervon ist denn zu viel?"
+    },
+
+ Metadaten wie Titel (`"title"`), Einleitungstext (`"intro"`), Beschreibung (`"description"`) und die Labels der Buttons können hier angepasst werden. Außerdem sind die Antwortoptionen definiert mit Label (`"antwort"`) und der Information, ob die jeweilige Option wahr ist (`"korrekt"`). Die Auswertung geschieht immer sobald eine Option ausgewählt wurde, daher kann immer nur eine Antwort richtig sein.

+ 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

Diferenças do arquivo suprimidas por serem muito extensas
+ 2 - 0
doc/html/css/github-markdown.css


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

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

+ 233 - 0
doc/html/index.html

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

+ 127 - 0
gulpfile.babel.js

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

+ 77 - 0
package.json

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

+ 9 - 0
public/browserconfig.xml

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

BIN
public/favicon.ico


+ 5 - 0
public/robots.txt

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

+ 434 - 0
readme.md

@@ -0,0 +1,434 @@
+Inhalt
+------
+
+-   [Einführung](#einführung)
+-   [Übersicht der Module](#übersicht-der-module)
+-   [Verzeichnisstruktur](#verzeichnisstruktur)
+-   [Modul 5: Relative Risiken
+    verstehen](#modul-5-relative-risiken-verstehen)
+-   [Entwicklungsumgebung](#build-tool-chain)
+-   [Javascript](#javascript)
+-   [Sass](#sass)
+-   [Bilddateien](#bilddateien)
+-   [Entwicklungshistorie](#entwicklungshistorie)
+
+Einführung
+----------
+
+Das Projekt [RisikoAtlas](https://risikoatlas.de) hat die Förderung der
+Risikokompetenz zum Ziel. Zu diesem Zweck wurden am Harding-Zentrum für
+Risikokompetenz am Max-Planck-Institut für Bildungsforschung digitale
+Werkzeuge entwickelt, die auf wissenschaftlichen Erkenntnissen beruhen.
+Neben interaktiven Visualisierungen evidenzbasierter
+Risikokommunikation, einer App zur Entscheidungsunterstützung und einer
+Browser-Erweiterung als Leseassistenz sind Lernvisualisierungen zur
+Verbesserung der Risikokompetenz Teil des Projektes. Das vorliegende
+Modul ist eines dieser sechs Lernmodule.
+
+Die Lernmodule wurden ursprünglich entwickelt von [kf
+interactive](https://www.kf-interactive.com).
+
+Übersicht der Module
+--------------------
+
+Die folgende Liste gibt einen Überblick über die Ziele der Module:
+
+1.  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 5: Relative Risiken verstehen
+-----------------------------------
+
+Mit diesem Modul soll das Verständnis von relativen Risiken geschult
+werden.\
+Dazu werden den Nutzern Multiple-Choice-Fragen gestellt, die durch
+animierte Icon Arrays illustriert werden. Die Auflösung wird
+gegebenenfalls (im 'online' Modus) mit dem Vergleich zu anderen Nutzern
+präsentiert.
+
+### Javascript Verzeichnisstruktur
+
+Für einen besseren Überblick sind die Quell-Dateien mit kurzen
+Beschreibungen aufgelistet:
+
+    ├── main.jsx                      // Einstiegspunkt für App *mit* Verwendung der API ("online mode")
+    ├── main-offline.jsx              // Einstiegspunkt für App *ohne* Verwendung der API ("offline mode")
+    ├── config.js                     // Globale Konfiguration der WebApp
+    ├── components
+    │   ├── Index.jsx                 // Web App Haupt-Komponente
+    │   ├── FinalScreen.jsx           // Abschließende Ansicht
+    │   ├── QuestionScreen.jsx
+    │   ├── TitleScreen.jsx           // Einleitende Ansicht
+    │   └── partials
+    │       ├── BubbleChartItem.jsx   // d3 Visualisierungen
+    │       ├── HeaderLightItem.jsx
+    │       ├── IntroItem.jsx         // Einführung zu den Fragen
+    │       ├── QuestionItem.jsx      // Benutzereingaben für Fragen
+    │       ├── ScoreItem.jsx         // Auflösung / Erklärung zu den Fragen
+    │       └── VoteItem.jsx          // Ergebnisse anderer Nutzer
+    ├── content
+    │   ├── module.json               // Definition der Labels und Texte des User Interfaces
+    │   └── questions.json            // Definition der Multiple Choice Fragen
+    ├── d3
+    │   ├── bar.js                    // Balkendiagramm für Vergleich mit anderen Nutzern
+    │   ├── bubblechart-decolor.js
+    │   ├── bubblechart-divide-gradual.js
+    │   ├── bubblechart-divide.js
+    │   ├── bubblechart-reduce.js
+    │   ├── bubblechart.js
+    │   └── defs.js
+    ├── services
+    └── utilities
+        ├── api.js                    // API zum Lese- und Schreib-Zugriff auf die Datenbank
+        ├── enableTouch.js
+        ├── fonts.js
+        ├── formatter.js
+        └── randomizer.js
+
+### Wie ändere ich Inhalte und Daten?
+
+#### Labels
+
+In `module.json` werden die umgebenden Texte definiert:
+
+    {
+      "title": "Prozent von was??",
+      "introtext": "Täglicher Wurstkonsum erhöht Darmkrebsrisiko um 18 %.\nDamit steht dieses Risiko auf der gleichen Gefahrenstufe der Weltgesundheitsorganisation WHO wie Asbest und Rauchen.",
+      "introContinue": "Worum geht es hier?",
+      "next": "Erklärung",
+      "start": "Start",
+      "restart": "Neustart",
+      "percent": "18 %",
+      "prediction": "mehr Darmkrebsrisiko bei erhöhtem Wurstkonsum",
+      "conclusion": "Was haben Sie gelernt?",
+      "finalTitle": "Fragen Sie sich immer, bei jeder Prozentangabe",
+      "finalSlogan": "„% von was“?",
+      "finalText": "Was ist „was“ genau?\nWie häufig kommt etwas normalerweise sowieso vor?\nDann lebt sich‘s viel entspannter.",
+      "reference": {
+        "source": "Quelle",
+        "author": "WHO",
+        "year": "(2015)",
+        "title": "Links between processed meat and colorectal cancer.",
+        "url": "http://www.who.int/mediacentre/news/statements/2015/processed-meat-cancer/en/"
+      },
+      "sausageEater": "Wurstesser",
+      "mainstreamEater": "Durchschnittsesser",
+      "usersRight": "Wie viele der anderen Nutzer lagen richtig?"
+    }
+
+Hier können Titel, Einleitungstext und Zusammenfassung angepasst werden.
+Auch die Labels der Buttons sind dort festgelegt.
+
+#### Fragen
+
+In `src/js/content/question.json` werden die Fragen definiert:
+
+    {
+      "intro": "Haben Sie sich dazu auch mal folgende Frage gestellt?",
+      "title": "Welche Wurst?",
+      "answers": [
+        {
+          "id": 1,
+          "antwort": "Verarbeitetes Fleisch",
+          "korrekt": true
+        },
+        {
+          "id": 2,
+          "antwort": "Rotes Fleisch",
+          "korrekt": false
+        },
+        {
+          "id": 3,
+          "antwort": "Geräuchertes Fleisch",
+          "korrekt": false
+        },
+        {
+          "id": 4,
+          "antwort": "Weißes Fleisch",
+          "korrekt": false
+        }
+      ],
+      "description": "Verarbeitetes Fleisch ist Fleisch, das durch Verarbeitungsprozesse wie Salzen, Räuchern, Reifen bzw. Fermentieren oder andere Verfahren verändert wurde, um den Geschmack zu verbessern oder es haltbar zu machen. Meistens sind Schweine-, Rind- oder Geflügelfleisch, aber auch Innereien enthalten. Wurstwaren, Schinken, Hackfleischprodukte und Fleischkonserven sind verarbeitetes Fleisch.",
+      "onext": "Worum geht es hier?",
+      "next": "Nur wie viel hiervon ist denn zu viel?"
+    },
+
+Metadaten wie Titel (`"title"`), Einleitungstext (`"intro"`),
+Beschreibung (`"description"`) und die Labels der Buttons können hier
+angepasst werden. Außerdem sind die Antwortoptionen definiert mit Label
+(`"antwort"`) und der Information, ob die jeweilige Option wahr ist
+(`"korrekt"`). Die Auswertung geschieht immer sobald eine Option
+ausgewählt wurde, daher kann immer nur eine Antwort richtig sein.
+
+Build Tool Chain
+----------------
+
+In diesem Abschnitt werden die technischen Voraussetzungen für die
+Erstellung von im Browser lauffähigem Code und und die Installation und
+Verwendung der Entwicklungsumgebung erläutert.
+
+### Voraussetzungen
+
+Dieses Projekt wurde entwickelt auf Basis von
+[nodejs](https://nodejs.org) unter Verwendung von
+[npm](https://www.npmjs.com/) als Paket-Manager. Mit den folgenden
+Versionen wurde zuletzt getestet:
+
+    nodejs: v14.4.0
+       npm:  6.14.4
+
+Alle Abhängigkeiten sind definiert in der `npm` Konfigurations-Datei
+`package.json`. Wie üblich werden diese installiert mit dem Befehl
+`npm install`. Als Task-Manager dieses Projekts wird
+[gulp](https://gulpjs.com/) dabei global installiert.
+
+Für das Erstellen der Dokumentation aus den einzelnen
+*Markdown*-Dateien, die im Verzeichnis `doc/` liegen, wird
+[pandoc](https://pandoc.org) verwendet. Dieses ist für viele
+Betriebssysteme und Distributionen verfügbar, muss aber gesondert
+installiert werden.
+
+### Konfiguration
+
+Die Build Konfiguration ist in `config.js` im Wurzelverzeichnis
+definiert. Außerdem sind in der Datei `package.json` die zu
+unterstützenden Browser-Versionen für `autoprefixer` angegeben.
+
+Konfigurationen für Babel, Editoren und *Linter* sind ebenfalls im
+Wurzelverzeichnis zu finden:
+
+           babel: .babelrc
+    editorconfig: .editorconfig
+            html: .htmlhintrc
+      javascript: .eslintrc.yml
+            sass: .sass-lint.yml
+
+### Erstellen von Builds
+
+Alle Schritte zum Erstellen von Builds sind in den Javascript-Dateien
+unter `tasks/` definiert und werden von der `gulp` Konfigurationsdatei
+`gulpfile.babel.js` importiert. Dort sind die Teilschritte in *Tasks*
+zusammengefasst, die man am häufigsten benötigt.
+
+    $ gulp        # Default task, Kurzform für 'gulp watch'
+    $ gulp build  # Erstellt einen Development Build
+    $ gulp watch  # Erstellt einen Development Build und startet den Entwicklungsserver
+
+#### Build Target
+
+Die Unterscheidung zwischen Development und Production Build wird anhand
+der `nodejs` Umgebungsvariable `NODE_ENV` vorgenommen. Ohne diese Angabe
+wird immer ein Development Build erstellt (siehe `config.js`). Für das
+Development Target werden Javascript und CSS zusätzlich mit *Sourcemaps*
+versehen, für die Produktiv-Version dagegen werden die Dateien von
+unnötigem Ballast befreit (`terser` für Javascript, `cssnano` für CSS).
+
+    # Erstellen eines Production Build
+    $ NODE_ENV=production gulp build
+
+#### Build Mode
+
+Zusätzlich gibt es für dieses Projekt die Unterscheidung zwischen
+'online' und 'offline' Versionen. Im Fall der 'online' Version wird auf
+eine API zugegriffen, um die benötigten Inhalte zu laden, und um die
+Antworten der Benutzer zu speichern, um ihnen einen Vergleich mit
+Anderen zu ermöglichen. Diese Unterscheidung kann beim Aufruf von gulp
+auf der Kommandozeile mit einem Parameter getroffen werden. Soweit
+verfügbar, wird standardmäßig der 'online' Modus verwendet, (siehe
+`config.js`).
+
+    # Erstellen eines Development Build für den 'offline' Modus
+    $ gulp --api-mode=offline
+
+#### npm Shortcuts
+
+Da die Handhabung mit dem Setzen der Umgebungsvariable und das Übergeben
+des Parameters etwas umständlich ist, sind in `package.json` `npm` ein
+paar Shortcuts definiert, z.B.:
+
+    # Erstellen eines Production Build für den 'offline' Modus per gulp Script
+    $ NODE_ENV=production gulp build --api-mode=offline
+
+    # Erstellen eines Production Build für den 'offline' Modus per npm Script
+    $ npm run build:prod:offline
+
+### Erstellen der Dokumentation
+
+Die Dokumentation in einzelne *Markdown*-Dateien aufgeteilt, die im
+Verzeichnis `doc/` liegen. Zum Erstellen einer zusammenhängender
+Dokumentation sind folgende `npm` Scripts definiert, die auf `pandoc`
+basieren:
+
+    npm build:doc       // Kurzform für das Erstellen der Dokumentation im bevorzugten Ausgabeformat (Standard: Markdown)
+    npm build:doc:html  // Erstellt eine HTML Dokumentation als `index.html` in `doc/html`
+    npm build:doc:md    // Erstellt eine zusammenhängende Dokumentation als `readme.md` im Wurzelverzeichnis
+
+### Konventionen
+
+Der Javascript Code ist in ES6 (bzw. ES2015) verfasst. Als
+CSS-Preprocessor wird Sass mit der `scss` Syntax verwendet. Die Code
+Style Konventionen wurden von den ursprünglichen Entwicklern übernommen
+und nur an wenigen Stellen leicht angepasst.
+
+Das Projekt verwendet [editorconfig](http://editorconfig.org) für die
+Integration dieser Konventionen in Editoren, die entsprechende Datei
+heißt `.editorconfig`.
+
+Für die statische Überprüfung des Quellcodes werden folgende *Linter*
+verwendet:
+
+-   Javascript: [eslint](https://eslint.org)
+-   Sass: [sass-lint](https://github.com/sasstools/sass-lint)
+-   HTML: [HTMLHint](https://github.com/htmlhint/HTMLHint)
+
+Die zugehörigen Konfigurationsdateien befinden sich im Root-Verzeichnis,
+wie oben in der Auflistung angegeben.
+
+Javascript
+----------
+
+### Verwendete Bibliotheken
+
+Die grundlegende Architektur der WebApp wurde implementiert auf Basis
+von [preact](https://preactjs.com/), von den Entwicklern beworben mit
+
+> Fast 3kB alternative to React with the same modern API.
+
+Es ist allerdings keine exakte Reimplementierung, weswegen ein eigener
+Teil der Dokumentation der [Erläuterung der Unterschiede zu
+React](https://preactjs.com/guide/v10/differences-to-react) gewidmet
+ist.
+
+Für Visualisierungen wird die großartige und weit verbreitete Bibliothek
+[D3.js](https://d3js.org/) verwendet.
+
+### Verzeichnisstruktur
+
+Die folgende Auflistung gibt einen groben Überblick über die
+Verzeichnisstruktur der Javascript Quelldateien in `src/js/`. Als
+Einstieg dient `main.jsx` bzw. `main-offline.jsx` für den "offline"
+Modus. `config.js` ist die zentrale Konfigurationsdatei der WebApp. In
+`components` liegen die Komponenten der *preact*-WebApp. Der Quellcode
+für die D3-Visualisierungen befindet sich unter `d3`.
+
+    ├── main.jsx            // Einstiegspunkt für App in "online" Modus
+    ├── main-offline.jsx    // Einstiegspunkt für App in "offline" Modus
+    ├── config.js           // Konfiguration der WebApp
+    ├── components/         // Verzeichnis für (p)react Komponenten
+    │   ├── Index.jsx       // Web App Haupt-Komponente
+    │   └── partials/       // Vezeichnis für Teilkomponenten
+    ├── content/            // Verzeichnis für "offline" Inhalte
+    ├── d3/                 // d3 Module
+    └── utilities/          // Verzeichnis für Hilfs-Bibliotheken und Werkzeuge
+
+Sass
+----
+
+Zum Kompilieren von Sass zu CSS wird `gulp-sass` verwendet, das
+`node-sass` benutzt, welches wiederum auf `libsass` basiert. `node-sass`
+hat sich beim wiederholten gedankenlosen Aktualisieren von `nodejs` und
+/ oder `npm` als notorischer Nerventöter herausgestellt, daher an dieser
+Stelle der [Verweis zur *Troubleshooting*
+Dokumentation](https://github.com/sass/node-sass/blob/master/TROUBLESHOOTING.md)
+von `node-sass`. Meist reichte im Falle eines Problems aber ein
+`npm rebuild node-sass`.
+
+### Struktur
+
+In der Datei `main.scss` werden alle Stile eingebunden, die in den
+*Partials* definiert werden, woraus die endgültige CSS-Datei generiert
+wird. Die Struktur des `src/scss` Verzeichnisses sieht folgendermaßen
+aus:
+
+    ├── base     // Stile für HTML Elemente
+    ├── config   // Globale Variable
+    ├── modules  // Stile für Module
+    └── tools    // Definierte *mixins* und Funktionen
+
+### Konventionen, Techniken und Tools
+
+Generell wird eine "mobile first" Strategie verfolgt. Als
+Standard-Einheit wird `rem` verwendet, auf deren Grundlage die
+Basis-Einheit definiert ist. Da sich alle Größen auf diese Einheit
+beziehen sollten, wird so das Skalieren des Layouts erleichtert.
+
+Sass wird in diesem Projekt mit der scss-Syntax verwendet. Stilistisch
+ist es in "oldschool BEM-Style" gehalten, Zitat der ursprünglichen
+Entwickler. Sie beziehen sich zudem auf bestimmte Guidelines:
+
+> Hugo Giraudel wrote an awesome piece on everything you need to know
+> about Sass, it's called [Sass Guidelines](http://sass-guidelin.es/)
+> and you should really have a look at it. I agree with this guideline
+> in almost all points, but I try to keep something more simple, and
+> some things more strict, the linter will let you know :)
+
+ʕ̡̢̡ॢ•̫͡ॢ•ʔ̢̡̢
+
+Regeln mit Browser-spezifischen Präfixen (*vendor prefixes*) werden dem
+CSS automatisch durch
+[autoprefixer](https://github.com/postcss/autoprefixer) hinzugefügt. Die
+Liste der zu unterstützenden Browser ist in `package.json` unter
+`browserslist` zu finden.
+
+Bilddateien
+-----------
+
+Alle in diesem Projekt verwendeten Bilddateien befinden sich unter
+`src/img/`.
+
+Icons in Form von SVG-Dateien befinden sich im Unterordner
+`src/img/sprites` und werden im Build-Prozess mittels `gulp-svg-sprite`
+zu einem Sprite zusammengefasst. Sie wie folgt in HTML referenziert
+werden:
+
+    <svg class="icon  icon--arrow-left">
+      <use xlink:href="assets/img/sprites.svg#icon--arrow-left"/>
+    </svg>
+
+Stil-Definitionen für Icons sind unter `src/scss/modules/_icons.scss` zu
+finden. Für die Unterstützung von Fragmentbezeichnern (*fragment
+identifier*) in Internet Explorer wird
+[svgxuse](https://github.com/Keyamoon/svgxuse) verwendet.

BIN
src/fonts/roboto-light.woff


BIN
src/fonts/roboto-light.woff2


BIN
src/fonts/roboto-medium.woff


BIN
src/fonts/roboto-medium.woff2


BIN
src/fonts/roboto-regular.woff


BIN
src/fonts/roboto-regular.woff2


BIN
src/fonts/roboto-thin.woff


BIN
src/fonts/roboto-thin.woff2


BIN
src/fonts/robotomono-light.woff


BIN
src/fonts/robotomono-light.woff2


+ 31 - 0
src/html/index.html

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

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


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


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


BIN
src/img/favicon-16x16.png


BIN
src/img/favicon-32x32.png


BIN
src/img/mstile-150x150.png


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

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

+ 1 - 0
src/img/sprites.svg

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,22 @@
+import { h, render } from 'preact'; // eslint-disable-line no-unused-vars
+import HeaderLightItem from './partials/HeaderLightItem.jsx';
+
+// renders outro screen containing restart feature
+const FinalScreen = props => ( // eslint-disable-line no-unused-vars
+  <section class="wrapper">
+    <HeaderLightItem { ...props } />
+    <main className="wrapper__main  wrapper--centered">
+      <section className="centered">
+        <h4>{props.finalTitle}</h4>
+        <h1 className="huge">{props.finalSlogan}</h1>
+        <p>{props.finalText.split('\n').map(item => <span>{item}<br /></span>)}</p>
+      </section>
+    </main>
+    <footer className="footer footer--introscreen">
+      { !props.isFetching ? <a href="#" title={props.restart} className="button--wide" onClick={() => props.navigate('titlescreen')}>
+        {props.restart}</a> : [] }
+    </footer>
+  </section>
+);
+
+export default FinalScreen;

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

@@ -0,0 +1,251 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+// content and config:
+import content from '../content/module.json';
+import config from '../config';
+// services and helper:
+import api from '../utilities/api';
+import { fixedDigits } from '../utilities/formatter';
+// screens and items:
+import TitleScreen from './TitleScreen.jsx';
+import FinalScreen from './FinalScreen.jsx';
+import QuestionScreen from './QuestionScreen.jsx';
+
+/**
+ * titlescreen, questionscreen (repeat), finalscreen
+ */
+export default class App extends Component {
+
+  // construct and initialize functions
+  constructor (props) {
+    super(props);
+
+    // for example: (1400 - 200) / 3 -> 400 points per section
+    const diff = (config.numberOfPoints.first - config.numberOfPoints.last) / 3;
+
+    // for example: 1400, 1000, 600, 200
+    this.numberOfPoints = [
+      parseInt(config.numberOfPoints.first, 10),
+      parseInt(config.numberOfPoints.first - diff, 10),
+      parseInt(config.numberOfPoints.last + diff, 10),
+      parseInt(config.numberOfPoints.last, 10)
+    ];
+
+    this.state = {
+      route: 'titlescreen',                          // current screen
+      isFetching: false,                             // currently performing XHR?
+      userId: null,                                  // current user id for posting
+      token: null,                                   // current access token
+      mode: 'intro',                                 // current mode: init || intro || question || score
+      currentQuestion: 0,                            // current question number
+      currentAnswerIndex: null,                      // index in question pair of the currently set answer
+      currentAnswerCorrect: false,                   // is the currently set question the correct one?
+      currentNumberOfPoints: this.numberOfPoints[0], // current number of points
+      nextNumberOfPoints: this.numberOfPoints[1],    // next number of points
+      currentAmount: config.highlightedAmount,       // curent amount of points to be highlighted
+      voteRatios: null                               // get votes from other users
+    };
+
+    if (window.location.hash) this.state.route = window.location.hash.replace('#', '');
+
+    // context binding
+    this.navigate = this.navigate.bind(this);
+    this.toNextQuestion = this.toNextQuestion.bind(this);
+    this.setAnswer = this.setAnswer.bind(this);
+    this.getUserVotes = this.getUserVotes.bind(this);
+    this.reset = this.reset.bind(this);
+    this.setup = this.setup.bind(this);
+    this.endUserSession = this.endUserSession.bind(this);
+  }
+
+  // to next question
+  toNextQuestion () {
+    // intro screen, introduce bubbles :P
+    if (this.state.mode === 'intro') {
+      this.setState({
+        // currentQuestion: null,
+        mode: 'init',
+        route: 'question'
+      });
+    } else if (this.state.currentQuestion === 0 && this.state.mode === 'init') {
+      // is first question?
+      this.setState({
+        currentAmount: 0,
+        mode: 'question',
+        route: 'question'
+      });
+    } else if (this.state.currentQuestion === 3 && this.state.mode === 'score') {
+      // is last question? go to final screen
+      this.setState({
+        route: 'finalscreen',
+        mode: 'intro',
+        currentNumberOfPoints: this.numberOfPoints[0],
+        currentAmount: config.highlightedAmount,
+        currentQuestion: 0,
+        currentAnswerIndex: null,
+        currentAnswerCorrect: false,
+        nextNumberOfPoints: this.numberOfPoints[1],
+        voteRatios: null
+      });
+    } else if (this.state.voteRatios && this.state.mode === 'question') {
+      // go to score screen
+      this.setState({
+        route: 'question',
+        mode: 'score'
+      });
+    } else {
+      // go to next question
+      this.setState({
+        route: 'question',
+        mode: 'question',
+        currentQuestion: this.state.currentQuestion + 1,
+        currentAnswerIndex: null,
+        currentNumberOfPoints: this.numberOfPoints[this.state.currentQuestion + 1],
+        nextNumberOfPoints: this.numberOfPoints[this.state.currentQuestion + 2],
+        currentAmount: 0,
+        voteRatios: null
+      });
+    }
+  }
+
+  // set and check answer
+  setAnswer (index, isCorrect) {
+    if (this.state.currentAnswerIndex === null) {
+      // check answer
+      this.setState({
+        currentAnswerIndex: index,
+        currentAnswerCorrect: isCorrect,
+        mode: 'question'
+      });
+
+      // submit answer
+      if (!this.props.isOffline) {
+        // only send answer for current question
+        const payload = { userId: this.state.userId };
+
+        payload[`answerQ${this.state.currentQuestion + 1}`] = this.state.currentAnswerIndex + 1;
+        api.post(config.api.create, payload, this.state.token).then(() => this.getUserVotes());
+      } else {
+        this.getUserVotes();
+      }
+    }
+  }
+
+  // get user votes from api
+  getUserVotes () {
+    if (!this.props.isOffline) {
+      api.get(config.api.proportions, { question: this.state.currentQuestion + 1 }).then(
+        json => this.setState({ voteRatios: [ json.anteile.richtig, json.anteile.falsch ] })
+      );
+    } else {
+      this.setState({ voteRatios: [] });
+    }
+  }
+
+  // initial setup
+  setup () {
+    if (!this.props.isOffline) {
+      this.setState({ isFetching: true });
+
+      api.getToken().then(accessToken => {
+        this.setState({ token: accessToken });
+
+        // create user
+        api.createUser(accessToken)
+          .then(user => {
+            this.setState({ userId: user.userId, isFetching: false });
+            this.jumpTo();
+          });
+      });
+    }
+  }
+
+  // shortcut: jump to specific screen using `#{something}`
+  jumpTo () {
+    if (window.location.hash) {
+      const hashInfo = window.location.hash.replace('#', '').split('_');
+
+      this.setState({
+        route: 'question',
+        mode: hashInfo[0],
+        currentAmount: 0,
+        currentAnswerCorrect: null,
+        currentAnswerIndex: null,
+        currentNumberOfPoints: 1000,
+        currentQuestion: parseInt(hashInfo[1], 10),
+        nextNumberOfPoints: 200,
+        voteRatios: null
+      });
+    }
+  }
+
+  // set navigation state
+  navigate (route) {
+    this.setState({ route });
+  }
+
+  // finish user session
+  endUserSession () {
+    api.endSession(this.state.userId, this.state.token);
+  }
+
+  // reset everything to restart the module
+  reset () {
+    this.setState({ route: 'titlescreen' });
+    this.setup();
+  }
+
+  // LIFECYLCE
+  componentWillMount () {
+    this.setup();
+  }
+
+  // RENDER
+  render () {
+    let outputContent;
+
+    // get header content
+    const total = fixedDigits(config.numberQuestions, 2);
+    const index = this.state.currentQuestion + 1; // 0-indexed
+    const headerState = `${fixedDigits(index, 2)}/${total}`;
+
+    switch (this.state.route) {
+
+      case 'question':
+        outputContent = <QuestionScreen
+          {...content}
+          isOffline={this.props.isOffline}
+          route={this.state.route}
+          mode={this.state.mode}
+          headerState={headerState}
+          currentQuestion={this.state.currentQuestion}
+          currentAnswerIndex={this.state.currentAnswerIndex}
+          currentAnswerCorrect={this.state.currentAnswerCorrect}
+          currentNumberOfPoints={this.state.currentNumberOfPoints}
+          currentAmount={this.state.currentAmount}
+          nextNumberOfPoints={this.state.nextNumberOfPoints}
+          toNextQuestion={this.toNextQuestion}
+          voteRatios={this.state.voteRatios}
+          setAnswer={this.setAnswer} />;
+        break;
+
+      case 'finalscreen':
+        outputContent = <FinalScreen
+          {...content}
+          navigate={this.navigate}
+          isFetching={this.state.isFetching} />;
+        break;
+
+      case 'titlescreen':
+      default:
+        outputContent = <TitleScreen
+          {...content}
+          navigate={this.navigate}
+          navigateTo='question'
+          isFetching={this.state.isFetching} />;
+        break;
+    }
+
+    return outputContent;
+  }
+
+}

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

@@ -0,0 +1,96 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import questions from '../content/questions.json';
+import HeaderLightItem from './partials/HeaderLightItem.jsx';
+import QuestionItem from './partials/QuestionItem.jsx';
+import ScoreItem from './partials/ScoreItem.jsx';
+import IntroItem from './partials/IntroItem.jsx';
+import BubbleChartItem from './partials/BubbleChartItem.jsx';
+
+// module05: Question Screen
+// wrapper for the current question
+export default class QuestionScreen extends Component {
+
+  // construct and initialize functions
+  constructor (props) {
+    super(props);
+
+    this.state = { continue: true };
+
+    // context binding
+    this.setData = this.setData.bind(this);
+    this.toNext = this.toNext.bind(this);
+  }
+
+  // handle continue button visibility
+  setData (data) {
+    if (data.continue) this.setState({ continue: true });
+  }
+
+  // go to next screen
+  // also handles continue button visibility
+  toNext () {
+    const scoreMode = this.props.mode === 'score';
+    const introMode = this.props.mode === 'intro';
+    const questionMode = this.props.mode === 'question';
+
+    if (introMode || scoreMode || (questionMode && this.props.currentQuestion === 2) || questionMode && this.props.currentQuestion === 3) {
+      this.setState({ continue: false });
+    }
+    this.props.toNextQuestion();
+  }
+
+  // RENDER output depending on mode
+  // - a) intro text
+  // - b) current question
+  // - c) current score
+  render () {
+    let isCorrectClass = '';
+    let isIncorrectClass = '';
+    let label = '';
+
+    const hasAnswered = this.props.currentAnswerIndex !== null;
+    const question = questions[this.props.currentQuestion];
+
+    if (hasAnswered) {
+      isCorrectClass = 'correct';
+      isIncorrectClass = 'incorrect';
+    }
+
+    const mainClasses = 'wrapper__main  wrapper--centered  wrapper__ofwhat  wrapper__main--question';
+    label = this.props.mode === 'question' ? this.props.next : question.next;  // unlogischer spaghettiwirrsinn
+    let componentAbove = '';
+
+    switch (this.props.mode) {
+      case 'intro':
+        label = this.props.introContinue;
+        componentAbove = <IntroItem { ...this.props } />;
+        break;
+
+      case 'init':
+      case 'question':
+        componentAbove = <QuestionItem { ...this.props } setData={this.setData} />;
+        break;
+
+      default:
+        componentAbove = <ScoreItem { ...this.props } />;
+        break;
+    }
+
+    return (
+      <section className="wrapper  wrapper__question">
+        <HeaderLightItem { ...this.props } />
+        <main className={`${mainClasses}  ${isCorrectClass}  ${isIncorrectClass}`}>
+          {componentAbove}
+          <BubbleChartItem { ...this.props } hasAnswered={hasAnswered} setData={this.setData} continue={this.state.continue} />
+        </main>
+        <footer className="footer footer--chart">
+          {(this.props.currentAnswerIndex !== null || this.props.mode === 'intro') && this.state.continue
+            ? <a href="#" title={label} className="button--wide" onClick={() => this.toNext() }>
+              {label}
+            </a>
+            : []}
+        </footer>
+      </section>
+    );
+  }
+}

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

@@ -0,0 +1,19 @@
+import { h, render } from 'preact'; // eslint-disable-line no-unused-vars
+
+// shared Title component
+// displays intro text and a link to start the module
+const TitleScreen = props => ( // eslint-disable-line no-unused-vars
+  <section class="wrapper">
+    <header className="header">
+      <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;

+ 76 - 0
src/js/components/partials/BubbleChartItem.jsx

@@ -0,0 +1,76 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import d3BubbleChart from '../../d3/bubblechart';
+import d3BubbleChartDecoloring from '../../d3/bubblechart-decolor';
+import d3BubbleChartReduce from '../../d3/bubblechart-reduce';
+import d3BubbleChartDivide from '../../d3/bubblechart-divide';
+import * as d3 from 'd3';
+
+// module05: Bubble Chart Item
+// renders bubble chart
+export default class BubbleChartItem extends Component {
+
+  // LIFECYCLE methods
+  componentDidMount () {
+    // gets executet initially at start introscreen and first questionscreen
+    const options = {
+      numberOfNodes: this.props.currentNumberOfPoints,
+      amount: this.props.currentAmount
+    };
+    const bubbleChart = d3BubbleChart().options(options);
+
+    d3.select(this.container).call(bubbleChart);
+  }
+
+  shouldComponentUpdate (nextProps) {
+    // just keep bubblechart if mode is chart
+    // if it is not the second to the last question
+    const isFirstQuestion = nextProps.mode === 'init';
+    const isSecondQuestion = nextProps.currentQuestion === 2;
+    const isModeQuestion = nextProps.mode === 'question';
+    const isNotLastQuestion = nextProps.currentQuestion !== 3;
+
+    const cond = isFirstQuestion || isSecondQuestion || (isModeQuestion && isNotLastQuestion) || (!isNotLastQuestion && !isModeQuestion);
+
+    return cond && nextProps.continue === false;
+  }
+
+  // call different bubble charts depending on state and mode
+  componentWillUpdate (nextProps) {
+    let bubbleChart;
+    const options = { setData: this.props.setData };
+
+    if (nextProps.hasAnswered && (nextProps.voteRatios || nextProps.isOffline) && nextProps.mode === 'question') {
+      // reduce number of points
+      options.numberOfNodes = this.props.currentNumberOfPoints - nextProps.nextNumberOfPoints;
+      bubbleChart = d3BubbleChartReduce().options(options);
+    } else if (nextProps.currentQuestion === 2 && nextProps.mode === 'score') {
+      // divide points into two sets ordered not randomly
+      options.text = { left: this.props.mainstreamEater, right: this.props.sausageEater };
+      bubbleChart = d3BubbleChartDivide().options(options);
+    } else if (nextProps.currentQuestion === 3 && nextProps.mode === 'score') {
+      // last question, score mode – extend text
+      const extend = {
+        text: { left: this.props.mainstreamEater, right: this.props.sausageEater },
+        textPrepend: { left: 5, right: 6 },
+        textResultat: '18% mehr',
+        textFormula: `18% x 5 / 100 ${this.props.mainstreamEater}`,
+        step: 2
+      };
+
+      bubbleChart = d3BubbleChartDivide().options(Object.assign(options, extend));
+    } else {
+      // gets executet initially at start introscreen and first questionscreen
+      bubbleChart = d3BubbleChartDecoloring();
+    }
+
+    d3.select(this.container).call(bubbleChart);
+  }
+
+  // output entry point for d3js module
+  render () {
+    return (
+      <div className="chart  mod5__chart" ref={ elem => (this.container = elem) }></div>
+    );
+  }
+
+}

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

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

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

@@ -0,0 +1,13 @@
+import { h, render } from 'preact'; // eslint-disable-line no-unused-vars
+
+// module05: Intro Item
+// renders intro screen containing start feature
+const IntroItem = props => ( // eslint-disable-line no-unused-vars
+  <section className="wrapper--besides  wrapper--mb  highlight">
+    <div className="number--huge">{props.percent}</div>
+    <h2>{props.prediction.split('\n').map(item => <span>{item}<br /></span>)}</h2>
+  </section>
+);
+
+export default IntroItem;
+

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

@@ -0,0 +1,55 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import questions from '../../content/questions.json';
+
+// module05: Question Item
+// renders the current question
+export default class QuestionItem extends Component {
+
+  // construct and initialize functions
+  constructor (props) {
+    super(props);
+
+    // context binding
+    this.sendAnswer = this.sendAnswer.bind(this);
+  }
+
+  // send anser, call setData
+  sendAnswer (index, correct) {
+    this.props.setAnswer(index, correct);
+
+    // no animation at this point
+    if (this.props.currentQuestion === 3) {
+      this.props.setData({ continue: true });
+    }
+  }
+
+  // output question
+  render () {
+    const keyPrefix = `${this.props.currentQuestion}`;
+    const question = questions[this.props.currentQuestion];
+    const addClass = this.props.currentQuestion > 1 ? '  question__ofwhat--no-mb' : '';
+
+    return (
+      <div className={`question  question__ofwhat${addClass}`}>
+        { question.intro ? <div className="question__intro">{question.intro}</div> : [] }
+        <div className="question__title">{question.title}</div>
+        <div className="question__options">
+          {question.answers.map((answer, index) => {
+            const markerClass = index === this.props.currentAnswerIndex ? 'question__options__item--marked' : '';
+            const correctClass = answer.korrekt && this.props.currentAnswerIndex !== null ? 'question__options__item--correct' : '';
+
+            return (
+              <p className={`question__options__item ${markerClass} ${correctClass}`} key={`${keyPrefix}_${index}`}>
+                <input class="checkbox-input" name="option" id={`option-${index}`} value="1" type="radio" />
+                <label onClick={ () => this.sendAnswer(index, answer.korrekt) } class="checkbox-label" for={`option-${index}`}>
+                  {answer.antwort}
+                </label>
+              </p>);
+          }
+          ) }
+        </div>
+      </div>
+    );
+  }
+
+}

+ 25 - 0
src/js/components/partials/ScoreItem.jsx

@@ -0,0 +1,25 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import questions from '../../content/questions.json';
+import VoteItem from './VoteItem.jsx';
+
+// renders the current score
+export default class ScoreItem extends Component {
+
+  render () {
+    const question = questions[this.props.currentQuestion];
+    const correctAnswer = question.answers.find(answer => answer.korrekt === true);
+
+    return (
+      <div className="wrapper--sidebyside">
+        <div className="wrapper--sidebyside--first">
+          <h4 class="iscorrect">{correctAnswer.antwort}</h4>
+          <p class="no-mb">{question.description}</p>
+        </div>
+        { !this.props.isOffline
+          ? <VoteItem label={this.props.usersRight} votesFraction={this.props.voteRatios[0]} activeColor='#7ed321' width='410' />
+          : [] }
+      </div>
+    );
+  }
+
+}

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

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

+ 41 - 0
src/js/config.js

@@ -0,0 +1,41 @@
+// basic config containing global api base URL
+export default {
+  numberQuestions: 4,
+  numberOfPoints: {
+    first: 1200,
+    last: 200
+  },
+  highlightedAmount: 0.18,
+  api: {
+    create: 'M5_WurstverzehrUserData/Create',
+    proportions: 'M5_WurstverzehrUserData/ProportionCorrect',
+    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'
+    }
+  }
+};

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

@@ -0,0 +1,24 @@
+{
+  "title": "Prozent von was??",
+  "introtext": "Täglicher Wurstkonsum erhöht Darmkrebsrisiko um 18 %.\nDamit steht dieses Risiko auf der gleichen Gefahrenstufe der Weltgesundheitsorganisation WHO wie Asbest und Rauchen.",
+  "introContinue": "Worum geht es hier?",
+  "next": "Erklärung",
+  "start": "Start",
+  "restart": "Neustart",
+  "percent": "18 %",
+  "prediction": "mehr Darmkrebsrisiko bei erhöhtem Wurstkonsum",
+  "conclusion": "Was haben Sie gelernt?",
+  "finalTitle": "Fragen Sie sich immer, bei jeder Prozentangabe",
+  "finalSlogan": "„% von was“?",
+  "finalText": "Was ist „was“ genau?\nWie häufig kommt etwas normalerweise sowieso vor?\nDann lebt sich‘s viel entspannter.",
+  "reference": {
+    "source": "Quelle",
+    "author": "WHO",
+    "year": "(2015)",
+    "title": "Links between processed meat and colorectal cancer.",
+    "url": "http://www.who.int/mediacentre/news/statements/2015/processed-meat-cancer/en/"
+  },
+  "sausageEater": "Wurstesser",
+  "mainstreamEater": "Durchschnittsesser",
+  "usersRight": "Wie viele der anderen Nutzer lagen richtig?"
+}

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

@@ -0,0 +1,115 @@
+[
+  {
+    "intro": "Haben Sie sich dazu auch mal folgende Frage gestellt?",
+    "title": "Welche Wurst?",
+    "answers": [
+      {
+        "id": 1,
+        "antwort": "Verarbeitetes Fleisch",
+        "korrekt": true
+      },
+      {
+        "id": 2,
+        "antwort": "Rotes Fleisch",
+        "korrekt": false
+      },
+      {
+        "id": 3,
+        "antwort": "Geräuchertes Fleisch",
+        "korrekt": false
+      },
+      {
+        "id": 4,
+        "antwort": "Weißes Fleisch",
+        "korrekt": false
+      }
+    ],
+    "description": "Verarbeitetes Fleisch ist Fleisch, das durch Verarbeitungsprozesse wie Salzen, Räuchern, Reifen bzw. Fermentieren oder andere Verfahren verändert wurde, um den Geschmack zu verbessern oder es haltbar zu machen. Meistens sind Schweine-, Rind- oder Geflügelfleisch, aber auch Innereien enthalten. Wurstwaren, Schinken, Hackfleischprodukte und Fleischkonserven sind verarbeitetes Fleisch.",
+    "onext": "Worum geht es hier?",
+    "next": "Nur wie viel hiervon ist denn zu viel?"
+  },
+  {
+    "intro": "",
+    "title": "Wie viel ist „erhöhter Konsum“?",
+    "answers": [
+      {
+        "id": 1,
+        "antwort": "5 g pro Tag",
+        "korrekt": false
+      },
+      {
+        "id": 2,
+        "antwort": "10 g pro Tag",
+        "korrekt": false
+      },
+      {
+        "id": 3,
+        "antwort": "50 g pro Tag",
+        "korrekt": true
+      },
+      {
+        "id": 4,
+        "antwort": "100 g pro Tag",
+        "korrekt": false
+      }
+    ],
+    "description": "Für je 50 g mehr verarbeitetes Fleisch am Tag steigt das besagte Risiko. Zum Vergleich, ca. 40 g verarbeitetes Fleisch trägt ein Salamibrötchen oder auch ein Mettbrötchen. 300 g sind in zwei Bratwürsten enthalten und 100 g enthält ein Dönerteller.",
+    "next": "18 % von was?"
+  },
+  {
+    "intro": "",
+    "title": "18 % von was oder wem?",
+    "answers": [
+      {
+        "id": 1,
+        "antwort": "von je 100 Wurstessern kriegen 18 Darmkrebs",
+        "korrekt": false
+      },
+      {
+        "id": 2,
+        "antwort": "von je 100 Wurstessern kriegen 18 mehr Darmkrebs als von 100 Wurstverzichtern",
+        "korrekt": false
+      },
+      {
+        "id": 3,
+        "antwort": "pro 100 Wurstverzichter, die an Darmkrebs erkranken, kriegen 118 Wurstesser, die 50 g mehr als die Allgemeinbevölkerung essen, Darmkrebs",
+        "korrekt": false
+      },
+      {
+        "id": 4,
+        "antwort": "pro 100 Durchschnitts­personen, die an Darmkrebs erkranken, kriegen 118 Wurstesser, die 50 g mehr als der Durchschnitt essen, Darmkrebs",
+        "korrekt": true
+      }
+    ],
+    "description": "Diese Wurstesser kriegen 18 % häufiger Darmkrebs als Durchschnittsesser. Die Angabe beschreibt ein relatives Risiko und sagt nichts darüber aus, wie oft die beiden Gruppen Darmkrebs kriegen. Das tatsächliche (absolute) Risiko des Wurstverzehrs ist dadurch noch nicht erkennbar.",
+    "next": "Nur wie häufig kommt Darmkrebs nun tatsächlich vor?"
+  },
+  {
+    "intro": "",
+    "title": "Nur wie häufig kommt Darmkrebs normalerweise vor und wie viel häufiger beim Wurstesser mit 50 g höherem Wurstkonsum?",
+    "answers": [
+      {
+        "id": 1,
+        "antwort": "von je 100 Wurstessern mit erhöhtem Konsum kriegen 6 statt nur 5 in ihrem Leben Darmkrebs",
+        "korrekt": true
+      },
+      {
+        "id": 2,
+        "antwort": "von je 100 Wurstessern mit erhöhtem Konsum kriegen 0,6 statt nur 0,5 in ihrem Leben Darmkrebs",
+        "korrekt": false
+      },
+      {
+        "id": 3,
+        "antwort": "von je 100 Wurstessern mit erhöhtem Konsum kriegen 60 statt nur 50 in ihrem Leben Darmkrebs",
+        "korrekt": false
+      },
+      {
+        "id": 4,
+        "antwort": "von je 100 Wurstessern mit erhöhtem Konsum kriegen 0,06 statt nur 0,05 in ihrem Leben Darmkrebs",
+        "korrekt": false
+      }
+    ],
+    "description": "",
+    "next": "Was haben Sie gelernt?"
+  }
+]

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

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

+ 17 - 0
src/js/d3/bubblechart-decolor.js

@@ -0,0 +1,17 @@
+import * as d3 from 'd3';
+
+// d3js module for module05: unify circle color
+export default () => {
+  // start
+  const bubbleChart = selection => {
+    selection.selectAll('.circle--colored')
+      .transition()
+      .delay(d => ((d.x + d.y) * Math.random()))
+      .ease(d3.easeSin)
+      .duration(1000)
+      .attr('fill', '#D8D8D8')
+      .attr('class', 'circle');
+  };
+
+  return bubbleChart;
+};

+ 101 - 0
src/js/d3/bubblechart-divide-gradual.js

@@ -0,0 +1,101 @@
+import * as d3 from 'd3';
+
+// d3js module for module05: divide bubble chart gradual
+export default (selection, options) => {
+  const duration = 500;
+  const delay = 500;
+
+  // step 2: update text
+  selection.select('.mod5-text--left')
+    .text(`${options.textPrepend.left} / ${options.numberOfNodes / 2} ${options.text.left}`);
+
+  selection.select('.mod5-text--right')
+    .text(`${options.textPrepend.right} / ${options.numberOfNodes / 2} ${options.text.right}`);
+
+  // append text – FORMULA
+  selection.append('text')
+    .attr('x', options.xvalTextLeft)
+    .attr('y', options.height / 2)
+    .attr('fill-opacity', 0)
+    .attr('class', 'mod5-text  mod5-text--formula')
+    .style('text-anchor', 'middle')
+    .text(options.textFormula)
+    .transition()
+    .ease(d3.easeSin)
+    .duration(duration)
+    .delay(duration)
+    .attr('fill-opacity', 1);
+
+  // append text – PLUS
+  selection.append('text')
+    .attr('x', options.xvalTextLeft)
+    .attr('y', options.height / 1.6)
+    .attr('fill-opacity', 0)
+    .attr('class', 'mod5-text  mod5-text--formula')
+    .style('text-anchor', 'middle')
+    .text('+')
+    .transition()
+    .ease(d3.easeSin)
+    .duration(duration)
+    .delay(duration + delay)
+    .attr('fill-opacity', 1);
+
+  // append text – EQUALSIGN
+  selection.append('text')
+    .attr('x', options.width / 2)
+    .attr('y', options.height / 1.6)
+    .attr('fill-opacity', 0)
+    .attr('class', 'mod5-text  mod5-text--formula')
+    .style('text-anchor', 'middle')
+    .text('=')
+    .transition()
+    .ease(d3.easeSin)
+    .duration(duration)
+    .delay(duration + delay * 2)
+    .attr('fill-opacity', 1);
+
+  // append text - RESULT
+  selection.append('text')
+    .attr('x', options.xvalTextRight)
+    .attr('y', options.height / 2)
+    .attr('fill-opacity', 0)
+    .attr('class', 'mod5-headline')
+    .style('text-anchor', 'middle')
+    .text(options.textResultat)
+    .transition()
+    .ease(d3.easeSin)
+    .duration(duration)
+    .delay(duration + delay * 3)
+    .attr('fill-opacity', 1);
+
+  // append arrow – ARROW
+  selection.append('line')
+    .attr('class', 'mod5-arrow__line')
+    .attr('stroke-width', 2)
+    .attr('stroke-opacity', 0)
+    .attr('x1', options.xvalTextRight)
+    .attr('y1', options.height / 2 + 20)
+    .attr('x2', options.xvalTextRight)
+    .attr('y2', options.height / 2 + 50)
+    .transition()
+    .ease(d3.easeSin)
+    .duration(duration)
+    .delay(duration + delay * 4)
+    .attr('marker-end', 'url(#arrow)')
+    .attr('stroke-opacity', 1)
+    .on('end', () => options.setData({ continue: true }));
+
+  // highlight circles - left
+  selection.selectAll('.circle--left')
+    .filter((d, index) => [ 0, 1, 2, 5, 6 ].includes(index))
+    .transition()
+    .duration(1000)
+    .attr('fill', '#D0021B');
+
+  // highlight circles - right
+  selection.selectAll('.circle--right')
+    .filter((d, index) => [ 0, 1, 2, 5, 6, 7 ].includes(index))
+    .transition()
+    .duration(1000)
+    .attr('fill', '#D0021B');
+};

+ 113 - 0
src/js/d3/bubblechart-divide.js

@@ -0,0 +1,113 @@
+import addGradualContent from './bubblechart-divide-gradual';
+
+// d3js module for module05: divide bubble chart
+export default () => {
+  // defaults
+  const options = {
+    width: 912,
+    height: 350,
+    radius: 5,
+    numberOfNodes: 200,
+    rows: 5,
+    offset: 5,
+    spacingInner: 40,
+    fontsize: 18,
+    text: { left: 'left headline', right: 'right headline' },
+    textPrepend: { left: 5, right: 6 },
+    textFormula: 'a² + b²',
+    textResultat: '18% mehr',
+    step: 1,
+    setData: () => {} // eslint-disable-line no-empty-function
+  };
+
+
+  // start
+  const bubbleChart = selection => {
+    // node selection
+    const svg = selection.select('svg');
+    const circles = svg.selectAll('.circle');
+    const circlesLeft = circles.filter((d, i) => (i < options.numberOfNodes / 2));
+    const circlesRight = circles.filter((d, i) => (i >= options.numberOfNodes / 2));
+
+    // calculate defaults
+    const cols = options.numberOfNodes / 2 / options.rows;
+    const rowWidth = options.radius * 2 * cols + options.offset * (cols - 1);
+    const colHeight = options.rows * options.radius * 2 + (options.rows - 1) * options.offset;
+    const blockOffset = {
+      width: options.width / 2 - rowWidth,
+      height: options.height - colHeight
+    };
+    const margin = {
+      left: blockOffset.width - options.spacingInner / 2,
+      right: options.width / 2 + options.spacingInner / 2
+    };
+    const bottomLimit = options.height - options.radius;
+    const unitWidth = options.radius * 2 + options.offset; // width circle incl. offset right
+    const yvalText = blockOffset.height - options.spacingInner / 2;
+    const xvalTextLeft = margin.left + rowWidth / 2;
+    const xvalTextRight = margin.right + rowWidth / 2;
+    const duration = 3000;
+
+    // merge options
+    Object.assign(options, { xvalTextLeft, xvalTextRight });
+
+    // divide circles
+    if (options.step === 1) {
+      // circles left change coordinates
+      circlesLeft
+        .attr('class', 'circle  circle--left')
+        .transition()
+        .duration(duration)
+        .attr('cx', d => (margin.left + parseInt(d.index / options.rows, 10) * unitWidth))
+        .attr('cy', d => (bottomLimit - parseInt(d.index % options.rows, 10) * unitWidth));
+
+      // circles right change coordinates
+      circlesRight
+        .attr('class', 'circle  circle--right')
+        .transition()
+        .duration(duration)
+        .attr('cx', d => (margin.right + parseInt((d.index - 100) / options.rows, 10) * unitWidth))
+        .attr('cy', d => (bottomLimit - parseInt(d.index % options.rows, 10) * unitWidth))
+        .on('end', (d, i) => {
+          if (circlesRight.size() === i + 1) options.setData({ continue: true });
+        });
+
+      // append text left
+      svg.append('text')
+        .transition()
+        .delay(duration / 2)
+        .duration(500)
+        .attr('x', xvalTextLeft)
+        .attr('y', yvalText)
+        .attr('class', 'mod5-text  mod5-text--left')
+        .style('font-size', `${options.fontsize}px`)
+        .style('text-anchor', 'middle')
+        .text(`${options.numberOfNodes / 2} ${options.text.left}`);
+
+      // append text right
+      svg.append('text')
+        .transition()
+        .delay(duration / 2)
+        .duration(500)
+        .attr('x', xvalTextRight)
+        .attr('y', yvalText)
+        .attr('class', 'mod5-text  mod5-text--right')
+        .style('font-size', `${options.fontsize}px`)
+        .style('text-anchor', 'middle')
+        .text(`${options.numberOfNodes / 2} ${options.text.right}`);
+
+    } else {
+      addGradualContent(svg, options);
+    }
+
+  };
+
+  // "setter"
+  bubbleChart.options = input => {
+    Object.assign(options, input);
+
+    return bubbleChart;
+  };
+
+  return bubbleChart;
+};

+ 116 - 0
src/js/d3/bubblechart-reduce.js

@@ -0,0 +1,116 @@
+import * as d3 from 'd3';
+
+// d3js module for module05: reduce bubble chart
+export default () => {
+  // defaults
+  const options = {
+    width: 912,
+    height: 350,
+    radius: 5,
+    numberOfNodes: 500,
+    setData: () => {} // eslint-disable-line no-empty-function
+  };
+
+  // start
+  const bubbleChart = selection => {
+    const svg = selection.select('svg');
+
+    // shuffle array
+    const shuffle = array => {
+      let currentIndex = array.length;
+      let temporaryValue;
+      let randomIndex;
+
+      // While there remain elements to shuffle...
+      while (currentIndex !== 0) {
+        // Pick a remaining element...
+        randomIndex = Math.floor(Math.random() * currentIndex);
+        currentIndex -= 1;
+
+        // And swap it with the current element.
+        temporaryValue = array[currentIndex];
+        array[currentIndex] = array[randomIndex];
+        array[randomIndex] = temporaryValue;
+      }
+
+      return array;
+    };
+
+    // select `numberOfNodes` random
+    const circles = svg.selectAll('.circle');
+    const baseArr = [];
+
+    for (let i = 0; i < circles.size(); i += 1) baseArr.push(i);
+
+    // segmenting circles
+    const shuffledArr = shuffle(baseArr);
+    const falling = shuffledArr.slice(0, options.numberOfNodes);
+    const keeping = shuffledArr.slice(options.numberOfNodes, shuffledArr.length);
+    const circlesFalling = circles.filter(d => falling.includes(d.index));
+    const circlesKeeping = circles.filter(d => keeping.includes(d.index));
+    const factor = circlesKeeping.size() === 200 ? 0.3 : 0.07;
+
+    // reorder remaining circles at the bottom
+    const reorderRemaining = () => {
+      const ticked = () => {
+        circlesKeeping
+          .attr('cx', d => (d.x = Math.max(options.radius, Math.min(options.width - options.radius, d.x))))
+          .attr('cy', d => (d.y = Math.min(options.height - options.radius, d.y)));
+      };
+
+      // .. using a force simulation
+      const simulation = d3.forceSimulation()
+        .force('y', d3.forceY(() => (options.height))
+          .strength(d => ((options.height - d.y) / (options.height) * factor)))
+        .velocityDecay(0.6)
+        .alphaDecay(0.08)
+        .force('collide', d3.forceCollide(options.radius * 2)
+          .strength(0.5)
+        );
+
+      // add timer
+      const endTime = 1500;
+      const transitionTimer = d3.timer(elapsed => {
+        const dt = elapsed / endTime;
+
+        if (dt >= 1.0) {
+          transitionTimer.stop();
+          simulation.stop();
+          options.setData({ continue: true });
+        }
+      });
+
+      // move circles
+      simulation.nodes(circlesKeeping.data())
+        .on('tick', ticked);
+    };
+
+    // change color of falling circles
+    circlesFalling
+      .transition()
+      .ease(d3.easeSin)
+      .duration(1000)
+      .attr('fill', '#D0021B');
+
+    // change y coordinate of falling circles
+    // after they have been fallen down, remove them
+    circlesFalling
+      .transition()
+      .delay(1000)
+      .duration(d => ((options.height - d.y) * 5))
+      .ease(d3.easeCircleIn)
+      .attr('cy', options.height * 1.25)
+      .remove();
+
+    window.setTimeout(() => (reorderRemaining()), 1500);
+  };
+
+  // "setter"
+  bubbleChart.options = input => {
+    Object.assign(options, input);
+
+    return bubbleChart;
+  };
+
+  return bubbleChart;
+};

+ 90 - 0
src/js/d3/bubblechart.js

@@ -0,0 +1,90 @@
+import * as d3 from 'd3';
+import addDefs from './defs';
+
+// d3js module for module05: initiale bubble chart, colored center
+export default () => {
+  // defaults
+  const options = {
+    width: 912,
+    height: 350,
+    radius: 5,
+    numberOfNodes: 500,
+    remainingNodes: 200,
+    amount: 0.5
+  };
+
+  // start
+  const bubbleChart = selection => {
+    // get number of nodes highlighted
+    const numberOfNodesHighlighted = options.numberOfNodes * options.amount;
+
+    const svg = selection
+      .append('svg')
+      .attr('width', options.width)
+      .attr('height', options.height);
+
+    // add svg definitions
+    addDefs(svg);
+
+    // determine ramdom data points
+    const datapoints = d3.range(options.numberOfNodes).map(() => {
+      const obj = {
+        x: Math.round(Math.random() * (options.width - options.radius * 2) + options.radius),
+        y: Math.round(Math.random() * (options.height - options.radius * 2) + options.radius)
+      };
+      const distance = Math.sqrt(Math.pow(obj.x - options.width / 2, 2) + Math.pow(obj.y - options.height / 2, 2));
+
+      Object.assign(obj, { distance });
+
+      return obj;
+    });
+
+    // sort them by distance
+    datapoints.sort((a, b) => (d3.ascending(a.distance, b.distance)));
+
+    // append circles, colored regarding to distance
+    const circles = svg.selectAll('.circle')
+      .data(datapoints)
+      .enter()
+      .append('circle')
+      .attr('r', options.radius)
+      .attr('class', (d, i) => (i < numberOfNodesHighlighted ? 'circle  circle--colored' : 'circle'))
+      .attr('fill', (d, i) => (i < numberOfNodesHighlighted ? '#D0021B' : '#D8D8D8'));
+
+    // move them smoothly
+    const ticked = () => {
+      circles
+        .attr('cx', d => (d.x = Math.max(options.radius, Math.min(options.width - options.radius, d.x))))
+        .attr('cy', d => (d.y = Math.max(options.radius, Math.min(options.height - options.radius, d.y))));
+    };
+
+    // use a force simulation to get closer to the determined destination
+    const simulation = d3.forceSimulation()
+      .force('x', d3.forceX(d => d.x))
+      .force('y', d3.forceY(d => d.y))
+      .force('collide', d3.forceCollide(options.radius * 2).strength(0.8));
+
+    // add timer
+    const endTime = 4000;
+    const transitionTimer = d3.timer(elapsed => {
+      const dt = elapsed / endTime;
+
+      if (dt >= 1.0) {
+        transitionTimer.stop();
+        simulation.stop();
+      }
+    });
+
+    simulation.nodes(datapoints)
+      .on('tick', ticked);
+  };
+
+  // "setter"
+  bubbleChart.options = input => {
+    Object.assign(options, input);
+
+    return bubbleChart;
+  };
+
+  return bubbleChart;
+};

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

@@ -0,0 +1,16 @@
+// d3js module for module05: add svg definitions
+export default (selection) => {
+  const defs = selection.append('defs');
+
+  defs.append('marker')
+    .attr('id', 'arrow')
+    .attr('orient', 'auto')
+    .attr('refX', 5)
+    .attr('refY', 0)
+    .attr('viewBox', '0 -5 10 10')
+    .attr('markerHeight', 4)
+    .attr('markerWidth', 4)
+    .append('path')
+    .attr('d', 'M0,-5L10,0L0,5')
+    .attr('class', 'mod5-arrow__head');
+};

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

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

@@ -0,0 +1,83 @@
+// format helper to get a fixed number of digits
+const fixedDigits = (number, numberDigits) => {
+  const str = `${number}`;
+  let pad = '';
+
+  for (let i = 0; i < numberDigits; i += 1) {
+    pad += '0';
+  }
+
+  return pad.substring(0, pad.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;
+};
+
+const round = (value, digits) => {
+  const precision = Math.pow(10, digits);
+
+  return Math.round(value * precision) / precision;
+};
+
+// - 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 = (probability1, probability2) => {
+
+  const p1 = getFormattedValue(probability1);
+  const p2 = getFormattedValue(probability2);
+
+  return p1 === p2 || (p1 < 0.1 && p2 < 0.1);
+};
+// };
+
+export {
+  fixedDigits,
+  needsSpacing,
+  getPrecision,
+  getSpacing,
+  getFormattedValue,
+  round,
+  haveSameRepresentation
+};
+
+export default '';  // transpiler complains without default export @#$%^&

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,311 @@
+svg { 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;
+    }
+  }
+}

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

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

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff