changeset 2805:eb91ad1d7a93

client: waterlevel diagram: optimized code for better readability
author Markus Kottlaender <markus@intevation.de>
date Mon, 25 Mar 2019 21:26:52 +0100
parents e3c5efd21cb4
children c6374c520228
files client/src/components/gauge/Waterlevel.vue
diffstat 1 files changed, 451 insertions(+), 435 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/components/gauge/Waterlevel.vue	Mon Mar 25 18:37:36 2019 +0100
+++ b/client/src/components/gauge/Waterlevel.vue	Mon Mar 25 21:26:52 2019 +0100
@@ -22,6 +22,8 @@
       circle
         stroke-width: 0
         fill: steelblue
+        &.d3-line-chunked-chunk-predicted-point
+          fill-opacity: 0.6
 
     .hdc-line,
     .ldc-line,
@@ -123,522 +125,526 @@
 import { mapState, mapGetters } from "vuex";
 import * as d3Base from "d3";
 import { lineChunked } from "d3-line-chunked";
+import { startOfDay, endOfDay } from "date-fns";
 import debounce from "debounce";
 
 // we should load only d3 modules we need but for now we'll go with the lazy way
 // https://www.giacomodebidda.com/how-to-import-d3-plugins-with-webpack/
+// d3-line-chunked plugin: https://github.com/pbeshai/d3-line-chunked
 const d3 = Object.assign(d3Base, { lineChunked });
 
 export default {
+  data() {
+    return {
+      svg: null,
+      diagram: null,
+      navigation: null,
+      dimensions: null,
+      extent: null,
+      scale: null,
+      axes: null
+    };
+  },
   computed: {
     ...mapState("gauges", [
-      "waterlevels",
       "dateFrom",
       "dateTo",
+      "waterlevels",
       "nashSutcliffe"
     ]),
     ...mapGetters("gauges", ["selectedGauge"])
   },
-  watch: {
-    waterlevels() {
-      this.drawDiagram();
-    }
-  },
   methods: {
     drawDiagram() {
-      // TODO: Optimize code. I'm pretty sure all of this can be done in a much
-      // more elegant way and with less lines of code.
-
-      // remove old diagram
+      // remove old diagram and exit if necessary data is missing
       d3.select(".diagram-container svg").remove();
-
       if (!this.selectedGauge || !this.waterlevels.length) return;
 
-      // get HDC/LDC/MW of the gauge
-      let refWaterLevels = JSON.parse(
+      // PREPARE HELPERS
+
+      // HDC/LDC/MW for the selected gauge
+      const refWaterLevels = JSON.parse(
         this.selectedGauge.properties.reference_water_levels
       );
 
-      // CREATE SVG AND SET DIMENSIONS/MARGINS
+      // dimensions (widths, heights, margins)
+      this.dimensions = this.getDimensions();
+
+      // get min/max values for date and waterlevel axis
+      this.extent = this.getExtent(refWaterLevels);
+
+      // scaling helpers
+      this.scale = this.getScale();
 
-      let svgWidth = document.querySelector(".diagram-container").clientWidth;
-      let svgHeight = document.querySelector(".diagram-container").clientHeight;
-      let svg = d3
+      // creating the axes based on the scales
+      this.axes = {
+        x: d3
+          .axisTop(this.scale.x)
+          .tickSizeInner(this.dimensions.mainHeight)
+          .tickSizeOuter(0),
+        y: d3
+          .axisRight(this.scale.y)
+          .tickSizeInner(this.dimensions.width)
+          .tickSizeOuter(0),
+        x2: d3.axisBottom(this.scale.x2)
+      };
+
+      // DRAW DIAGRAM/NAVIGATION AREAS
+
+      // create svg
+      this.svg = d3
         .select(".diagram-container")
         .append("svg")
         .attr("width", "100%")
         .attr("height", "100%");
-      let mainMargin = { top: 20, right: 20, bottom: 110, left: 80 },
-        navMargin = {
-          top: svgHeight - mainMargin.top - 65,
-          right: 20,
-          bottom: 30,
-          left: 80
-        },
-        width = +svgWidth - mainMargin.left - mainMargin.right,
-        mainHeight = +svgHeight - mainMargin.top - mainMargin.bottom,
-        navHeight = +svgHeight - navMargin.top - navMargin.bottom;
 
-      // PREPARING AXES/SCALING
-
-      // scaling helpers to convert real values to pixels
-      // based on the diagrams dimensions
-      let x = d3.scaleTime().range([0, width]),
-        x2 = d3.scaleTime().range([0, width]),
-        y = d3.scaleLinear().range([mainHeight, 0]),
-        y2 = d3.scaleLinear().range([navHeight, 0]);
-      // find min/max values for the waterlevel axis
-      // including HDC (+ 1/8 HDC-LDC) and LDC (- 1/4 HDC-LDC)
-      let WaterlevelMinMax = d3.extent(
-        [
-          ...this.waterlevels,
-          {
-            waterlevel:
-              refWaterLevels.HDC + (refWaterLevels.HDC - refWaterLevels.LDC) / 8
-          },
-          {
-            waterlevel: Math.max(
-              refWaterLevels.LDC -
-                (refWaterLevels.HDC - refWaterLevels.LDC) / 4,
-              0
-            )
-          }
-        ],
-        d => d.waterlevel
-      );
-      // setting the min and max values for the diagram axes
-      let dateTo = new Date(this.dateTo.getTime() + 86400);
-      x.domain(d3.extent([this.dateFrom, dateTo]));
-      y.domain(WaterlevelMinMax);
-      x2.domain(x.domain());
-      y2.domain(y.domain());
-      // creating the axes based on these scales
-      let xAxis = d3
-        .axisTop(x)
-        .tickSizeInner(mainHeight)
-        .tickSizeOuter(0);
-      let xAxis2 = d3.axisBottom(x2);
-      let yAxis = d3
-        .axisRight(y)
-        .tickSizeInner(width)
-        .tickSizeOuter(0);
-
-      // PREPARING CHART FUNCTIONS
-
-      // points are "next to each other" when they are exactly 15 minutes apart
-      const isNext = (prev, current) =>
-        current.date - prev.date === 15 * 60 * 1000;
+      // create container for main diagram
+      this.diagram = this.svg
+        .append("g")
+        .attr("class", "main")
+        .attr(
+          "transform",
+          `translate(${this.dimensions.mainMargin.left}, ${
+            this.dimensions.mainMargin.top
+          })`
+        );
 
-      const preditionStyle = {
-        predicted: {
-          pointStyles: {
-            fill: "steelblue",
-            "fill-opacity": 0.6
-          }
-        }
-      };
+      // create container for navigation diagram
+      this.navigation = this.svg
+        .append("g")
+        .attr("class", "nav")
+        .attr(
+          "transform",
+          `translate(${this.dimensions.navMargin.left}, ${
+            this.dimensions.navMargin.top
+          })`
+        );
 
-      // waterlevel line in big chart
-      // d3-line-chunked plugin: https://github.com/pbeshai/d3-line-chunked
-      var mainLineChart = d3
-        .lineChunked()
-        .x(d => x(d.date))
-        .y(d => y(d.waterlevel))
-        .curve(d3.curveLinear)
-        .isNext(isNext)
-        .pointAttrs({ r: 2.2 })
-        .chunk(d => (d.predicted ? "predicted" : "line"))
-        .chunkDefinitions(preditionStyle);
-      // waterlevel line in small chart
-      let navLineChart = d3
-        .lineChunked()
-        .x(d => x2(d.date))
-        .y(d => y2(d.waterlevel))
-        .curve(d3.curveMonotoneX)
-        .isNext(isNext)
-        .pointAttrs({ r: 1.7 })
-        .chunk(d => (d.predicted ? "predicted" : "line"))
-        .chunkDefinitions(preditionStyle);
-      // hdc/ldc/mw
-      let refWaterlevelLine = d3
-        .line()
-        .x(d => x(d.x))
-        .y(d => y(d.y));
-      // now
-      let nowLine = d3
-        .line()
-        .x(d => x(d.x))
-        .y(d => y(d.y));
-      let nowLineNav = d3
-        .line()
-        .x(d => x2(d.x))
-        .y(d => y2(d.y));
-      let nowLineLabel = selection => {
-        selection.attr(
-          "transform",
-          `translate(${x(new Date())}, ${y(WaterlevelMinMax[1] - 16)})`
-        );
-      };
-      // prediction area
-      let predictionArea = d3
-        .area()
-        .defined(d => d.predicted && d.min && d.max)
-        .x(d => x(d.date))
-        .y0(d => y(d.min))
-        .y1(d => y(d.max));
-      let predictionAreaNav = d3
-        .area()
-        .defined(d => d.predicted && d.min && d.max)
-        .x(d => x2(d.date))
-        .y0(d => y2(d.min))
-        .y1(d => y2(d.max));
-
-      // DRAWING MAINCHART
-
-      // define visible chart area
-      // everything outside this area will be hidden (clipped)
-      svg
+      // define visible area, everything outside this area will be hidden
+      this.svg
         .append("defs")
         .append("clipPath")
         .attr("id", "clip")
         .append("rect")
-        .attr("width", width)
-        .attr("height", mainHeight);
+        .attr("width", this.dimensions.width)
+        .attr("height", this.dimensions.mainHeight);
+
+      // DRAW DIAGRAM PARTS
+
+      // Each drawSomething function (with the exception of drawRefLines)
+      // returns a fuction to update the respective chart/area/etc. These
+      // updater functions are used by the zoom feature to rescale all elements.
+      const updaters = [];
+
+      // draw
+      this.drawRefLines(refWaterLevels); // static, doesn't need an updater
+      updaters.push(this.drawAxes());
+      updaters.push(this.drawWaterlevelCharts());
+      updaters.push(this.drawNowLines());
+      updaters.push(this.drawPredictionAreas());
+      updaters.push(this.drawNashSutcliffe(24));
+      updaters.push(this.drawNashSutcliffe(48));
+      updaters.push(this.drawNashSutcliffe(72));
+
+      // INTERACTIONS
+
+      // create rectanlge on the main chart area to capture mouse events
+      const eventRect = this.svg
+        .append("rect")
+        .attr("class", "zoom")
+        .attr("width", this.dimensions.width)
+        .attr("height", this.dimensions.mainHeight)
+        .attr(
+          "transform",
+          `translate(${this.dimensions.mainMargin.left}, ${
+            this.dimensions.mainMargin.top
+          })`
+        );
 
-      let mainChart = svg
-        .append("g")
-        .attr("class", "main")
-        .attr("transform", `translate(${mainMargin.left}, ${mainMargin.top})`);
+      this.createZoom(updaters, eventRect);
+      this.createTooltips(eventRect);
+    },
+    getDimensions() {
+      // dimensions and margins
+      const svgWidth = document.querySelector(".diagram-container").clientWidth;
+      const svgHeight = document.querySelector(".diagram-container")
+        .clientHeight;
+      const mainMargin = { top: 20, right: 20, bottom: 110, left: 80 };
+      const navMargin = {
+        top: svgHeight - mainMargin.top - 65,
+        right: 20,
+        bottom: 30,
+        left: 80
+      };
+      const width = +svgWidth - mainMargin.left - mainMargin.right;
+      const mainHeight = +svgHeight - mainMargin.top - mainMargin.bottom;
+      const navHeight = +svgHeight - navMargin.top - navMargin.bottom;
 
-      // axes
-      mainChart
+      return { width, mainHeight, navHeight, mainMargin, navMargin };
+    },
+    getExtent(refWaterLevels) {
+      return {
+        // set min/max values for the date axis
+        date: [startOfDay(this.dateFrom), endOfDay(this.dateTo)],
+        // set min/max values for the waterlevel axis
+        // including HDC (+ 1/8 HDC-LDC) and LDC (- 1/4 HDC-LDC)
+        waterlevel: d3.extent(
+          [
+            ...this.waterlevels,
+            {
+              waterlevel:
+                refWaterLevels.HDC +
+                (refWaterLevels.HDC - refWaterLevels.LDC) / 8
+            },
+            {
+              waterlevel: Math.max(
+                refWaterLevels.LDC -
+                  (refWaterLevels.HDC - refWaterLevels.LDC) / 4,
+                0
+              )
+            }
+          ],
+          d => d.waterlevel
+        )
+      };
+    },
+    getScale() {
+      // scaling helpers to convert real world values into pixels
+      const x = d3.scaleTime().range([0, this.dimensions.width]);
+      const y = d3.scaleLinear().range([this.dimensions.mainHeight, 0]);
+      const x2 = d3.scaleTime().range([0, this.dimensions.width]);
+      const y2 = d3.scaleLinear().range([this.dimensions.navHeight, 0]);
+
+      // setting the min and max values for the diagram axes
+      x.domain(d3.extent(this.extent.date));
+      y.domain(this.extent.waterlevel);
+      x2.domain(x.domain());
+      y2.domain(y.domain());
+
+      return { x, y, x2, y2 };
+    },
+    drawAxes() {
+      this.diagram
         .append("g")
         .attr("class", "axis--x")
-        .attr("transform", `translate(0, ${mainHeight})`)
-        .call(xAxis)
+        .attr("transform", `translate(0, ${this.dimensions.mainHeight})`)
+        .call(this.axes.x)
         .selectAll(".tick text")
         .attr("y", 15);
-      mainChart // label
+      this.diagram // label
         .append("text")
         .text(this.$gettext("Waterlevel [cm]"))
         .attr("text-anchor", "middle")
-        .attr("transform", `translate(-45, ${mainHeight / 2}) rotate(-90)`);
-      mainChart
+        .attr(
+          "transform",
+          `translate(-45, ${this.dimensions.mainHeight / 2}) rotate(-90)`
+        );
+      this.diagram
         .append("g")
-        .call(yAxis)
+        .call(this.axes.y)
         .selectAll(".tick text")
         .attr("x", -25);
 
-      // reference waterlevels
-      // filling area between HDC and LDC
-      mainChart
-        .append("rect")
-        .attr("class", "hdc-ldc-area")
-        .attr("x", 0)
-        .attr("y", y(refWaterLevels.HDC))
-        .attr("width", width)
-        .attr("height", y(refWaterLevels.LDC) - y(refWaterLevels.HDC));
+      this.navigation
+        .append("g")
+        .attr("class", "axis axis--x")
+        .attr("transform", `translate(0, ${this.dimensions.navHeight})`)
+        .call(this.axes.x2);
+
+      return () => {
+        this.diagram
+          .select(".axis--x")
+          .call(this.axes.x)
+          .selectAll(".tick text")
+          .attr("y", 15);
+      };
+    },
+    drawWaterlevelCharts() {
+      const waterlevelChartDrawer = isNav => {
+        return d3
+          .lineChunked()
+          .x(d => this.scale[isNav ? "x2" : "x"](d.date))
+          .y(d => this.scale[isNav ? "y2" : "y"](d.waterlevel))
+          .curve(d3.curveLinear)
+          .isNext(this.isNext(900))
+          .pointAttrs({ r: isNav ? 1.7 : 2.2 })
+          .chunk(d => (d.predicted ? "predicted" : "line"))
+          .chunkDefinitions({ predicted: {} });
+      };
 
-      // HDC
-      mainChart
-        .append("path")
-        .datum([
-          { x: 0, y: refWaterLevels.HDC },
-          { x: dateTo, y: refWaterLevels.HDC }
-        ])
-        .attr("class", "hdc-line")
-        .attr("d", refWaterlevelLine);
-      mainChart // label
-        .append("text")
-        .text("HDC")
-        .attr("class", "ref-waterlevel-label")
-        .attr("x", x(dateTo) - 20)
-        .attr("y", y(refWaterLevels.HDC) - 3);
-      // LDC
-      mainChart
+      this.diagram
+        .append("g")
+        .attr("class", "line")
+        .datum(this.waterlevels)
+        .call(waterlevelChartDrawer());
+
+      this.navigation
+        .append("g")
+        .attr("class", "line")
+        .datum(this.waterlevels)
+        .call(waterlevelChartDrawer(true));
+
+      return () => {
+        this.diagram.select(".line").call(waterlevelChartDrawer());
+      };
+    },
+    drawNowLines() {
+      const nowLine = d3
+        .line()
+        .x(d => this.scale.x(d.x))
+        .y(d => this.scale.y(d.y));
+
+      const nowLabel = selection => {
+        selection.attr(
+          "transform",
+          `translate(${this.scale.x(new Date())}, ${this.scale.y(
+            this.extent.waterlevel[1] - 16
+          )})`
+        );
+      };
+
+      // draw in main
+      this.diagram
         .append("path")
         .datum([
-          { x: 0, y: refWaterLevels.LDC },
-          { x: dateTo, y: refWaterLevels.LDC }
-        ])
-        .attr("class", "ldc-line")
-        .attr("d", refWaterlevelLine);
-      mainChart // label
-        .append("text")
-        .text("LDC")
-        .attr("class", "ref-waterlevel-label")
-        .attr("x", x(dateTo) - 20)
-        .attr("y", y(refWaterLevels.LDC) - 3);
-      // MW
-      mainChart
-        .append("path")
-        .datum([
-          { x: 0, y: refWaterLevels.MW },
-          { x: dateTo, y: refWaterLevels.MW }
-        ])
-        .attr("class", "mw-line")
-        .attr("d", refWaterlevelLine);
-      mainChart // label
-        .append("text")
-        .text("MW")
-        .attr("class", "ref-waterlevel-label")
-        .attr("x", x(dateTo) - 20)
-        .attr("y", y(refWaterLevels.MW) - 3);
-
-      // now
-      mainChart
-        .append("path")
-        .datum([
-          { x: new Date(), y: WaterlevelMinMax[0] },
-          { x: new Date(), y: WaterlevelMinMax[1] - 20 }
+          { x: new Date(), y: this.extent.waterlevel[0] },
+          { x: new Date(), y: this.extent.waterlevel[1] - 20 }
         ])
         .attr("class", "now-line")
         .attr("d", nowLine);
-      mainChart // label
+      this.diagram // label
         .append("text")
         .text(this.$gettext("Now"))
         .attr("class", "now-line-label")
         .attr("text-anchor", "middle")
-        .call(nowLineLabel);
+        .call(nowLabel);
+
+      // draw in nav
+      this.navigation
+        .append("path")
+        .datum([
+          { x: new Date(), y: this.extent.waterlevel[0] },
+          { x: new Date(), y: this.extent.waterlevel[1] - 20 }
+        ])
+        .attr("class", "now-line")
+        .attr(
+          "d",
+          d3
+            .line()
+            .x(d => this.scale.x2(d.x))
+            .y(d => this.scale.y2(d.y))
+        );
 
-      // prediction area
-      mainChart
+      return () => {
+        this.diagram.select(".now-line").attr("d", nowLine);
+        this.diagram.select(".now-line-label").call(nowLabel);
+      };
+    },
+    drawPredictionAreas() {
+      const predictionArea = isNav =>
+        d3
+          .area()
+          .defined(d => d.predicted && d.min && d.max)
+          .x(d => this.scale[isNav ? "x2" : "x"](d.date))
+          .y0(d => this.scale[isNav ? "y2" : "y"](d.min))
+          .y1(d => this.scale[isNav ? "y2" : "y"](d.max));
+
+      this.diagram
+        .append("path")
+        .datum(this.waterlevels)
+        .attr("class", "prediction-area")
+        .attr("d", predictionArea());
+
+      this.navigation
         .append("path")
         .datum(this.waterlevels)
         .attr("class", "prediction-area")
-        .attr("d", predictionArea);
-
-      // waterlevel chart
-      mainChart
-        .append("g")
-        .attr("class", "line")
-        .datum(this.waterlevels)
-        .call(mainLineChart);
-
-      // DRAWING NAVCHART
+        .attr("d", predictionArea(true));
 
-      let navChart = svg
-        .append("g")
-        .attr("class", "nav")
-        .attr("transform", `translate(${navMargin.left}, ${navMargin.top})`);
+      return () => {
+        this.diagram.select(".prediction-area").attr("d", predictionArea());
+      };
+    },
+    drawRefLines(refWaterLevels) {
+      // filling area between HDC and LDC
+      this.diagram
+        .append("rect")
+        .attr("class", "hdc-ldc-area")
+        .attr("x", 0)
+        .attr("y", this.scale.y(refWaterLevels.HDC))
+        .attr("width", this.dimensions.width)
+        .attr(
+          "height",
+          this.scale.y(refWaterLevels.LDC) - this.scale.y(refWaterLevels.HDC)
+        );
 
-      // axis (nav chart only has x-axis)
-      navChart
-        .append("g")
-        .attr("class", "axis axis--x")
-        .attr("transform", `translate(0, ${navHeight})`)
-        .call(xAxis2);
+      const refWaterlevelLine = d3
+        .line()
+        .x(d => this.scale.x(d.x))
+        .y(d => this.scale.y(d.y));
 
-      // now
-      navChart
+      // HDC
+      this.diagram
         .append("path")
         .datum([
-          { x: new Date(), y: WaterlevelMinMax[0] },
-          { x: new Date(), y: WaterlevelMinMax[1] - 20 }
+          { x: 0, y: refWaterLevels.HDC },
+          { x: this.extent.date[1], y: refWaterLevels.HDC }
         ])
-        .attr("class", "now-line")
-        .attr("d", nowLineNav);
-
-      // prediction area
-      navChart
+        .attr("class", "hdc-line")
+        .attr("d", refWaterlevelLine);
+      this.diagram // label
+        .append("text")
+        .text(`HDC (${refWaterLevels.HDC})`)
+        .attr("class", "ref-waterlevel-label")
+        .attr("x", 5)
+        .attr("y", this.scale.y(refWaterLevels.HDC) - 3);
+      // LDC
+      this.diagram
         .append("path")
-        .datum(this.waterlevels)
-        .attr("class", "prediction-area")
-        .attr("d", predictionAreaNav);
-
-      // waterlevel chart
-      navChart
-        .append("g")
-        .attr("class", "line")
-        .datum(this.waterlevels)
-        .call(navLineChart);
-
-      // NASH SUTCLIFFE
-
-      let nashSut24 = this.nashSutcliffe.coeffs.find(c => c.hours === 24);
-      let nashSut48 = this.nashSutcliffe.coeffs.find(c => c.hours === 48);
-      let nashSut72 = this.nashSutcliffe.coeffs.find(c => c.hours === 72);
-
-      let nashSutDateNow = new Date(this.nashSutcliffe.when);
-      let nashSutDate24 = new Date(this.nashSutcliffe.when);
-      let nashSutDate48 = new Date(this.nashSutcliffe.when);
-      let nashSutDate72 = new Date(this.nashSutcliffe.when);
-      nashSutDate24.setDate(nashSutDate24.getDate() - 1);
-      nashSutDate48.setDate(nashSutDate48.getDate() - 2);
-      nashSutDate72.setDate(nashSutDate72.getDate() - 3);
+        .datum([
+          { x: 0, y: refWaterLevels.LDC },
+          { x: this.extent.date[1], y: refWaterLevels.LDC }
+        ])
+        .attr("class", "ldc-line")
+        .attr("d", refWaterlevelLine);
+      this.diagram // label
+        .append("text")
+        .text(`LDC (${refWaterLevels.LDC})`)
+        .attr("class", "ref-waterlevel-label")
+        .attr("x", 5)
+        .attr("y", this.scale.y(refWaterLevels.LDC) - 3);
+      // MW
+      this.diagram
+        .append("path")
+        .datum([
+          { x: 0, y: refWaterLevels.MW },
+          { x: this.extent.date[1], y: refWaterLevels.MW }
+        ])
+        .attr("class", "mw-line")
+        .attr("d", refWaterlevelLine);
+      this.diagram // label
+        .append("text")
+        .text(`MW (${refWaterLevels.MW})`)
+        .attr("class", "ref-waterlevel-label")
+        .attr("x", 5)
+        .attr("y", this.scale.y(refWaterLevels.MW) - 3);
+    },
+    drawNashSutcliffe(hours) {
+      const coeff = this.nashSutcliffe.coeffs.find(c => c.hours === hours);
+      const dateNow = new Date(this.nashSutcliffe.when);
+      const dateStart = new Date(dateNow.getTime() - hours * 60 * 60 * 1000);
 
       const nashSutcliffeBox = hours => {
         return d3
           .area()
-          .x(d => x(d))
-          .y0(() => mainHeight + 0.5)
-          .y1(() => mainHeight - 15 * (hours / 24));
+          .x(d => this.scale.x(d))
+          .y0(() => this.dimensions.mainHeight + 0.5)
+          .y1(() => this.dimensions.mainHeight - 15 * (hours / 24));
       };
 
       const nashSutcliffeLabel = (label, date, hours) => {
         let days = hours / 24;
         label
-          .attr("x", x(date) + 3)
-          .attr("y", mainHeight - (15 * days + 0.5) + 12);
+          .attr("x", this.scale.x(date) + 3)
+          .attr("y", this.dimensions.mainHeight - (15 * days + 0.5) + 12);
       };
 
       // Show nash-sutcliffe only when x-axis extent is smaller than 35 days
       // (3024000000 ms). Since it shows squares representing 1, 2 and 3 days
       // it does not make sense to show them on a x-axis with hundres of days.
-      if (this.nashSutcliffe && x.domain()[1] - x.domain()[0] < 3024000000) {
-        if (nashSut24.samples) {
-          mainChart
-            .append("path")
-            .datum([nashSutDate24, nashSutDateNow])
-            .attr("class", "nash-sutcliffe ns24")
-            .attr("d", nashSutcliffeBox(24));
-          mainChart
-            .append("text")
-            .attr("class", "nash-sutcliffe ns24")
-            .call(nashSutcliffeLabel, nashSutDate24, 24)
-            .append("tspan")
-            .text(nashSut24.value.toFixed(2))
-            .select(function() {
-              return this.parentNode;
-            })
-            .append("tspan")
-            .text(` (${nashSut24.samples})`)
-            .attr("dy", -1);
-        }
-        if (nashSut48.samples) {
-          mainChart
-            .append("path")
-            .datum([nashSutDate48, nashSutDateNow])
-            .attr("class", "nash-sutcliffe ns48")
-            .attr("d", nashSutcliffeBox(48));
-          mainChart
-            .append("text")
-            .attr("class", "nash-sutcliffe ns48")
-            .call(nashSutcliffeLabel, nashSutDate48, 48)
-            .append("tspan")
-            .text(nashSut48.value.toFixed(2))
-            .select(function() {
-              return this.parentNode;
-            })
-            .append("tspan")
-            .text(` (${nashSut48.samples})`)
-            .attr("dy", -1);
-        }
-        if (nashSut72.samples) {
-          mainChart
-            .append("path")
-            .datum([nashSutDate72, nashSutDateNow])
-            .attr("class", "nash-sutcliffe ns72")
-            .attr("d", nashSutcliffeBox(72));
-          mainChart
-            .append("text")
-            .attr("class", "nash-sutcliffe ns72")
-            .call(nashSutcliffeLabel, nashSutDate72, 72)
-            .append("tspan")
-            .text(nashSut72.value.toFixed(2))
-            .select(function() {
-              return this.parentNode;
-            })
-            .append("tspan")
-            .text(` (${nashSut72.samples})`)
-            .attr("dy", -1);
-        }
+      if (
+        coeff.samples &&
+        this.scale.x.domain()[1] - this.scale.x.domain()[0] < 3024000000
+      ) {
+        this.diagram
+          .append("path")
+          .datum([dateStart, dateNow])
+          .attr("class", "nash-sutcliffe ns" + hours)
+          .attr("d", nashSutcliffeBox(hours));
+        this.diagram
+          .append("text")
+          .attr("class", "nash-sutcliffe ns" + hours)
+          .call(nashSutcliffeLabel, dateStart, hours)
+          .append("tspan")
+          .text(coeff.value.toFixed(2))
+          .select(function() {
+            return this.parentNode;
+          })
+          .append("tspan")
+          .text(` (${coeff.samples})`)
+          .attr("dy", -1);
+
+        return () => {
+          this.diagram
+            .select("path.nash-sutcliffe.ns" + hours)
+            .attr("d", nashSutcliffeBox(hours));
+          this.diagram
+            .select("text.nash-sutcliffe.ns" + hours)
+            .call(nashSutcliffeLabel, dateStart, hours);
+        };
       }
-
-      // INTERACTIVITY
-
-      const updateChart = () => {
-        mainChart.select(".line").call(mainLineChart);
-        mainChart.select(".now-line").attr("d", nowLine);
-        mainChart.select(".now-line-label").call(nowLineLabel);
-        mainChart.select(".prediction-area").attr("d", predictionArea);
-        mainChart
-          .select("path.nash-sutcliffe.ns24")
-          .attr("d", nashSutcliffeBox(24));
-        mainChart
-          .select("text.nash-sutcliffe.ns24")
-          .call(nashSutcliffeLabel, nashSutDate24, 24);
-        mainChart
-          .select("path.nash-sutcliffe.ns48")
-          .attr("d", nashSutcliffeBox(48));
-        mainChart
-          .select("text.nash-sutcliffe.ns48")
-          .call(nashSutcliffeLabel, nashSutDate48, 48);
-        mainChart
-          .select("path.nash-sutcliffe.ns72")
-          .attr("d", nashSutcliffeBox(72));
-        mainChart
-          .select("text.nash-sutcliffe.ns72")
-          .call(nashSutcliffeLabel, nashSutDate72, 72);
-        mainChart
-          .select(".axis--x")
-          .call(xAxis)
-          .selectAll(".tick text")
-          .attr("y", 15);
-      };
-
-      // selecting time period in nav chart
-      let brush = d3
+    },
+    createZoom(updaters, eventRect) {
+      const brush = d3
         .brushX()
         .handleSize(4)
-        .extent([[0, 0], [width, navHeight]])
-        .on("brush end", () => {
-          if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom")
-            return; // ignore brush-by-zoom
-          let s = d3.event.selection || x2.range();
-          x.domain(s.map(x2.invert, x2));
-          updateChart();
-          svg
-            .select(".zoom")
-            .call(
-              zoom.transform,
-              d3.zoomIdentity.scale(width / (s[1] - s[0])).translate(-s[0], 0)
-            );
-        });
+        .extent([[0, 0], [this.dimensions.width, this.dimensions.navHeight]]);
 
-      // zooming with mousewheel in main chart
-      let zoom = d3
+      const zoom = d3
         .zoom()
         .scaleExtent([1, Infinity])
-        .translateExtent([[0, 0], [width, mainHeight]])
-        .extent([[0, 0], [width, mainHeight]])
-        .on("zoom", () => {
-          if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush")
-            return; // ignore zoom-by-brush
-          let t = d3.event.transform;
-          x.domain(t.rescaleX(x2).domain());
-          updateChart();
-          navChart
-            .select(".brush")
-            .call(brush.move, x.range().map(t.invertX, t));
-        })
-        .on("start", () => {
-          svg.select(".chart-dot").style("opacity", 0);
-          svg.select(".chart-tooltip").style("opacity", 0);
-        });
+        .translateExtent([
+          [0, 0],
+          [this.dimensions.width, this.dimensions.mainHeight]
+        ])
+        .extent([[0, 0], [this.dimensions.width, this.dimensions.mainHeight]]);
 
-      navChart
+      brush.on("brush end", () => {
+        if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom")
+          return; // ignore brush-by-zoom
+        let s = d3.event.selection || this.scale.x2.range();
+        this.scale.x.domain(s.map(this.scale.x2.invert, this.scale.x2));
+        updaters.forEach(u => u && u());
+        this.svg
+          .select(".zoom")
+          .call(
+            zoom.transform,
+            d3.zoomIdentity
+              .scale(this.dimensions.width / (s[1] - s[0]))
+              .translate(-s[0], 0)
+          );
+      });
+
+      zoom.on("zoom", () => {
+        if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush")
+          return; // ignore zoom-by-brush
+        let t = d3.event.transform;
+        this.scale.x.domain(t.rescaleX(this.scale.x2).domain());
+        updaters.forEach(u => u && u());
+        this.navigation
+          .select(".brush")
+          .call(brush.move, this.scale.x.range().map(t.invertX, t));
+      });
+      zoom.on("start", () => {
+        this.svg.select(".chart-dot").style("opacity", 0);
+        this.svg.select(".chart-tooltip").style("opacity", 0);
+      });
+
+      this.navigation
         .append("g")
         .attr("class", "brush")
         .call(brush)
-        .call(brush.move, x.range());
+        .call(brush.move, this.scale.x.range());
 
-      let zoomRect = svg
-        .append("rect")
-        .attr("class", "zoom")
-        .attr("width", width)
-        .attr("height", mainHeight)
-        .attr("transform", `translate(${mainMargin.left}, ${mainMargin.top})`)
-        .call(zoom);
-
-      // TOOLTIPS
-
-      let dots = mainChart.append("g").attr("class", "chart-dots");
+      eventRect.call(zoom);
+    },
+    createTooltips(eventRect) {
+      let dots = this.diagram.append("g").attr("class", "chart-dots");
       dots
         .append("circle")
         .attr("class", "chart-dot")
         .attr("r", 4);
-      let tooltips = mainChart.append("g").attr("class", "chart-tooltip");
+      let tooltips = this.diagram.append("g").attr("class", "chart-tooltip");
       tooltips
         .append("rect")
         .attr("x", -25)
@@ -657,35 +663,43 @@
         .attr("x", 8)
         .attr("y", 8);
 
-      let bisectDate = d3.bisector(d => d.date).left;
-      zoomRect
+      eventRect
         .on("mouseover", () => {
-          svg.select(".chart-dot").style("opacity", 1);
-          svg.select(".chart-tooltip").style("opacity", 1);
+          this.svg.select(".chart-dot").style("opacity", 1);
+          this.svg.select(".chart-tooltip").style("opacity", 1);
         })
         .on("mouseout", () => {
-          svg.select(".chart-dot").style("opacity", 0);
-          svg.select(".chart-tooltip").style("opacity", 0);
+          this.svg.select(".chart-dot").style("opacity", 0);
+          this.svg.select(".chart-tooltip").style("opacity", 0);
         })
         .on("mousemove", () => {
-          let x0 = x.invert(d3.mouse(document.querySelector(".zoom"))[0]),
-            i = bisectDate(this.waterlevels, x0, 1),
+          let x0 = this.scale.x.invert(
+              d3.mouse(document.querySelector(".zoom"))[0]
+            ),
+            i = d3.bisector(d => d.date).left(this.waterlevels, x0, 1),
             d0 = this.waterlevels[i - 1],
             d1 = this.waterlevels[i] || d0,
             d = x0 - d0.date > d1.date - x0 ? d1 : d0;
 
-          svg
+          this.svg
             .select(".chart-dot")
             .style("opacity", 1)
-            .attr("transform", `translate(${x(d.date)}, ${y(d.waterlevel)})`);
-          svg
+            .attr(
+              "transform",
+              `translate(${this.scale.x(d.date)}, ${this.scale.y(
+                d.waterlevel
+              )})`
+            );
+          this.svg
             .select(".chart-tooltip")
             .style("opacity", 1)
             .attr(
               "transform",
-              `translate(${x(d.date) - 25}, ${y(d.waterlevel) - 25})`
+              `translate(${this.scale.x(d.date) - 25}, ${this.scale.y(
+                d.waterlevel
+              ) - 25})`
             );
-          svg.select(".chart-tooltip text tspan:first-child").text(
+          this.svg.select(".chart-tooltip text tspan:first-child").text(
             d.date.toLocaleString([], {
               year: "2-digit",
               month: "2-digit",
@@ -694,18 +708,20 @@
               minute: "2-digit"
             })
           );
-          svg
+          this.svg
             .select(".chart-tooltip text tspan:last-child")
             .text(d.waterlevel + " cm");
         });
+    },
+    isNext(seconds) {
+      // helper to check whether points in the chart are "next to each other"
+      // for that they need to be exactly the specified amount of seconds apart.
+      return (prev, current) => current.date - prev.date === seconds * 1000;
     }
   },
   created() {
     window.addEventListener("resize", debounce(this.drawDiagram), 100);
   },
-  mounted() {
-    this.drawDiagram();
-  },
   updated() {
     this.drawDiagram();
   }