Jelajahi Sumber

Initial commit

Michael Zitzmann 5 tahun lalu
melakukan
0e98aad7da
100 mengubah file dengan 4779 tambahan dan 0 penghapusan
  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
      config.js
  8. 38 0
      doc/01_introduction.md
  9. 31 0
      doc/02_structure.md
  10. 105 0
      doc/03_module.md
  11. 101 0
      doc/04_development.md
  12. 32 0
      doc/05_javascript.md
  13. 30 0
      doc/06_styles.md
  14. 15 0
      doc/07_images.md
  15. 2 0
      doc/html/css/github-markdown.css
  16. 121 0
      doc/html/css/github-syntax-highlight.css
  17. 233 0
      doc/html/index.html
  18. 127 0
      gulpfile.babel.js
  19. 72 0
      package.json
  20. 9 0
      public/browserconfig.xml
  21. TEMPAT SAMPAH
      public/favicon.ico
  22. 5 0
      public/robots.txt
  23. 432 0
      readme.md
  24. TEMPAT SAMPAH
      readme.pdf
  25. TEMPAT SAMPAH
      src/fonts/roboto-light.woff
  26. TEMPAT SAMPAH
      src/fonts/roboto-light.woff2
  27. TEMPAT SAMPAH
      src/fonts/roboto-medium.woff
  28. TEMPAT SAMPAH
      src/fonts/roboto-medium.woff2
  29. TEMPAT SAMPAH
      src/fonts/roboto-regular.woff
  30. TEMPAT SAMPAH
      src/fonts/roboto-regular.woff2
  31. TEMPAT SAMPAH
      src/fonts/roboto-thin.woff
  32. TEMPAT SAMPAH
      src/fonts/roboto-thin.woff2
  33. TEMPAT SAMPAH
      src/fonts/robotomono-light.woff
  34. TEMPAT SAMPAH
      src/fonts/robotomono-light.woff2
  35. 31 0
      src/html/index.html
  36. TEMPAT SAMPAH
      src/img/android-chrome-192x192.png
  37. TEMPAT SAMPAH
      src/img/android-chrome-512x512.png
  38. TEMPAT SAMPAH
      src/img/apple-touch-icon.png
  39. TEMPAT SAMPAH
      src/img/favicon-16x16.png
  40. TEMPAT SAMPAH
      src/img/favicon-32x32.png
  41. TEMPAT SAMPAH
      src/img/mstile-150x150.png
  42. 3 0
      src/img/safari-pinned-tab.svg
  43. 1 0
      src/img/sprites.svg
  44. 3 0
      src/img/sprites/correct.svg
  45. 3 0
      src/img/sprites/incorrect.svg
  46. 15 0
      src/img/sprites/sprites.yaml
  47. 1 0
      src/img/sprites/triangle.svg
  48. 19 0
      src/js/components/FinalScreen.jsx
  49. 126 0
      src/js/components/Index.jsx
  50. 147 0
      src/js/components/PollScreen.jsx
  51. 19 0
      src/js/components/TitleScreen.jsx
  52. 160 0
      src/js/components/partials/AnimItem.jsx
  53. 15 0
      src/js/components/partials/HeaderLightItem.jsx
  54. 17 0
      src/js/components/partials/LegendItem.jsx
  55. 86 0
      src/js/components/partials/QuestionItem.jsx
  56. 31 0
      src/js/components/partials/TextItem.jsx
  57. 31 0
      src/js/config.js
  58. 7 0
      src/js/content/data.json
  59. 140 0
      src/js/content/module.json
  60. 50 0
      src/js/content/questions.json
  61. 72 0
      src/js/d3/init-barcharts.js
  62. 42 0
      src/js/d3/init-circles.js
  63. 12 0
      src/js/d3/init-countertext.js
  64. 36 0
      src/js/d3/init-pollresults.js
  65. 27 0
      src/js/d3/poll-color.js
  66. 85 0
      src/js/d3/poll-count.js
  67. 56 0
      src/js/d3/poll-dopoll-falling.js
  68. 83 0
      src/js/d3/poll-dopoll-update.js
  69. 70 0
      src/js/d3/poll-dopoll.js
  70. 115 0
      src/js/d3/poll-generator.js
  71. 62 0
      src/js/d3/poll-moveup.js
  72. 33 0
      src/js/d3/poll-showdiff.js
  73. 36 0
      src/js/d3/poll-sort.js
  74. 105 0
      src/js/d3/poll-sortbars.js
  75. 39 0
      src/js/d3/poll.js
  76. 13 0
      src/js/main.jsx
  77. 6 0
      src/js/utilities/enableTouch.js
  78. 32 0
      src/js/utilities/fonts.js
  79. 93 0
      src/js/utilities/formatter.js
  80. 6 0
      src/js/utilities/math.js
  81. 19 0
      src/scss/base/_fonts.scss
  82. 52 0
      src/scss/base/_forms.scss
  83. 48 0
      src/scss/base/_headings.scss
  84. 12 0
      src/scss/base/_links.scss
  85. 24 0
      src/scss/base/_rhythm.scss
  86. 30 0
      src/scss/base/_root.scss
  87. 17 0
      src/scss/config/_breakpoints.scss
  88. 32 0
      src/scss/config/_colors.scss
  89. 37 0
      src/scss/config/_defaults.scss
  90. 73 0
      src/scss/config/_fonts.scss
  91. 62 0
      src/scss/main.scss
  92. 21 0
      src/scss/modules/_animations.scss
  93. 115 0
      src/scss/modules/_answers.scss
  94. 29 0
      src/scss/modules/_boxes.scss
  95. 49 0
      src/scss/modules/_buttons.scss
  96. 36 0
      src/scss/modules/_content.scss
  97. 311 0
      src/scss/modules/_d3js.scss
  98. 10 0
      src/scss/modules/_debug.scss
  99. 11 0
      src/scss/modules/_footer.scss
  100. 92 0
      src/scss/modules/_forms.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
config.js

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

+ 31 - 0
doc/02_structure.md

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

+ 105 - 0
doc/03_module.md

@@ -0,0 +1,105 @@
+## Modul 4: Stichproben verstehen
+
+Ziel dieses Moduls ist die Verbesserung des Verständnisses darüber, wie Stichproben entstehen.  
+Dafür werden Methoden des erfahrungsbasierten Lernens eingesetzt, die in in einer linear erzählten Geschichte eingebettet sind.
+
+### Javascript Verzeichnisstruktur
+
+Für einen schnellen Überblick sind hier die Quell-Dateien mit kurzen Beschreibungen aufgelistet:
+
+```
+├── main.jsx                      // Einstiegspunkt für App
+├── config.js                     // Globale Konfiguration der WebApp
+├── components                    // (p)react Komponenten
+│   ├── Index.jsx                 // Web App Haupt-Komponente
+│   ├── FinalScreen.jsx           // Abschließende Ansicht
+│   ├── PollScreen.jsx            // View für die interaktive Visualisierung
+│   ├── TitleScreen.jsx           // Einleitende Ansicht
+│   └── partials
+│       ├── AnimItem.jsx          // d3 Visualisierungen
+│       ├── HeaderLightItem.jsx   // Kopfzeile
+│       ├── LegendItem.js         // Legende der Graphen
+│       ├── QuestionItem.jsx      // Multiple Choice Fragen
+│       └── TextItem.jsx          // Begleitende Texte
+├── content
+│   ├── data.json                 // Geordnete Liste von Klassendefinitionen, aus denen Stichproben gezogen werden
+│   ├── module.json               // Allgemeine Texte, Texte und Daten der Abschnitte
+│   └── questions.json            // Definition der Mutiple Choice Fragen
+├── d3
+│   ├── init-barcharts.js
+│   ├── init-circles.js
+│   ├── init-countertext.js
+│   ├── init-pollresults.js
+│   ├── poll-color.js
+│   ├── poll-count.js
+│   ├── poll-dopoll-falling.js
+│   ├── poll-dopoll-update.js
+│   ├── poll-dopoll.js
+│   ├── poll-generator.js
+│   ├── poll-moveup.js
+│   ├── poll-showdiff.js
+│   ├── poll-sort.js
+│   ├── poll-sortbars.js
+│   └── poll.js                   // d3 Einstiegsmodul
+└── utilities
+    ├── enableTouch.js
+    ├── fonts.js
+    ├── formatter.js
+    └── math.js
+```
+
+### Wie ändere ich Inhalte und Daten?
+
+#### Labels
+
+##### Umgebende Texte
+
+In `module.json` sind Texte und Labels definiert, auch solche, die nicht direkt Teil WebApp sind. Dies umfasst den Titel (`"title"`) der WebApp, den einleitenden Text (`"introtext"`) und das Label des Start-Buttons:
+
+
+    "title": "Glaube keiner Statistik…",
+    "introtext": "..deren Zustandekommen du nicht verstanden hast. …",
+    "start": "Start",
+    "sections": [
+      {
+        "headline": "Amerikanische Forscher haben gezeigt...",
+        "text": "Bargeldabschaffung als Beispiel um wissenschaftliche Studien zu erklären. …",
+        "swap": false,
+        "question": null,
+        "next": [ "Weiter" ]
+      }, …
+
+##### Multiple Choice Fragen
+
+Darüberhinaus ist die Abfolge der einzelnen Abschnitte definiert. Diese verfügen über Überschrift, erklärenden Text, und gegebenenfalls die ID einer multiple Choice Frage. Diese sind in `questions.json` definiert und sehen folgendermaßen aus:
+
+    {
+      "intro": "Eine Zwischenfrage",
+      "title": "Worauf ist denn zu achten, wenn man so eine kleine Gruppe befragt?",
+      "answers": [
+        {
+          "id": 1,
+          "antwort": "Vorab festzulegen, wen das Abbild repräsentieren soll",
+          "korrekt": true,
+          "feedback": "Sie legen unbedingt vorab fest, …"
+        }, …
+      ]
+    }
+
+Hier werden Einleitungstext, Titel und Antworten der Fragen festgelegt. Jede Frage verfügt über mehrere Antwortoptionen. Diese bestehen aus einem initialen Text (`"antwort"`), die Information, ob diese Option richtig ist (`"korrekt"`) und einen (optionalen) zusätzlichen Text (`"feedback"`), der eingeblendet wird, wenn die Option fälschlicherweise ausgewählt bzw. nicht ausgewählt wurde.
+
+#### Daten
+
+Die Daten, auf der dieses Modul basiert, beschreiben Ereignisse und deren Eintrittswahrscheinlichkeiten:
+
+
+    [
+      { "label": "klar dafür", "score": 4.1, "cumul": 4.1, "color": "#4a90e2" },
+      { "label": "eher dafür", "score": 7.6, "cumul": 11.7, "color": "#90CFEB" },
+      { "label": "unentschieden", "score": 1, "cumul": 12.7, "color": "#969696" },
+      { "label": "eher dagegen", "score": 3.3, "cumul": 16, "color": "#FD8C33" },
+      { "label": "klar dagegen", "score": 84, "cumul": 100, "color": "#FC4C2D" }
+    ]
+
+Der beschreibende Text des Ereignisses wird unter `"label"` definiert. `"score"` bezeichnet die Wahrcheinlichkeit in Prozent. Mit `"cumul"` wird die kumulierte Wahrscheinlichkeit angegeben und unter `"color"` wird die Farbe festgelegt. Erignisse sind aufsteigend nach ihrer Eintrittswahrscheinlichkeit sortiert. Alle Stichproben werden auf Grundlage dieser Klassendefinitionen gezogen.
+Wichtig ist, dass es sich hierbei um disjunkte Ereignisse handelt und sich alle Wahrscheinlichkeiten zu 1 (100 %) aufsummieren müssen.

+ 101 - 0
doc/04_development.md

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

+ 32 - 0
doc/05_javascript.md

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

+ 30 - 0
doc/06_styles.md

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

+ 15 - 0
doc/07_images.md

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

File diff ditekan karena terlalu besar
+ 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
+};

+ 72 - 0
package.json

@@ -0,0 +1,72 @@
+{
+  "name": "understanding-sampling",
+  "version": "1.0.0",
+  "author": "Tabea David <tabea.david@kf-interactive.com>, zitzmann@mpib-berlin.mpg.de",
+  "browserslist": [
+    "last 2 versions",
+    "ie >= 11"
+  ],
+  "devDependencies": {
+    "@babel/core": "^7.10.1",
+    "@babel/plugin-proposal-object-rest-spread": "^7.10.1",
+    "@babel/plugin-transform-object-assign": "^7.10.1",
+    "@babel/plugin-transform-react-jsx": "^7.10.1",
+    "@babel/preset-env": "^7.10.1",
+    "@babel/preset-react": "^7.10.1",
+    "@babel/register": "^7.10.1",
+    "@rollup/plugin-babel": "^5.0.2",
+    "@rollup/plugin-commonjs": "^12.0.0",
+    "@rollup/plugin-json": "^4.0.3",
+    "@rollup/plugin-node-resolve": "^8.0.0",
+    "autoprefixer": "^9.8.0",
+    "babel-eslint": "^10.1.0",
+    "babel-preset-env": "^1.7.0",
+    "babel-preset-react": "^6.24.1",
+    "browser-sync": "^2.26.7",
+    "cssnano": "^4.1.10",
+    "del": "^5.1.0",
+    "eslint-plugin-react": "^7.20.0",
+    "gulp": "^4.0.2",
+    "gulp-cached": "^1.1.1",
+    "gulp-eslint": "^6.0.0",
+    "gulp-htmlhint": "^3.0.0",
+    "gulp-if": "^3.0.0",
+    "gulp-plumber": "^1.2.1",
+    "gulp-postcss": "^8.0.0",
+    "gulp-rename": "^2.0.0",
+    "gulp-replace": "^1.0.0",
+    "gulp-sass": "^4.1.0",
+    "gulp-sass-lint": "^1.4.0",
+    "gulp-sourcemaps": "^2.6.5",
+    "gulp-svg-sprite": "^1.5.0",
+    "lodash": "^4.17.15",
+    "posthtml": "^0.13.0",
+    "posthtml-expressions": "^1.4.1",
+    "rollup": "^2.11.2",
+    "rollup-plugin-terser": "^6.1.0"
+  },
+  "scripts": {
+    "build": "gulp build",
+    "build:dev": "npm run build",
+    "build:prod": "NODE_ENV=production npm run build",
+    "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"
+  },
+  "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>

TEMPAT SAMPAH
public/favicon.ico


+ 5 - 0
public/robots.txt

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

+ 432 - 0
readme.md

@@ -0,0 +1,432 @@
+Inhalt
+------
+
+-   [Einführung](#einführung)
+-   [Übersicht der Module](#übersicht-der-module)
+-   [Verzeichnisstruktur](#verzeichnisstruktur)
+-   [Modul 4: Stichproben verstehen](#modul-4-stichproben-verstehen)
+-   [Entwicklungsumgebung](#build-tool-chain)
+-   [Javascript](#javascript)
+-   [Sass](#sass)
+-   [Bilddateien](#bilddateien)
+-   [Entwicklungshistorie](#entwicklungshistorie)
+
+Einführung
+----------
+
+Das Projekt [RisikoAtlas](https://risikoatlas.de) hat die Förderung der
+Risikokompetenz zum Ziel. Zu diesem Zweck wurden am Harding-Zentrum für
+Risikokompetenz am Max-Planck-Institut für Bildungsforschung digitale
+Werkzeuge entwickelt, die auf wissenschaftlichen Erkenntnissen beruhen.
+Neben interaktiven Visualisierungen evidenzbasierter
+Risikokommunikation, einer App zur Entscheidungsunterstützung und einer
+Browser-Erweiterung als Leseassistenz sind Lernvisualisierungen zur
+Verbesserung der Risikokompetenz Teil des Projektes. Das vorliegende
+Modul ist eines dieser sechs Lernmodule.
+
+Die Lernmodule wurden ursprünglich entwickelt von [kf
+interactive](https://www.kf-interactive.com).
+
+Übersicht der Module
+--------------------
+
+Die folgende Liste gibt einen Überblick über die Ziele der Module:
+
+1.  Module01 -- Risiken vergleichen **\***
+2.  Module02 -- Diagramme verstehen **\***
+3.  Module03 -- Trends schätzen **\***
+4.  **Module04 -- Stichproben verstehen** (original: [Rock 'n
+    poll](http://rocknpoll.graphics/))
+5.  Module05 -- Relative Risiken verstehen **\***
+6.  Module06 -- Wachstumsprozesse verstehen
+
+Alle mit einem **\*** versehenen Module greifen über eine API auf eine
+Datenbank zu. Auf diese Weise rufen sie die benötigten Daten ab, und
+speichern andererseits Antworten der Benutzer, um ihnen den Vergleich zu
+Anderen zu ermöglichen. Für jedes dieser Module existiert auch eine
+offline-Version, die ausschließlich auf lokale Daten zugreift.
+
+Verzeichnisstruktur
+-------------------
+
+Im Wurzelverzeichnis liegen die für den Build Prozess notwendigen
+Konfigurationsdateien. Das Verzeichnis `doc/` enthält detailliertere
+Dokumentationen zu einzelnen Aspekten des Projekts. Alle für die WebApp
+benötigten Dateien werden in `public/` erstellt bzw. dorthin kopiert. Im
+`src/` Verzeichnis befinden sich alle Quelldateien, Bilder und Fonts.
+Der `tasks/` Ordner enthält Javascript-Dateien, die die Teilschritte des
+Build-Prozesses definieren.
+
+Die grobe Struktur sieht folgendermaßen aus:
+
+
+    ├── .editorconfig        // Konfiguration für Texteditoren
+    ├── .babelrc             // Konfiguration von babel
+    ├── .eslintrc.yml        // Konfiguration des Javascript Linters
+    ├── .htmlhintrc          // Konfiguration des HTML Linters
+    ├── .sass-lint.yml       // Konfiguration des Sass Linters
+    ├── config.js            // Konfiguration des Build-Systems
+    ├── gulpfile.babel.js    // gulp Datei, verwendet Definitionen unter `tasks/`
+    ├── package.json         // npm Abhängigkeiten, Shortcuts für gulp tasks
+    ├── doc/                 // Dokumentation in markdown
+    ├── public/              // Zielverzeichnis für den Build-Prozess
+    ├── src/                 // Quellverzeichnis
+    │   ├── fonts            // Font Dateien
+    │   ├── html             // HTML 'Templates'
+    │   ├── img              // Bilder und Sprites
+    │   ├── js               // Javascript Quelldateien
+    │   └── scss             // Sass stylesheets
+    └── tasks/               // Definitionen für den gulp Build-Prozess
+
+Modul 4: Stichproben verstehen
+------------------------------
+
+Ziel dieses Moduls ist die Verbesserung des Verständnisses darüber, wie
+Stichproben entstehen.\
+Dafür werden Methoden des erfahrungsbasierten Lernens eingesetzt, die in
+in einer linear erzählten Geschichte eingebettet sind.
+
+### Javascript Verzeichnisstruktur
+
+Für einen schnellen Überblick sind hier die Quell-Dateien mit kurzen
+Beschreibungen aufgelistet:
+
+    ├── main.jsx                      // Einstiegspunkt für App
+    ├── config.js                     // Globale Konfiguration der WebApp
+    ├── components                    // (p)react Komponenten
+    │   ├── Index.jsx                 // Web App Haupt-Komponente
+    │   ├── FinalScreen.jsx           // Abschließende Ansicht
+    │   ├── PollScreen.jsx            // View für die interaktive Visualisierung
+    │   ├── TitleScreen.jsx           // Einleitende Ansicht
+    │   └── partials
+    │       ├── AnimItem.jsx          // d3 Visualisierungen
+    │       ├── HeaderLightItem.jsx   // Kopfzeile
+    │       ├── LegendItem.js         // Legende der Graphen
+    │       ├── QuestionItem.jsx      // Multiple Choice Fragen
+    │       └── TextItem.jsx          // Begleitende Texte
+    ├── content
+    │   ├── data.json                 // Geordnete Liste von Klassendefinitionen, aus denen Stichproben gezogen werden
+    │   ├── module.json               // Allgemeine Texte, Texte und Daten der Abschnitte
+    │   └── questions.json            // Definition der Mutiple Choice Fragen
+    ├── d3
+    │   ├── init-barcharts.js
+    │   ├── init-circles.js
+    │   ├── init-countertext.js
+    │   ├── init-pollresults.js
+    │   ├── poll-color.js
+    │   ├── poll-count.js
+    │   ├── poll-dopoll-falling.js
+    │   ├── poll-dopoll-update.js
+    │   ├── poll-dopoll.js
+    │   ├── poll-generator.js
+    │   ├── poll-moveup.js
+    │   ├── poll-showdiff.js
+    │   ├── poll-sort.js
+    │   ├── poll-sortbars.js
+    │   └── poll.js                   // d3 Einstiegsmodul
+    └── utilities
+        ├── enableTouch.js
+        ├── fonts.js
+        ├── formatter.js
+        └── math.js
+
+### Wie ändere ich Inhalte und Daten?
+
+#### Labels
+
+##### Umgebende Texte
+
+In `module.json` sind Texte und Labels definiert, auch solche, die nicht
+direkt Teil WebApp sind. Dies umfasst den Titel (`"title"`) der WebApp,
+den einleitenden Text (`"introtext"`) und das Label des Start-Buttons:
+
+    "title": "Glaube keiner Statistik…",
+    "introtext": "..deren Zustandekommen du nicht verstanden hast. …",
+    "start": "Start",
+    "sections": [
+      {
+        "headline": "Amerikanische Forscher haben gezeigt...",
+        "text": "Bargeldabschaffung als Beispiel um wissenschaftliche Studien zu erklären. …",
+        "swap": false,
+        "question": null,
+        "next": [ "Weiter" ]
+      }, …
+
+##### Multiple Choice Fragen
+
+Darüberhinaus ist die Abfolge der einzelnen Abschnitte definiert. Diese
+verfügen über Überschrift, erklärenden Text, und gegebenenfalls die ID
+einer multiple Choice Frage. Diese sind in `questions.json` definiert
+und sehen folgendermaßen aus:
+
+    {
+      "intro": "Eine Zwischenfrage",
+      "title": "Worauf ist denn zu achten, wenn man so eine kleine Gruppe befragt?",
+      "answers": [
+        {
+          "id": 1,
+          "antwort": "Vorab festzulegen, wen das Abbild repräsentieren soll",
+          "korrekt": true,
+          "feedback": "Sie legen unbedingt vorab fest, …"
+        }, …
+      ]
+    }
+
+Hier werden Einleitungstext, Titel und Antworten der Fragen festgelegt.
+Jede Frage verfügt über mehrere Antwortoptionen. Diese bestehen aus
+einem initialen Text (`"antwort"`), die Information, ob diese Option
+richtig ist (`"korrekt"`) und einen (optionalen) zusätzlichen Text
+(`"feedback"`), der eingeblendet wird, wenn die Option fälschlicherweise
+ausgewählt bzw. nicht ausgewählt wurde.
+
+#### Daten
+
+Die Daten, auf der dieses Modul basiert, beschreiben Ereignisse und
+deren Eintrittswahrscheinlichkeiten:
+
+    [
+      { "label": "klar dafür", "score": 4.1, "cumul": 4.1, "color": "#4a90e2" },
+      { "label": "eher dafür", "score": 7.6, "cumul": 11.7, "color": "#90CFEB" },
+      { "label": "unentschieden", "score": 1, "cumul": 12.7, "color": "#969696" },
+      { "label": "eher dagegen", "score": 3.3, "cumul": 16, "color": "#FD8C33" },
+      { "label": "klar dagegen", "score": 84, "cumul": 100, "color": "#FC4C2D" }
+    ]
+
+Der beschreibende Text des Ereignisses wird unter `"label"` definiert.
+`"score"` bezeichnet die Wahrcheinlichkeit in Prozent. Mit `"cumul"`
+wird die kumulierte Wahrscheinlichkeit angegeben und unter `"color"`
+wird die Farbe festgelegt. Erignisse sind aufsteigend nach ihrer
+Eintrittswahrscheinlichkeit sortiert. Alle Stichproben werden auf
+Grundlage dieser Klassendefinitionen gezogen. Wichtig ist, dass es sich
+hierbei um disjunkte Ereignisse handelt und sich alle
+Wahrscheinlichkeiten zu 1 (100 %) aufsummieren müssen.
+
+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.

TEMPAT SAMPAH
readme.pdf


TEMPAT SAMPAH
src/fonts/roboto-light.woff


TEMPAT SAMPAH
src/fonts/roboto-light.woff2


TEMPAT SAMPAH
src/fonts/roboto-medium.woff


TEMPAT SAMPAH
src/fonts/roboto-medium.woff2


TEMPAT SAMPAH
src/fonts/roboto-regular.woff


TEMPAT SAMPAH
src/fonts/roboto-regular.woff2


TEMPAT SAMPAH
src/fonts/roboto-thin.woff


TEMPAT SAMPAH
src/fonts/roboto-thin.woff2


TEMPAT SAMPAH
src/fonts/robotomono-light.woff


TEMPAT SAMPAH
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>

TEMPAT SAMPAH
src/img/android-chrome-192x192.png


TEMPAT SAMPAH
src/img/android-chrome-512x512.png


TEMPAT SAMPAH
src/img/apple-touch-icon.png


TEMPAT SAMPAH
src/img/favicon-16x16.png


TEMPAT SAMPAH
src/img/favicon-32x32.png


TEMPAT SAMPAH
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>

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

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

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

@@ -0,0 +1,126 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+// content and config:
+import content from '../content/module.json';
+import data from '../content/data.json';
+import config from '../config';
+// services and helper:
+import { fixedDigits } from '../utilities/formatter';
+// screens and items:
+import TitleScreen from './TitleScreen.jsx';
+import PollScreen from './PollScreen.jsx';
+
+/**
+ * titlescreen -> pollscreen (*n)
+ */
+export default class App extends Component {
+
+  // construct and initialize functions
+  constructor (props) {
+    super(props);
+
+    this.state = {
+      route: 'titlescreen',
+      iteration: 0,
+      currentAnswerIndex: null
+    };
+
+    if (window.location.hash) this.state.route = window.location.hash.replace('#', '');
+
+    // context binding
+    this.navigate = this.navigate.bind(this);
+    this.reset = this.reset.bind(this);
+    this.toNext = this.toNext.bind(this);
+    this.setAnswer = this.setAnswer.bind(this);
+    this.jumpTo = this.jumpTo.bind(this);
+  }
+
+  // set navigation state
+  navigate (route) {
+    this.setState({ route });
+  }
+
+  // reset everything to restart the module
+  reset () {
+    this.setState({
+      route: 'titlescreen',
+      currentAnswerIndex: null
+    });
+  }
+
+  // shortcut: jump to specific screen using `#question_2`
+  jumpTo () {
+    if (window.location.hash) {
+      const hashInfo = window.location.hash.replace('#', '').split('_');
+
+      if (hashInfo[1] === '1') {
+        this.setState({ iteration: 8, continue: false, question: 0, route: 'pollscreen' });
+        this.toNext();
+      } else if (hashInfo[1] === '2') {
+        this.setState({ iteration: 9, continue: false, question: 1, route: 'pollscreen' });
+        this.toNext();
+      }
+    }
+  }
+
+  // to next question, if finished trigger restart
+  toNext () {
+    if (this.state.iteration < config.numberQuestions - 1) {
+      this.setState({
+        iteration: this.state.iteration + 1,
+        currentAnswerIndex: null
+      });
+    } else {
+      location.reload();
+    }
+  }
+
+  // set and check answer
+  setAnswer (checked) {
+    if (this.state.currentAnswerIndex === null) {
+      this.setState({ currentAnswerIndex: checked });
+    }
+  }
+
+  // LIFECYLCE
+  componentWillMount () {
+    this.jumpTo();
+  }
+
+  // RENDER
+  render () {
+    let outputContent;
+
+    // get header content
+    const total = fixedDigits(config.numberQuestions, 2);
+    const index = this.state.iteration + 1; // 0-indexed
+    const headerState = `${fixedDigits(index, 2)}/${total}`;
+
+    // get header and intro content
+    const header = { title: content.title, number: content.number, headerState };
+    const introContent = { number: content.number, title: content.title, introtext: content.introtext, start: content.start };
+
+    switch (this.state.route) {
+      case 'pollscreen':
+        outputContent = <PollScreen
+          {...header }
+          {...content.sections[this.state.iteration]}
+          data={data}
+          toNext={this.toNext}
+          iteration={this.state.iteration}
+          currentAnswerIndex={this.state.currentAnswerIndex}
+          setAnswer={this.setAnswer} />;
+        break;
+
+      case 'titlescreen':
+      default:
+        outputContent = <TitleScreen
+          {...introContent}
+          navigateTo='pollscreen'
+          navigate={this.navigate} />;
+        break;
+    }
+
+    return outputContent;
+  }
+
+}

+ 147 - 0
src/js/components/PollScreen.jsx

@@ -0,0 +1,147 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import HeaderLightItem from './partials/HeaderLightItem.jsx';
+import TextItem from './partials/TextItem.jsx';
+import AnimItem from './partials/AnimItem.jsx';
+
+// module04: Poll Screen
+// wrapper which contains text and animation (side-by-side)
+export default class PollScreen extends Component {
+
+  // construct properties and initialize functions
+  constructor (props) {
+    super(props);
+
+    this.state = {
+      counter: 0,
+      point: 1,
+      pollindex: 0,
+      fillup: false,
+      scores: [ 0, 0, 0, 0, 0 ],
+      diffperc: [ 0, 0, 0, 0, 0 ],
+      shouldUpdate: false,
+      continue: false
+    };
+
+    this.btns = [];
+
+    // context binding
+    this.setData = this.setData.bind(this);
+    this.doPoll = this.doPoll.bind(this);
+    this.handleNext = this.handleNext.bind(this);
+  }
+
+  // set data is triggered from d3js module
+  setData (data) {
+    if (data.continue) this.setState({ continue: true });
+
+    // all polls completed
+    if (this.props.iteration === 14) {
+      this.props.toNext();
+    } else {
+      // iteration equals 7, 10 or 11
+      this.setState(Object.assign(data, { shouldUpdate: false, fillup: false }));
+
+      if (data.counter === 10 || data.counter === 100 || data.counter === 1000) {
+        this.props.toNext();
+      }
+    }
+  }
+
+  // execute poll
+  doPoll (point = 1, fillup = false) {
+    this.setState({ shouldUpdate: true, point, fillup });
+  }
+
+  // handle next iteration
+  handleNext (point = 1) {
+    if (this.state.continue) {
+      const map = { q7: 10, q10: 100, q11: 1000 };
+      const isQ0 = this.props.iteration === 0;
+      const isQ4 = this.props.iteration === 4;
+      const isQ13 = this.props.iteration === 13;
+      const isQ14 = this.props.iteration === 14;
+      const condQ7 = this.props.iteration === 7 && this.state.counter < map.q7;
+      const condQ10 = this.props.iteration === 10 && this.state.counter < map.q10;
+      const condQ11 = this.props.iteration === 11 && this.state.counter < map.q11;
+
+      const condHideBtn = !isQ0 && !isQ4 && !isQ13 && !isQ14;
+
+      if (condHideBtn) this.setState({ continue: false });
+
+      // do poll
+      if (condQ7 || condQ10 || condQ11) {
+        this.doPoll(point, point === map[`q${this.props.iteration}`]);
+      } else if (isQ14) {
+        // generate poll
+        this.setState({ pollindex: this.state.pollindex + 1, continue: this.state.pollindex < 3 });
+      } else {
+        // go to next screen
+        this.props.toNext();
+      }
+    }
+  }
+
+  // LIFECYCLE methods
+
+  // handle continue button visibility
+  componentDidUpdate () {
+    // handle question screen
+    const isQuestion = (this.props.iteration === 9 || this.props.iteration === 13) && this.props.currentAnswerIndex === null;
+
+    if (this.btns.length && !isQuestion) {
+      this.btns.forEach(btn => {
+        if (btn) {
+          if (this.state.continue) {
+            btn.classList.add('isvisible');
+          } else {
+            btn.classList.remove('isvisible');
+          }
+        }
+      });
+    }
+    if (!isQuestion) {
+      const contentNode = document.querySelector('.fakenews__content');
+      if (this.state.continue) {
+        contentNode.classList.add('isvisible');
+      } else {
+        contentNode.classList.remove('isvisible');
+      }
+    }
+  }
+
+  // RENDER
+  render () {
+    const numberOfBtns = this.props.next.length;
+    const multipleBtn = numberOfBtns > 1 && this.state.counter >= 1;
+    const pointMap = [ 1, 10, 100, 1000 ];
+    const classMap = [
+      multipleBtn ? '  button--wide--first' : '', // first button
+      numberOfBtns === 2 ? '  button--wide--last' : '  button--wide--between', // second button
+      numberOfBtns >= 3 ? '  button--wide--last' : '  button--wide--between', // third button
+      '  button--wide--last' // last btn
+    ];
+
+    return (
+      <section className="wrapper">
+        <HeaderLightItem { ...this.props } />
+        <main className="wrapper__main  wrapper--centered">
+
+          <div class="wrapper__fakenews">
+            <TextItem {...this.props} setData={this.setData} />
+            <AnimItem {...this.props} {...this.state} setData={this.setData} />
+          </div>
+
+        </main>
+        <footer className="footer footer--flex">
+          {this.props.next.map((btn, i) => (
+            <a href="#" title={btn} className={`button--wide  button--wide--cond  ${classMap[i]}`} key={`key-${i}`}
+              onClick={() => this.handleNext(pointMap[i]) } ref={elem => (this.btns[i] = elem)}>
+              {btn}
+            </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;

+ 160 - 0
src/js/components/partials/AnimItem.jsx

@@ -0,0 +1,160 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import * as d3 from 'd3';
+import d3Poll from '../../d3/poll';
+import d3PollColor from '../../d3/poll-color';
+import d3PollSort from '../../d3/poll-sort';
+import d3PollCount from '../../d3/poll-count';
+import d3PollMoveup from '../../d3/poll-moveup';
+import d3PollDoPoll from '../../d3/poll-dopoll';
+import d3PollShowDiff from '../../d3/poll-showdiff';
+import d3PollGenerator from '../../d3/poll-generator';
+import d3PollSortbars from '../../d3/poll-sortbars';
+
+// module04: Anim Item
+// animation area (side-by-side)
+export default class AnimItem extends Component {
+
+  // construct properties
+  constructor (props) {
+    super(props);
+
+    // d3js module defaults
+    this.state = {
+      data: this.props.data,
+      width: 475,
+      height: 500,
+      radius: 8,
+      baseColor: '#e3e3e3',
+      barHeight: 15,
+      xoffset: 100,
+      yoffset: 120, // 40
+      yoffsetSecRow: 175,
+      numberOfCols: 3,
+      setData: this.props.setData
+    };
+  }
+
+  // get poll properties
+  doPoll (props) {
+    return {
+      point: props.point,
+      fillup: props.fillup,
+      counter: props.counter,
+      scores: props.scores,
+      diffperc: props.diffperc,
+      setData: props.setData
+    };
+  }
+
+  // LIFECYCLE methods
+  componentDidMount () {
+    // gets executet initially at start
+    const options = { ...this.state };
+    const poll = d3Poll().options(options);
+
+    d3.select(this.container).call(poll);
+  }
+
+  shouldComponentUpdate (nextProps) {
+    const shouldUpdate = nextProps.shouldUpdate;
+    const shouldContinue = nextProps.continue;
+    const isGtQ1 = nextProps.iteration > 1;
+    const isQ5 = nextProps.iteration === 5;
+    const isQ7 = nextProps.iteration === 7;
+    const isQ10 = nextProps.iteration === 10;
+    const isQ11 = nextProps.iteration === 11;
+    const isQ14 = nextProps.iteration === 14;
+    const isNotSelected = !isQ7 && !isQ10 && !isQ11;
+
+    // iteration equals 7
+    const isInitQ7 = this.props.iteration === 6;
+    const isLastQ7 = nextProps.counter === 10;
+    const condQ7 = isNotSelected || (isQ7 && isInitQ7) || (isQ7 && isLastQ7) || (isQ7 && shouldUpdate);
+
+    // iteration equals 10
+    const isInitQ10 = this.props.iteration === 9;
+    const isLastQ10 = nextProps.counter === 100;
+    const condQ10 = isNotSelected || (isQ10 && isInitQ10) || (isQ10 && isLastQ10) || (isQ10 && shouldUpdate);
+
+    // iteration equals 11
+    const isInitQ11 = this.props.iteration === 10;
+    const isLastQ11 = nextProps.counter === 1000;
+    const condQ11 = isNotSelected || (isQ11 && isInitQ11) || (isQ11 && isLastQ11) || (isQ11 && shouldUpdate);
+
+    // iteration equals 14
+    const condQ14 = shouldContinue && isQ14 && nextProps.pollindex < 5;
+
+    return (!shouldContinue || condQ14) && (isGtQ1 && !isQ5) && (condQ7 || condQ10 || condQ11);
+  }
+
+  componentWillUpdate (nextProps) {
+    const options = { ...this.state };
+    let poll;
+    let doUpdate = true;
+
+    switch (nextProps.iteration) {
+      case 2:
+        poll = d3PollColor().options(options);
+        break;
+
+      case 3:
+        poll = d3PollSort().options(options);
+        break;
+
+      case 4:
+      case 5:
+        poll = d3PollCount().options(options);
+        break;
+
+      case 6:
+        poll = d3PollMoveup().options(options);
+        break;
+
+      case 7:
+        poll = d3PollDoPoll().options(Object.assign(options, this.doPoll(nextProps)));
+        if (nextProps.counter === 10) doUpdate = false;
+        break;
+
+      case 10:
+        if (this.props.iteration === 9) {
+          poll = d3PollShowDiff().options(options);
+        } else {
+          poll = d3PollDoPoll().options(Object.assign(options, this.doPoll(nextProps)));
+          if (nextProps.counter === 100) doUpdate = false;
+        }
+        break;
+
+      case 11:
+        if (this.props.iteration === 10) {
+          doUpdate = false;
+        } else {
+          poll = d3PollDoPoll().options(Object.assign(options, this.doPoll(nextProps)));
+          if (nextProps.counter === 1000) doUpdate = false;
+        }
+        break;
+
+      case 14:
+        options.pollindex = nextProps.pollindex;
+        poll = d3PollGenerator().options(options);
+        break;
+
+      case 16:
+        poll = d3PollSortbars().options(options);
+        break;
+
+      default:
+        doUpdate = false;
+        break;
+    }
+
+    if (doUpdate) d3.select(this.container).call(poll);
+  }
+
+  // output entry point for d3js module
+  render () {
+    return (
+      <section className="fakenews" ref={ elem => (this.container = elem) }></section>
+    );
+  }
+
+}

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

+ 17 - 0
src/js/components/partials/LegendItem.jsx

@@ -0,0 +1,17 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+
+// module04: Legend Item
+// output legend for colors
+const LegendItem = props => ( // eslint-disable-line no-unused-vars
+  <div className="legend">
+    { props.data.map(item => (
+      <div className="legend-item">
+        <span className="circlelized" style={`background: ${item.color}`}></span>
+        {item.label}
+      </div>
+    )) }
+    <div className="legend-item  legend-item--unit">(in Prozent)</div>
+  </div>
+);
+
+export default LegendItem;

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

@@ -0,0 +1,86 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import questions from '../../content/questions.json';
+
+// Question view
+export default class QuestionItem extends Component {
+
+  // Construct and initialize functions
+  constructor (props) {
+    super(props);
+
+    this.state = { checked: [] };
+
+    // Context binding
+    this.transferAnswer = this.transferAnswer.bind(this);
+    this.toggleCheckbox = this.toggleCheckbox.bind(this);
+  }
+
+  // Toggle checkbox state: checked vs. unchecked
+  toggleCheckbox (index, answered) {
+    if (!answered) {
+      const checked = this.state.checked;
+      const i = checked.indexOf(index);
+
+      if (i !== -1) {
+        checked.splice(i, 1);
+      } else {
+        checked.push(index);
+      }
+
+      this.setState({ checked });
+    }
+  }
+
+  // Bind answer
+  transferAnswer (event) {
+    event.preventDefault();
+
+    this.props.setAnswer(this.state.checked);
+    this.props.setData({ continue: true });
+  }
+
+  // RENDER question
+  render () {
+    const question = questions[this.props.question];
+    const answered = this.props.currentAnswerIndex !== null;
+    const feedback = [];
+
+    return (
+      <div className="question  question__fakenews">
+        <h3>{question.intro}</h3>
+        <p>{question.title}</p>
+        <form onSubmit={this.transferAnswer}>
+          {question.answers.map((answer, index) => {
+            const checked = this.state.checked.indexOf(index) !== -1;
+            const markerClass = answered && checked ? 'question__options__item--marked' : '';
+            const correctClass = answered && answer.korrekt ? 'question__options__item--correct' : '';
+            const checkedClass = !answered && checked ? 'question__options__item--selected' : '';
+
+            return (
+              <div className={`question__options__item ${markerClass} ${correctClass} ${checkedClass}`}>
+                <input class="checkbox-input"
+                  name="option[]" id={`option-${index}`} value="1" type="checkbox"
+                  onChange={() => this.toggleCheckbox(index, answered)} checked={checked} />
+                <label class="checkbox-label" for={`option-${index}`}>
+                  {answer.antwort}
+                </label>
+                { answered && ((!checked && answer.korrekt) || (checked && !answer.korrekt))
+                  ? <p className="checkbox-feedback">{question.answers[index].feedback}</p>
+                  : [] }
+              </div>);
+          }
+          ) }
+
+          { !answered
+            ? <button class="button" type="submit">Antworten</button>
+            : []
+          }
+        </form>
+        { answered && feedback.length > 0
+          ? feedback.map(i => <p className="iscorrect">{question.answers[i].feedback}</p>)
+          : [] }
+      </div>
+    );
+  }
+
+}

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

@@ -0,0 +1,31 @@
+import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
+import QuestionItem from './QuestionItem.jsx';
+import LegendItem from './LegendItem.jsx';
+
+// module04: Text Item
+// renders current question
+export default class TextItem extends Component {
+
+  // RENDER text and question where needed
+  render () {
+    let content = '';
+    const hasQuestion = this.props.question !== null;
+    const hasAnswered = this.props.currentAnswerIndex !== null;
+
+    if (!this.props.swap) {
+      content = <div className="fakenews__content"><h3>{this.props.headline}</h3><p>{this.props.text}</p></div>;
+    } else {
+      content = <div className="fakenews__content"><p>{this.props.text}</p><h3>{this.props.headline}</h3></div>;
+    }
+
+    return (
+      <section className="wrapper--col">
+        { hasQuestion ? <QuestionItem {...this.props} /> : [] }
+        { hasQuestion && hasAnswered ? content : [] }
+        { !hasQuestion ? content : [] }
+        { this.props.iteration >= 2 ? <LegendItem data={this.props.data} /> : [] }
+      </section>
+    );
+  }
+
+}

+ 31 - 0
src/js/config.js

@@ -0,0 +1,31 @@
+// basic config containing global api base URL
+export default {
+  numberQuestions: 17,
+  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'
+    }
+  }
+};

+ 7 - 0
src/js/content/data.json

@@ -0,0 +1,7 @@
+[
+  { "label": "klar dafür", "score": 4.1, "cumul": 4.1, "color": "#4a90e2" },
+  { "label": "eher dafür", "score": 7.6, "cumul": 11.7, "color": "#90CFEB" },
+  { "label": "unentschieden", "score": 1, "cumul": 12.7, "color": "#969696" },
+  { "label": "eher dagegen", "score": 3.3, "cumul": 16, "color": "#FD8C33" },
+  { "label": "klar dagegen", "score": 84, "cumul": 100, "color": "#FC4C2D" }
+]

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

@@ -0,0 +1,140 @@
+{
+  "number": "04",
+  "title": "Glaube keiner Statistik…",
+  "introtext": "..deren Zustandekommen du nicht verstanden hast.\nDas ist besonders wichtig, wenn Statistiken einander zu widersprechen scheinen. Wichtig ist: Studienergebnisse dürfen das. Sie müssen sogar voneinander abweichen. Immer, wenn Sie bestimmte Teile der Bevölkerung für eine Studie befragen, hängt es stark davon ab, wen Sie fragen. So wie beim Bargeld. Wollen Sie auf Bargeld verzichten? Fragen Sie mal! Los geht’s!",
+  "start": "Start",
+  "sections": [
+    {
+      "headline": "Amerikanische Forscher haben gezeigt...",
+      "text": "Bargeldabschaffung als Beispiel um wissenschaftliche Studien zu erklären. Warum man vieles richtig, aber auch vieles falsch machen kann, wenn man ein paar Leute befragt und daraus Schlüsse zieht.",
+      "swap": false,
+      "question": null,
+      "next": [ "Weiter" ]
+    },
+    {
+      "headline": "Wollen die Leute in Deutschland kein Bargeld mehr, oder doch?",
+      "text": "Gehen Sie davon aus, dass die meisten Menschen in Deutschland zu der Frage eine Meinung haben. In unserem Beispiel sind die Leute entweder klar dafür, eher dafür, unentschieden, eher dagegen oder klar dagegen. Stellen wir uns im folgenden immer vor, wir wüssten wie alle 70 Millionen Menschen in Deutschland (ohne die Kinder bis 14 Jahren) tatsächlich denken!",
+      "swap": false,
+      "question": null,
+      "next": [ "Stellen wir es uns vor" ]
+    },
+    {
+      "headline": "Doch wie viele sind nun dafür oder dagegen usw...? Könnten wir das zählen?",
+      "text": "Jetzt sehen wir anhand ihrer Farben, wie die einzelnen Menschen in Deutschland entweder klar für, eher für, unentschieden, eher gegen oder klar gegen die Bargeldabschaffung sind. Ein abstraktes Gemälde von Einstellungen.",
+      "swap": true,
+      "question": null,
+      "next": [ "Vielleicht sortieren wir erstmal" ]
+    },
+    {
+      "headline": "",
+      "text": "Das sieht schon viel übersichtlicher aus.",
+      "swap": false,
+      "question": null,
+      "next": [ "Zählen wir" ]
+    },
+    {
+      "headline": "",
+      "text": "Die Meinungen der Menschen in Deutschland zur Bargeldabschaffung sind hier recht klar verteilt. Die meisten sind deutlich dagegen, wenn es auch eine nicht zu unterschätzende Minderheit gibt. Normalerweise können wir nicht wissen, wie die Menschen tatsächlich denken. Deshalb haben wir hier die Realität vorgegeben. Nur dasselbe mit echten Studien zu belegen ist etwas anderes. So können wir aus praktischen Gründen nicht alle 70 Millionen Menschen befragen.",
+      "swap": false,
+      "question": null,
+      "next": [ "Wie belegen wir es?" ]
+    },
+    {
+      "headline": "Studien",
+      "text": "Man bräuchte ein Abbild der Meinungen der Menschen in Deutschland ohne alle zu fragen. Also fragen wir einfach weniger.",
+      "swap": false,
+      "question": null,
+      "next": [ "Wen fragen wir?" ]
+    },
+    {
+      "headline": "Was halten Sie von Bargeldabschaffung in Deutschland?",
+      "text": "Sie sehen jetzt alle 70 Millionen Menschen, die wir fragen könnten, fangen wir doch erstmal mit einem oder einer Bürgerin an. Man könnte sich also jemand beliebigen schnappen und fragen:",
+      "swap": true,
+      "question": null,
+      "next": [ "Wen fragen wir?" ]
+    },
+    {
+      "headline": "Was halten Sie von Bargeldabschaffung in Deutschland?",
+      "text": "Sie sehen jetzt alle 70 Millionen Menschen, die wir fragen könnten, fangen wir doch erstmal mit einem oder einer Bürgerin an. Man könnte sich also jemand beliebigen schnappen und fragen:",
+      "swap": true,
+      "question": null,
+      "next": [
+        "1 Person fragen",
+        "Für Ungeduldige"
+      ]
+    },
+    {
+      "headline": "Haben wir die Falschen gefragt?",
+      "text": "Es ist harte Arbeit, für eine Studie Leute zu befragen. Sie haben nun eine Vorstudie mit 10 Menschen abgeschlossen und es sieht so aus, als ob die gefundenen Einstellungen von unseren Erwartungen (weil wir in unserem Beispiel wissen, wie die Menschen denken) abweichen.",
+      "swap": true,
+      "question": null,
+      "next": [ "Eine Zwischenfrage" ]
+    },
+    {
+      "headline": "",
+      "text": "Wie sehen nun die Abweichungen zwischen den Befragten und der Realität in unserem Beispiel aus?",
+      "swap": false,
+      "question": 0,
+      "next": [ "Zu unseren Abweichungen" ]
+    },
+    {
+      "headline": "Sollten wir einfach mehr befragen?",
+      "text": "Das sind doch echt enorme Abweichungen in der Verteilung der Antworten zur Bargeldabschaffung. 10 Leute zu fragen bildet offenbar nicht passend die gesamte Bevölkerung ab. Es stehen Ihnen mehrere Möglichkeiten zur Verfügung!",
+      "swap": true,
+      "question": null,
+      "next": [
+        "1 Person",
+        "10 Personen",
+        "Für Ungeduldige"
+      ]
+    },
+    {
+      "headline": "Werden die Abweichungen noch kleiner, wenn wir noch mehr Menschen befragen?",
+      "text": "Jetzt haben Sie 100 Leute befragt. Und immer noch weicht die Befragung ab. Aber die Abweichungen sind kleiner geworden.",
+      "swap": true,
+      "question": null,
+      "next": [
+        "1 Person",
+        "10 Personen",
+        "100 Personen",
+        "Für Ungeduldige"
+      ]
+    },
+    {
+      "headline": "Gratulation",
+      "text": "Das waren jetzt 1.000 Befragte. Das entspricht üblichen repräsentativen Studien (man spricht auch von einer Stichprobengröße von 1.000).",
+      "swap": false,
+      "question": null,
+      "next": [ "Weiter" ]
+    },
+    {
+      "headline": "",
+      "text": "Studien sind halt nur Studien - Abweichungen bleiben - vor allem auch, wenn man das ganze mehrmals macht. Keine Studie gleicht der anderen",
+      "swap": false,
+      "question": null,
+      "question": 1,
+      "next": [ "Weitere Studie" ]
+    },
+    {
+      "headline": "Und noch eine Studie!",
+      "text": "Jede Studie liefert andere Ergebnisse zur Bargeldabschaffung. Zahlen der Befürworter und Gegner schwanken.",
+      "swap": false,
+      "question": null,
+      "next": [ "Weitere Studie" ]
+    },
+    {
+      "headline": "Und noch eine Studie!",
+      "text": "Jede Studie liefert andere Ergebnisse zur Bargeldabschaffung. Zahlen der Befürworter und Gegner schwanken.",
+      "swap": false,
+      "question": null,
+      "next": [ "Wie sieht das für jede der einzelnen Gruppen aus?" ]
+    },
+    {
+      "headline": "Sie wissen nun, dass Forschung viel richtig, aber auch viel falsch machen kann. Und Unsicherheit bleibt bei jeder Studie bestehen.",
+      "text": "Jede Studie liefert andere Ergebnisse zur Bargeldabschaffung. Zahlen der Befürworter und Gegner schwanken.",
+      "swap": false,
+      "question": null,
+      "next": [ "Wiederholen" ]
+    }
+  ]
+}

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

@@ -0,0 +1,50 @@
+[
+  {
+    "intro": "Eine Zwischenfrage",
+    "title": "Worauf ist denn zu achten, wenn man so eine kleine Gruppe befragt?",
+    "answers": [
+      {
+        "id": 1,
+        "antwort": "Vorab festzulegen, wen das Abbild repräsentieren soll",
+        "korrekt": true,
+        "feedback": "Sie legen unbedingt vorab fest, wen das Abbild repräsentieren soll, z.B. ob Sie eine Aussage nur für Frauen, für alle Hausbesitzer oder alle 70 Millionen Menschen in Deutschland, die älter als 14 Jahre sind, treffen wollen."
+      },
+      {
+        "id": 2,
+        "antwort": "Befragte zufällig auswählen",
+        "korrekt": true,
+        "feedback": "Sie befragen unbedingt zufällig ausgewählte Menschen, nicht jene, die sich bei Ihnen melden oder die Sie gerade kennen."
+      },
+      {
+        "id": 3,
+        "antwort": "Die repräsentative Abbildung von Schlüsselmerkmalen; zum Beispiel bestimmte Altersgruppen zu berücksichtigen",
+        "korrekt": true,
+        "feedback": "Sie berücksichtigen, dass Schlüsselmerkmale wie Alter, Geschlecht oder Bildung von den Anteilen her der Realität entsprechen, wenn Sie ein repräsentatives Abbild der Gesellschaft zeichnen möchten."
+      }
+    ]
+  },
+  {
+    "intro": "Noch eine Zwischenfrage",
+    "title": "Was bedeutet es, dass diese Abweichungen jetzt noch kleiner sind?",
+    "answers": [
+      {
+        "id": 1,
+        "antwort": "Kleinere Abweichungen sprechen für eine höhere Zuverlässigkeit dieses Befragungs-Ergebnisses",
+        "korrekt": true,
+        "feedback": ""
+      },
+      {
+        "id": 2,
+        "antwort": "Kleinere Abweichungen sagen nichts über die Zuverlässigkeit des Befragungs-Ergebnisses aus",
+        "korrekt": false,
+        "feedback": "Kleinere Abweichungen sprechen in der Tat für eine höhere Zuverlässigkeit dieses Befragungs-Ergebnisses. Der Grund ist der verringerte Schätzfehler mit zunehmender Größe der Befragung. Das bedeutet, dass nicht nur das Abbild vollständiger wird, sondern vor allem Abweichungen einzelner Befragter (z.B. Extremisten) kaum noch ins Gewicht fallen."
+      },
+      {
+        "id": 3,
+        "antwort": "Kleinere Abweichungen legen nahe, dass weitere (wissenschaftlich hochwertige) Studien zum gleichen Befragungsergebnis führen",
+        "korrekt": false,
+        "feedback": "Zwei Studien erbringen fast nie das gleiche Ergebnis. Dafür gibt es einen schlichten Grund, nämlich dass wir jedes Mal nur 1.000 Leute aus 70 Millionen in jeder Studie befragen (oder auch weniger). Sobald das aber 1.000 andere sind, sieht auch das Ergebnis etwas anderes aus."
+      }
+    ]
+  }
+]

+ 72 - 0
src/js/d3/init-barcharts.js

@@ -0,0 +1,72 @@
+export default (selection, options) => {
+  // difference bar chart
+  const resultGroup = selection.append('g').attr('class', 'poll-result');
+  const differenceGroup = resultGroup.append('g').attr('class', 'poll-result__differences');
+  const rasterGroup = resultGroup.append('g').attr('class', 'poll-result__raster');
+  const step = 18;
+  const yposRectDiff = i => (i * step + options.height / 2.1);
+  const fontsize = 16;
+  const distance = (options.width - options.xoffset * 2) / (options.numberOfCols - 1);
+
+  // single difference bar chart
+  // consisting of a rectanlge
+  differenceGroup.selectAll('rect.diff')
+    .data(options.data)
+    .enter()
+    .append('rect')
+    .attr('class', 'diff')
+    .attr('x', options.width / 2)
+    .attr('y', (d, i) => yposRectDiff(i))
+    .attr('height', options.barHeight)
+    .attr('width', 0)
+    .attr('fill-opacity', 0)
+    .attr('fill', d => d.color);
+
+  // ... and text
+  differenceGroup.selectAll('text.difftext')
+    .data(options.data)
+    .enter()
+    .append('text')
+    .attr('class', 'difftext')
+    .attr('text-anchor', 'middle')
+    .style('font-size', fontsize)
+    .attr('x', 0)
+    .attr('y', (d, i) => (yposRectDiff(i) + fontsize - 2))
+    .attr('fill', d => d.color)
+    .attr('fill-opacity', 0)
+    .text('');
+
+  // grid of difference bar charts
+  for (let x = 0; x < options.numberOfCols; x += 1) {
+    for (let y = 0; y < 2; y += 1) {
+      if (!(x === options.numberOfCols - 1 && y === 1)) {
+        const subgroup = rasterGroup.append('g').attr('class', 'poll-result__raster-item');
+
+        subgroup.selectAll('rect.diffgrid')
+          .data(options.data)
+          .enter()
+          .append('rect')
+          .attr('class', `gridbar diff${x}${y}`)
+          .attr('x', options.xoffset + x * distance)
+          .attr('y', (d, i) => (options.yoffset + i * step + options.yoffsetSecRow * y))
+          .attr('height', options.barHeight)
+          .attr('width', 0)
+          .attr('fill-opacity', 1)
+          .attr('fill', d => d.color);
+
+        subgroup.selectAll('text.diffgrid')
+          .data(options.data)
+          .enter()
+          .append('text')
+          .attr('class', `difftext${x}${y}`)
+          .attr('text-anchor', 'middle')
+          .style('font-size', 18)
+          .attr('x', 0)
+          .attr('y', (d, i) => (options.yoffset + i * step + options.yoffsetSecRow * y + fontsize - 2))
+          .attr('fill', d => d.color)
+          .attr('fill-opacity', 0)
+          .text('0');
+      }
+    }
+  }
+};

+ 42 - 0
src/js/d3/init-circles.js

@@ -0,0 +1,42 @@
+export default (selection, options) => {
+  const pointQuantityFactor = 5;
+  const group = selection.append('g').attr('class', 'circles');
+  const numberOfCircles = options.data.length;
+  let finished = 0;
+
+  // initialize function for the circles
+  const circlesInit = (record) => {
+    const myData = [];
+    const { number, color, cumul, score } = record;
+    const getCoordinate = refVal => (Math.max(options.radius, Math.min(refVal - options.radius, refVal * Math.random())));
+
+    for (let i = 0; i < number; i += 1) myData.push({ color, cumul, score });
+
+    // add circles
+    const circles = group.selectAll('circle.distribution')
+      .data(myData)
+      .enter()
+      .append('circle')
+      .attr('r', 0)
+      .attr('cx', d => (d.x = getCoordinate(options.width)))
+      .attr('cy', d => (d.y = getCoordinate(options.height)))
+      .attr('fill', options.baseColor)
+      .attr('fill-opacity', 1);
+
+    // add transition and display circles
+    // if finished, display continue button
+    circles.transition()
+      .delay((d, i) => (i * 10))
+      .duration(2000)
+      .attr('r', () => (options.radius))
+      .on('end', (d, i) => {
+        if (circles.size() === i + 1) finished += 1;
+        if (numberOfCircles === finished) options.setData({ continue: true });
+      });
+  };
+
+  // Call the initialize function for the circles
+  options.data.forEach(record => {
+    circlesInit(Object.assign(record, { number: record.score * pointQuantityFactor }));
+  });
+};

+ 12 - 0
src/js/d3/init-countertext.js

@@ -0,0 +1,12 @@
+export default (selection, options) => {
+  // counter text
+  selection.append('text')
+    .attr('x', options.width * 0.65)
+    .attr('y', options.height / 2.3)
+    .attr('class', 'counter')
+    .text('0')
+    .attr('fill', options.baseColor)
+    .attr('fill-opacity', 1)
+    .style('font-size', 64)
+    .style('visibility', 'hidden');
+};

+ 36 - 0
src/js/d3/init-pollresults.js

@@ -0,0 +1,36 @@
+// d3js module for module04: initialize elements for containing the poll results
+export default (selection, options) => {
+  const countsGroup = selection.append('g').attr('class', 'counts');
+  const group = countsGroup.append('g').attr('class', 'counts--second');
+  const yposRect = options.height * 0.8 - options.barHeight;
+  const yposText = {
+    above: yposRect - options.barHeight / 2,
+    below: yposRect + options.barHeight * 7 / 3
+  };
+
+  // add rectangle
+  group.selectAll('rect.poll')
+    .data(options.data)
+    .enter()
+    .append('rect')
+    .attr('class', 'poll')
+    .attr('x', 0)
+    .attr('y', yposRect)
+    .attr('height', options.barHeight)
+    .attr('width', 0)
+    .attr('fill', d => d.color);
+
+  // add text
+  group.selectAll('text.polltext')
+    .data(options.data)
+    .enter()
+    .append('text')
+    .attr('class', 'polltext')
+    .attr('text-anchor', 'middle')
+    .style('font-size', 16)
+    .attr('x', 0)
+    .attr('y', (d, i) => (i % 2 ? yposText.above : yposText.below))
+    .attr('fill', d => d.color)
+    .attr('fill-opacity', 0)
+    .text('');
+};

+ 27 - 0
src/js/d3/poll-color.js

@@ -0,0 +1,27 @@
+// d3js module for module04: fill circles randomly with specific color
+export default () => {
+  // defaults
+  const options = {
+    width: 475,
+    height: 500,
+    radius: 10
+  };
+
+  // start
+  const poll = selection => {
+    selection.selectAll('circle').transition()
+      .duration(4000)
+      .attr('fill', d => d.color)
+      .on('end', () => options.setData({ continue: true }));
+  };
+
+  // "setter"
+  poll.options = input => {
+    Object.assign(options, input);
+
+    return poll;
+  };
+
+  return poll;
+};
+

+ 85 - 0
src/js/d3/poll-count.js

@@ -0,0 +1,85 @@
+import * as d3 from 'd3';
+
+// d3js module for module04: add poll counter, display election results
+export default () => {
+  // defaults
+  const options = {
+    width: 475,
+    height: 500,
+    radius: 10,
+    data: []
+  };
+
+  // start
+  const poll = selection => {
+    const svg = selection.select('svg');
+
+    // move all circles to the top and minimize radius
+    svg.selectAll('circle').transition()
+      .duration(2000)
+      .delay((d, i) => (i * 2))
+      .attr('cy', options.height * 0.8)
+      .attr('r', 4);
+
+    const group = svg.select('.counts').append('g').attr('class', 'counts--first');
+    const xScale = d3.scaleLinear().domain([ 0, 100 ]).range([ 0, options.width ]);
+
+    // append hidden rectangle
+    const rects = group.selectAll('rect.election')
+      .data(options.data)
+      .enter()
+      .append('rect')
+      .attr('class', 'election')
+      .attr('x', d => xScale(d.cumul - d.score))
+      .attr('y', options.height * 0.8 - options.barHeight / 3)
+      .attr('height', options.barHeight)
+      .attr('width', d => xScale(d.score))
+      .attr('fill', d => d.color)
+      .attr('fill-opacity', 0);
+
+    // append hidden text
+    const results = group.selectAll('text.result')
+      .data(options.data.slice(0, 7))
+      .enter()
+      .append('text')
+      .attr('x', d => xScale(d.cumul - d.score / 2))
+      .attr('y', (d, i) => {
+        if (i % 2) return options.height * 0.8 - options.barHeight / 1.5; // above the bar
+        return options.height * 0.8 + options.barHeight * 2; // below the bar
+      })
+      .attr('fill', d => d.color)
+      .style('font-size', 16)
+      .attr('fill-opacity', 0)
+      .text(d => d.score.toLocaleString('de-DE'))
+      .attr('class', (d, i) => `count-text--${i}`)
+      .attr('class', 'result')
+      .attr('text-anchor', 'middle');
+
+    // fade-in rectangle
+    rects
+      .transition()
+      .delay((d, i) => (i * 500))
+      .duration(2000)
+      .attr('fill-opacity', 1);
+
+    // fade-in text
+    results
+      .transition()
+      .delay((d, i) => (i * 500))
+      .duration(1000)
+      .attr('fill-opacity', 0.8)
+      .on('end', (d, i) => {
+        // same size rects and results + same duration, it's enough to check only one 'ended'
+        if (results.size() === i + 1) options.setData({ continue: true });
+      });
+  };
+
+  // "setter"
+  poll.options = input => {
+    Object.assign(options, input);
+
+    return poll;
+  };
+
+  return poll;
+};

+ 56 - 0
src/js/d3/poll-dopoll-falling.js

@@ -0,0 +1,56 @@
+import * as d3 from 'd3';
+import update from './poll-dopoll-update.js';
+
+// d3js module for module04: display election results using falling colored circles
+// call it: `letItFall(svg, options, { randdata, rate });`
+export default (selection, options, { randdata, rate }) => {
+  const xScale = d3.scaleLinear().domain([ 0, 100 ]).range([ 0, options.width ]);
+
+  // adapt x position while falling down
+  const interpolXval = (circle, duration, delay) => {
+    circle.transition('interpolXval')
+      .ease(d3.easeSin)
+      .delay(delay)
+      .duration(duration)
+      .on('end', (d, i) => update(selection, options, { disindex: d.index, iindex: i, size: circle.size() }))
+      .attrTween('cx', (d, i, a) => {
+        const scaled = xScale(d.cumul - d.score * Math.random());
+        const ref = Math.max(options.radius, Math.min(options.width - options.radius, scaled));
+
+        return d3.interpolate(a[i].getAttribute('cx'), ref);
+      })
+      .remove();
+  };
+
+  // adapt y position while falling down
+  const interpolYval = (circle, duration, delay) => {
+    circle.transition('interpolYval')
+      .ease(d3.easeBackIn)
+      .delay(delay)
+      .duration(duration)
+      .attrTween('cy', (d, i, a) => d3.interpolate(a[i].getAttribute('cy'), options.height * 0.8 - 25));
+  };
+
+  // increase radius while falling down
+  const interpolRadius = (circle, duration, delay) => {
+    circle.transition('interpolRadius')
+      .delay(delay)
+      .duration(duration)
+      .attrTween('r', (d, i, a) => d3.interpolate(a[i].getAttribute('r'), options.radius));
+  };
+
+  // add ramdom circles falling down
+  selection.selectAll('circle.falling')
+    .data(randdata)
+    .enter()
+    .append('circle')
+    .attr('class', 'falling')
+    .attr('r', 0)
+    .attr('cx', () => (options.width * Math.random()))
+    .attr('cy', () => (options.height * 0.2 * Math.random()))
+    .attr('fill', d => d.color)
+    .attr('fill-opacity', 1)
+    .call(interpolYval, 1500, (d, i) => (i * rate))
+    .call(interpolRadius, 1500, (d, i) => (i * rate))
+    .call(interpolXval, 1500, (d, i) => (i * rate));
+};

+ 83 - 0
src/js/d3/poll-dopoll-update.js

@@ -0,0 +1,83 @@
+import * as d3 from 'd3';
+import { repr } from '../utilities/formatter';
+
+// d3js module for module04: update poll
+export default (selection, options, { disindex, iindex, size }) => {
+  const xScale = d3.scaleLinear().domain([ 0, 100 ]).range([ 0, options.width ]);
+  const scores = options.data.map(distribution => distribution.score);
+
+  // update counter
+  options.counter += 1;
+  selection.select('.counter').style('visibility', 'visible');
+  selection.select('.counter').text(options.counter);
+
+  // update poll scores
+  options.scores[disindex] += 1;
+
+  const scoresperc = options.scores.map(score => (Math.round(score / options.counter * 1000) / 10));
+
+  options.diffperc.forEach((element, index) => {
+    options.diffperc[index] = scoresperc[index] - scores[index];
+  });
+
+  // update diff barchart
+  selection.selectAll('rect.diff')
+    .data(options.diffperc)
+    .attr('x', d => {
+      if (d < 0) {
+        return options.width / 2 - Math.abs(xScale(d));
+      }
+      return options.width / 2;
+    })
+    .attr('width', d => Math.abs(xScale(d)));
+
+  // update difftext
+  selection.selectAll('.difftext')
+    .data(options.diffperc)
+    .attr('x', d => {
+      if (d > 0) {
+        return options.width / 2 + xScale(d) + 7; // after
+      }
+      return options.width / 2 + xScale(d) - 6; // before
+    })
+    .attr('text-anchor', d => (d > 0 ? 'start' : 'end'))
+    .text(d => repr(d));
+
+  // update poll barchart
+  selection.selectAll('rect.poll')
+    .data(scoresperc).transition().duration(200)
+    .attr('x', (d, i) => {
+      if (i > 0) {
+        return xScale(scoresperc.slice(0, i).reduce((previousValue, currentValue) => (previousValue + currentValue)));
+      }
+      return d.x;
+    })
+    .attr('width', d => xScale(d));
+
+  // update poll results
+  selection.selectAll('.polltext')
+    .data(scoresperc)
+    .transition()
+    .duration(200)
+    .attr('x', (d, i) => {
+      let val = xScale(d / 2);
+
+      if (i > 0) {
+        val = xScale(scoresperc.slice(0, i).reduce((previousValue, currentValue) => (previousValue + currentValue)) + d / 2);
+      }
+
+      return Math.max(10, val);
+    })
+    .text(d => d.toLocaleString('de-DE'))
+    .attr('fill-opacity', d => (d > 0 ? 0.8 : 0));
+
+  // set data in preact component
+  if (size === iindex + 1) {
+    options.setData({
+      counter: options.counter,
+      scores: options.scores,
+      diffperc: options.diffperc,
+      continue: true
+    });
+  }
+};

+ 70 - 0
src/js/d3/poll-dopoll.js

@@ -0,0 +1,70 @@
+import * as d3 from 'd3';
+import letItFall from './poll-dopoll-falling.js';
+
+// d3js module for module04: do poll
+export default () => {
+  // defaults
+  const options = {
+    width: 475,
+    height: 500,
+    radius: 10,
+    data: [],
+    counter: 0,
+    point: 1,
+    fillup: false,
+    scores: [],
+    diffperc: [],
+    setData: () => {} // eslint-disable-line no-empty-function
+  };
+
+  // start
+  const poll = selection => {
+    const svg = selection.select('svg');
+
+    // generate circles and let them fall down
+    const choice = (numbers, fillup) => {
+      const toadd = fillup === true ? numbers - options.counter : numbers;
+      let rate;
+
+      if (toadd < 5) {
+        rate = 2000;
+      } else if (toadd < 10) {
+        rate = 200;
+      } else if (toadd < 100) {
+        rate = 50;
+      } else {
+        rate = 10;
+      }
+
+      const bisect = d3.bisector(d => d.cumul).left;
+      const randdata = []; // random datagenarator
+
+      for (let i = 0; i < toadd; i += 1) {
+        // draw random sample ('index' corresponds to class index in 'data.json')
+        const index = bisect(options.data, Math.random() * 100);
+        const datum = {
+          color: options.data[index].color,
+          cumul: options.data[index].cumul,
+          score: options.data[index].score,
+          index
+        };
+
+        randdata.push(datum);
+      }
+
+      // assign random circles and animate them
+      letItFall(svg, options, { randdata, rate });
+    };
+
+    choice(options.point, options.fillup);
+  };
+
+  // "setter"
+  poll.options = input => {
+    Object.assign(options, input);
+
+    return poll;
+  };
+
+  return poll;
+};

+ 115 - 0
src/js/d3/poll-generator.js

@@ -0,0 +1,115 @@
+import * as d3 from 'd3';
+import { repr } from '../utilities/formatter';
+
+// d3js module for module04: poll generator
+export default () => {
+  // defaults
+  const options = {
+    width: 475,
+    height: 500,
+    radius: 10,
+    data: [],
+    pollindex: 0,
+    setData: () => {} // eslint-disable-line no-empty-function
+  };
+
+  // start
+  const poll = selection => {
+    const svg = selection.select('svg');
+    const scores = options.data.map(distribution => distribution.score);
+    const xScale = d3.scaleLinear().domain([ 0, 100 ]).range([ 0, options.width ]);
+    const bisect = d3.bisector(d => d.cumul).left;
+
+    // reset everything
+    const reset = () => {
+      svg.select('.counter').text('');
+      svg.selectAll('rect.diff, rect.election').transition().duration(1000).attr('width', 0);
+      svg.selectAll('text.difftext, text.result').text('');
+      svg.selectAll('rect.poll').transition().duration(1000).attr('width', 0);
+      svg.selectAll('text.polltext').text('');
+      svg.selectAll('circle').transition()
+        .duration(2000)
+        .attr('cy', () => (options.height * Math.random()))
+        .attr('cx', () => (options.width * Math.random()))
+        .attr('fill-opacity', 0.2);
+    };
+
+    // generate new poll
+    const generatepoll = pollind => {
+      if (options.pollindex === 0) reset();
+
+      let xIndex;
+      let yind;
+
+      // determine correct position
+      if (pollind < options.numberOfCols) {
+        xIndex = pollind;
+        yind = 0;
+      } else {
+        xIndex = pollind - options.numberOfCols;
+        yind = 1;
+      }
+
+      const polldata = [ 0, 0, 0, 0, 0 ];
+      let pollperc = [ 0, 0, 0, 0, 0 ];
+      let pollcounter = 0;
+
+      // add them one by one
+      const anim = setInterval(() => {
+        animatepoll(1000); // eslint-disable-line no-use-before-define
+      }, 1);
+
+      // show poll results
+      const animatepoll = pollsize => {
+        if (pollcounter === pollsize) {
+          clearInterval(anim);
+          if (options.pollindex === 4) {
+            options.setData({ continue: true });
+          }
+        }
+        pollcounter += 1;
+        // draw random sample ('index' corresponds to class index in 'data.json')
+        const index = bisect(options.data, Math.random() * 100);
+
+        // add to polldata
+        polldata[index] += 1;
+
+        // calculate difference with result
+        pollperc = polldata.map(score => (Math.round(score / pollcounter * 1000) / 10));
+        pollperc.forEach((element, i) => {
+          pollperc[i] = Math.round((pollperc[i] - scores[i]) * 100) / 100;
+        });
+
+        const distance = (options.width - options.xoffset * 2) / (options.numberOfCols - 1);
+        const xBase = options.xoffset + xIndex * distance;
+
+        // increase rectangle width
+        svg.selectAll(`rect.diff${xIndex}${yind}`)
+          .data(pollperc)
+          .attr('width', d => (xScale(Math.abs(d))))
+          .attr('x', d => (d < 0 ? xBase - Math.abs(xScale(d)) : xBase));
+
+        // add text, format it
+        svg.selectAll(`.difftext${xIndex}${yind}`)
+          .data(pollperc)
+          .attr('x', d => (d > 0 ? xBase + xScale(d) + 7 : xBase + xScale(d) - 6))
+          .attr('text-anchor', d => (d > 0 ? 'start' : 'end'))
+          .text(d => repr(d))
+          .attr('fill-opacity', pollcounter > 100 ? 1 : 0);
+      };
+    };
+
+    // start next poll
+    generatepoll(options.pollindex);
+  };
+
+  // "setter"
+  poll.options = input => {
+    Object.assign(options, input);
+
+    return poll;
+  };
+
+  return poll;
+};
+

+ 62 - 0
src/js/d3/poll-moveup.js

@@ -0,0 +1,62 @@
+// d3js module for module04: move everything to the top
+export default () => {
+  // defaults
+  const options = {
+    width: 475,
+    height: 500,
+    radius: 10
+  };
+
+  // start
+  const poll = selection => {
+    // selections and defaults
+    const circles = selection.selectAll('circle');
+    const resGroup = selection.select('.counts--first');
+    const rects = resGroup.selectAll('rect.election');
+    const texts = resGroup.selectAll('text.result');
+    const getCoordinate = (refVal, refVal2) => (Math.max(options.radius, Math.min(refVal - options.radius, refVal2 * Math.random())));
+    const yposRect = options.height * 0.9 + options.barHeight / 3;
+    const yposText = {
+      above: yposRect - options.barHeight / 2,
+      below: yposRect + options.barHeight * 7 / 3
+    };
+
+    // move circles to the top by changing x and y coordinate as well as radius
+    // use the same color for all circles, add opacity
+    circles
+      .transition()
+      .duration(2000)
+      .attr('cx', d => (d.x = getCoordinate(options.width, options.width)))
+      .attr('cy', d => (d.y = getCoordinate(options.height, options.height / 3.3)))
+      .attr('r', () => (2 + Math.random() * (options.radius - 2)))
+      .attr('fill', options.baseColor)
+      .attr('fill-opacity', 0.5);
+
+    // move rectangles to the top
+    rects
+      .transition()
+      .duration(2000)
+      .attr('y', yposRect);
+
+    // move texts as well to the top
+    // add continue button if finished
+    texts
+      .transition()
+      .duration(2000)
+      .attr('y', (d, i) => (i % 2 ? yposText.above : yposText.below))
+      .on('end', (d, i) => {
+        // no delay + same duration, it's enough to check only one 'ended'
+        if (texts.size() === i + 1) options.setData({ continue: true });
+      });
+  };
+
+  // "setter"
+  poll.options = input => {
+    Object.assign(options, input);
+
+    return poll;
+  };
+
+  return poll;
+};
+

+ 33 - 0
src/js/d3/poll-showdiff.js

@@ -0,0 +1,33 @@
+// d3js module for module04: show difference between election results
+export default () => {
+  // defaults
+  const options = {
+    width: 475,
+    height: 500,
+    radius: 10
+  };
+
+  // start
+  const poll = selection => {
+    const diffs = selection.selectAll('rect.diff, .difftext');
+
+    // fade-in: difference charts
+    diffs
+      .transition()
+      .duration(2000)
+      .attr('fill-opacity', 1)
+      .on('end', (d, i) => {
+        if (diffs.size() === i + 1) options.setData({ continue: true });
+      });
+  };
+
+  // "setter"
+  poll.options = input => {
+    Object.assign(options, input);
+
+    return poll;
+  };
+
+  return poll;
+};
+

+ 36 - 0
src/js/d3/poll-sort.js

@@ -0,0 +1,36 @@
+import * as d3 from 'd3';
+
+export default () => {
+  // defaults
+  const options = {
+    width: 475,
+    height: 500,
+    radius: 10
+  };
+
+  // start
+  const poll = selection => {
+    const xScale = d3.scaleLinear().domain([ 0, 100 ]).range([ 0, options.width ]);
+    const minWidth = options.radius;
+    const maxWidth = options.width - options.radius;
+    const circles = selection.selectAll('circle');
+
+    circles.transition()
+      .duration(2000)
+      .delay((d, i) => (i * 2))
+      .attr('cx', d => (d.x = Math.max(minWidth, Math.min(maxWidth, xScale(d.cumul - Math.random() * d.score)))))
+      .on('end', (d, i) => {
+        if (circles.size() === i + 1) options.setData({ continue: true });
+      });
+  };
+
+  // "setter"
+  poll.options = input => {
+    Object.assign(options, input);
+
+    return poll;
+  };
+
+  return poll;
+};
+

+ 105 - 0
src/js/d3/poll-sortbars.js

@@ -0,0 +1,105 @@
+import * as d3 from 'd3';
+
+// d3js module for module04: sort poll bar chart
+export default () => {
+  // defaults
+  const options = {
+    width: 475,
+    height: 500,
+    radius: 10
+  };
+
+  // start
+  const poll = selection => {
+    const svg = selection.select('svg');
+    const xScale = d3.scaleLinear().domain([ 0, 100 ]).range([ 0, options.width ]);
+    const distance = (options.width - options.xoffset * 2) / (options.numberOfCols - 1);
+    const step = 18;
+    const duration = 2000;
+
+    // get rectangle x coordinate
+    const getRectXval = (d, i) => {
+      let index = i;
+
+      if (i >= options.numberOfCols) index -= options.numberOfCols;
+
+      const xBase = options.xoffset + distance * index;
+
+      return d > 0 ? xBase : xBase - xScale(Math.abs(d));
+    };
+
+    // get text x coordinate
+    const getTextXval = (d, i) => {
+      let index = i;
+
+      if (i >= options.numberOfCols) index -= options.numberOfCols;
+
+      let xBase = options.xoffset + distance * index;
+
+      if (d > 0) {
+        xBase = xBase + xScale(d) + 7;
+      } else {
+        xBase = xBase - xScale(Math.abs(d)) - 6;
+      }
+
+      return xBase;
+    };
+
+    // get rectangle y coordinate
+    const getRectYval = (i, x, y) => {
+      let xBase = x;
+
+      if (y > 0) xBase += options.numberOfCols;
+
+      let yBase = options.yoffset + step * xBase;
+
+      if (i >= options.numberOfCols) yBase += options.yoffsetSecRow;
+
+      return yBase;
+    };
+
+    // get text y coordinate
+    const getTextYval = (i, x, y) => {
+      let xBase = x;
+
+      if (y > 0) xBase += options.numberOfCols;
+
+      let yBase = options.yoffset + step * xBase + 14;
+
+      if (i >= options.numberOfCols) yBase += options.yoffsetSecRow;
+
+      return yBase;
+    };
+
+    // move bars and texts
+    for (let x = 0; x < options.numberOfCols; x += 1) {
+      for (let y = 0; y < 2; y += 1) {
+        // Move the bars
+        svg.selectAll(`.diff${x}${y}`)
+          .transition()
+          .duration(duration)
+          .attr('x', (d, i) => getRectXval(d, i))
+          .attr('y', (d, i) => getRectYval(i, x, y));
+
+        // Move the text
+        svg.selectAll(`.difftext${x}${y}`)
+          .transition()
+          .duration(duration)
+          .attr('x', (d, i) => getTextXval(d, i))
+          .attr('y', (d, i) => getTextYval(i, x, y));
+      }
+    }
+
+    window.setTimeout(() => (options.setData({ continue: true })), duration);
+  };
+
+  // "setter"
+  poll.options = input => {
+    Object.assign(options, input);
+
+    return poll;
+  };
+
+  return poll;
+};
+

+ 39 - 0
src/js/d3/poll.js

@@ -0,0 +1,39 @@
+import barChartsInit from './init-barcharts.js';
+import pollResultsInit from './init-pollresults.js';
+import counterTextInit from './init-countertext.js';
+import circlesInit from './init-circles.js';
+
+
+export default () => {
+  // defaults
+  const options = {
+    width: 475,
+    height: 500,
+    radius: 10,
+    data: []
+  };
+
+  // start
+  const poll = selection => {
+    // append svg
+    const svg = selection
+      .append('svg')
+      .attr('width', options.width)
+      .attr('height', options.height);
+
+    // initialize counter, circles, bar charts and poll results
+    counterTextInit(svg, options);
+    circlesInit(svg, options);
+    barChartsInit(svg, options);
+    pollResultsInit(svg, options);
+  };
+
+  // "setter"
+  poll.options = input => {
+    Object.assign(options, input);
+
+    return poll;
+  };
+
+  return poll;
+};

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

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

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

@@ -0,0 +1,93 @@
+// Formatter for number representations
+import round from './math';
+
+// get a fixed number of digits
+const fixedDigits = (number, numberDigits) => {
+  const str = `${number}`;
+  let padding = '';
+
+  for (let i = 0; i < numberDigits; i += 1) {
+    padding += '0';
+  }
+
+  return padding.substring(0, padding.length - str.length) + str;
+};
+
+const needsSpacing = (val1, val2) => {
+  let toBeSpaced = 0;
+
+  // Add additional spacing when one value has decimal places and the other not
+  if ((val1 < 1 && val2 >= 1) || (val1 >= 1 && val2 < 1)) {
+    toBeSpaced = 1;
+  }
+
+  return toBeSpaced;
+};
+
+// - 0 <= x < 1:        1 decimal place
+// - 1 <= x <= 100.000: 0 decimal places
+const getPrecision = (val) => {
+  if (val < 1) {
+    return 10;
+  }
+  return 1;
+};
+
+// - 0 <= x < 1:        0 spaces
+// - 1 <= x <= 100.000: 2 spaces
+const getSpacing = (val) => {
+  if (val >= 1) {
+    return 2;
+  }
+  return 0;
+};
+
+// - 0 <= x < 1:        1 decimal place
+// - 1 <= x <= 100.000: 0 decimal places
+const getFormattedValue = (val) => {
+
+  const digits = Math.log(val) / Math.log(10);
+  let formattedValue;
+
+  if (digits < 0) {
+    formattedValue = Math.max(round(val, 1), 0.1);
+  } else {
+    formattedValue = round(val, 0);
+  }
+
+  return formattedValue;
+};
+
+const haveSameRepresentation = (value1, value2) => {
+
+  const p1 = getFormattedValue(value1);
+  const p2 = getFormattedValue(value2);
+
+  return p1 === p2 || (p1 < 0.1 && p2 < 0.1);
+};
+
+// returns a string representation of a given value
+// @param value: The value in question
+// @param d: Number of decimal places
+// @param locale: The locale in which the string representation should be formatted
+// @param sign: Boolean which indicates if representation should always be signed
+const repr = (value, d = 1, locale = 'de-DE', sign = true) => {
+  const val = round(value, d);
+  let rep = val.toLocaleString(locale);
+
+  if (sign && val > 0) {
+    rep = `+${rep}`;
+  }
+
+  return rep;
+};
+
+export {
+  fixedDigits,
+  needsSpacing,
+  getPrecision,
+  getSpacing,
+  getFormattedValue,
+  haveSameRepresentation,
+  repr
+};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 62 - 0
src/scss/main.scss

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,36 @@
+// content styles
+// ======================================================================
+.intro {
+  @include center;
+  max-width: $base-double * 10;
+  // text-align: center;
+}
+
+.spacingr {
+  @include spacing(r 4);
+}
+
+.fakenews__content {
+  opacity: 0;
+}
+
+.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;
+    }
+  }
+}
+
+

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini