changeset 3885:d42158f77841

waterlevel: factor out side-effects from diagram rendering
author Thomas Junk <thomas.junk@intevation.de>
date Wed, 10 Jul 2019 12:08:12 +0200
parents db24b4347604
children 9fa9a485c182
files client/src/components/gauge/Waterlevel.vue
diffstat 1 files changed, 229 insertions(+), 225 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/components/gauge/Waterlevel.vue	Wed Jul 10 12:00:58 2019 +0200
+++ b/client/src/components/gauge/Waterlevel.vue	Wed Jul 10 12:08:12 2019 +0200
@@ -127,13 +127,6 @@
   data() {
     return {
       containerId: "waterlevel-diagram-container",
-      svg: null,
-      diagram: null,
-      navigation: null,
-      dimensions: null,
-      extent: null,
-      scale: null,
-      axes: null,
       form: {
         template: null
       },
@@ -291,34 +284,44 @@
           });
       }
     },
-    addDiagram(position, offset) {
+    addDiagram(position, offset, width, height) {
       let x = offset.x,
         y = offset.y;
-
+      const svgWidth = 1550;
+      const svgHeight = 400;
+      // check if there are tow diagrams on the screen
+      // draw the diagram in a separated html element to get the full size
+      const offScreen = document.querySelector("#offScreen");
+      offScreen.style.width = `${svgWidth}px`;
+      offScreen.style.height = `${svgHeight}px`;
+      this.renderTo({
+        element: offScreen,
+        dimensions: this.getDimensions({
+          svgWidth: svgWidth,
+          svgHeight: svgHeight
+        })
+      });
+      var svg = offScreen.querySelector("svg");
       // use default width,height if they are missing in the template definition
-
+      if (!width) {
+        width = this.templateData.properties.paperSize === "a3" ? 380 : 290;
+      }
+      if (!height) {
+        height = this.templateData.properties.paperSize === "a3" ? 130 : 100;
+      }
       if (["topright", "bottomright"].indexOf(position) !== -1) {
-        x =
-          this.pdf.width -
-          offset.x -
-          document.getElementById(this.containerId).clientWidth;
+        x = this.pdf.width - offset.x - width;
       }
       if (["bottomright", "bottomleft"].indexOf(position) !== -1) {
-        y =
-          this.pdf.height -
-          offset.y -
-          document.getElementById(this.containerId).clientHeight;
+        y = this.pdf.height - offset.y - height;
       }
-
-      let svg = document.getElementById(this.containerId).firstElementChild;
       svg2pdf(svg, this.pdf.doc, {
         xOffset: x,
         yOffset: y,
         // TODO depend on the size and aspect ration on paper
         scale: this.templateData.properties.paperSize === "a3" ? 0.45 : 0.18
       });
-
-      this.containerId = "waterlevel-diagram-container";
+      offScreen.removeChild(svg);
     },
     // Diagram legend
     addDiagramLegend(position, offset, color) {
@@ -354,7 +357,15 @@
       // remove old diagram and exit if necessary data is missing
       d3.select("#" + this.containerId + " svg").remove();
       if (!this.selectedGauge || !this.waterlevels.length) return;
-
+      this.renderTo({
+        element: `#${this.containerId}`,
+        dimensions: this.getDimensions({
+          svgWidth: document.querySelector("#" + this.containerId).clientWidth,
+          svgHeight: document.querySelector("#" + this.containerId).clientHeight
+        })
+      });
+    },
+    renderTo({ element, dimensions }) {
       // PREPARE HELPERS
 
       // HDC/LDC/MW for the selected gauge
@@ -362,68 +373,63 @@
         this.selectedGauge.properties.reference_water_levels
       );
 
-      // dimensions (widths, heights, margins)
-      this.dimensions = this.getDimensions();
-
       // get min/max values for date and waterlevel axis
-      this.extent = this.getExtent(refWaterLevels);
+      const extent = this.getExtent(refWaterLevels);
 
       // scaling helpers
-      this.scale = this.getScale();
+      const scale = this.getScale({ dimensions, extent });
 
       // creating the axes based on the scales
-      this.axes = {
+      const axes = {
         x: d3
-          .axisTop(this.scale.x)
-          .tickSizeInner(this.dimensions.mainHeight)
+          .axisTop(scale.x)
+          .tickSizeInner(dimensions.mainHeight)
           .tickSizeOuter(0),
         y: d3
-          .axisRight(this.scale.y)
-          .tickSizeInner(this.dimensions.width)
+          .axisRight(scale.y)
+          .tickSizeInner(dimensions.width)
           .tickSizeOuter(0)
           .tickFormat(d => this.$options.filters.waterlevel(d)),
-        x2: d3.axisBottom(this.scale.x2)
+        x2: d3.axisBottom(scale.x2)
       };
 
       // DRAW DIAGRAM/NAVIGATION AREAS
 
       // create svg
-      this.svg = d3
-        .select("#" + this.containerId)
+      const svg = d3
+        .select(element)
         .append("svg")
         .attr("width", "100%")
         .attr("height", "100%");
 
       // create container for main diagram
-      this.diagram = this.svg
+      const diagram = svg
         .append("g")
         .attr("class", "main")
         .attr(
           "transform",
-          `translate(${this.dimensions.mainMargin.left}, ${
-            this.dimensions.mainMargin.top
+          `translate(${dimensions.mainMargin.left}, ${
+            dimensions.mainMargin.top
           })`
         );
 
       // create container for navigation diagram
-      this.navigation = this.svg
+      const navigation = svg
         .append("g")
         .attr("class", "nav")
         .attr(
           "transform",
-          `translate(${this.dimensions.navMargin.left}, ${
-            this.dimensions.navMargin.top
-          })`
+          `translate(${dimensions.navMargin.left}, ${dimensions.navMargin.top})`
         );
 
       // define visible area, everything outside this area will be hidden
-      this.svg
+      svg
         .append("defs")
         .append("clipPath")
         .attr("id", "waterlevel-clip")
         .append("rect")
-        .attr("width", this.dimensions.width)
-        .attr("height", this.dimensions.mainHeight);
+        .attr("width", dimensions.width)
+        .attr("height", dimensions.mainHeight);
 
       // DRAW DIAGRAM PARTS
 
@@ -433,144 +439,155 @@
       const updaters = [];
 
       // draw (order matters)
-      updaters.push(this.drawAxes());
-      updaters.push(this.drawWaterlevelChart());
+      updaters.push(this.drawAxes({ diagram, dimensions, axes, navigation }));
+      updaters.push(this.drawWaterlevelChart({ scale, diagram }));
       if (this.hasPredictions) {
-        updaters.push(this.drawPredictionAreas());
+        updaters.push(this.drawPredictionAreas({ scale, diagram, navigation }));
       }
-      updaters.push(this.drawNowLines());
+      updaters.push(this.drawNowLines({ scale, extent, diagram, navigation }));
 
       // static, don't need updater
-      this.drawNavigationChart();
-      this.drawRefLines(refWaterLevels);
+      this.drawNavigationChart({ scale, navigation });
+      this.drawRefLines({ refWaterLevels, diagram, scale, dimensions, extent });
 
-      updaters.push(this.drawNashSutcliffe(72));
-      updaters.push(this.drawNashSutcliffe(48));
-      updaters.push(this.drawNashSutcliffe(24));
+      updaters.push(
+        this.drawNashSutcliffe({ hours: 72, diagram, scale, dimensions })
+      );
+      updaters.push(
+        this.drawNashSutcliffe({ hours: 48, diagram, scale, dimensions })
+      );
+      updaters.push(
+        this.drawNashSutcliffe({ hours: 24, diagram, scale, dimensions })
+      );
 
       // INTERACTIONS
 
       // create rectanlge on the main chart area to capture mouse events
-      const eventRect = this.svg
+      const eventRect = svg
         .append("rect")
         .attr("id", "zoom-waterlevels")
         .attr("class", "zoom")
-        .attr("width", this.dimensions.width)
-        .attr("height", this.dimensions.mainHeight)
+        .attr("width", dimensions.width)
+        .attr("height", dimensions.mainHeight)
         .attr(
           "transform",
-          `translate(${this.dimensions.mainMargin.left}, ${
-            this.dimensions.mainMargin.top
+          `translate(${dimensions.mainMargin.left}, ${
+            dimensions.mainMargin.top
           })`
         );
 
-      this.createZoom(updaters, eventRect);
-      this.createTooltips(eventRect);
-      this.setInlineStyles();
+      this.createZoom({
+        updaters,
+        eventRect,
+        dimensions,
+        scale,
+        navigation,
+        svg
+      });
+      this.createTooltips({ eventRect, diagram, scale, dimensions });
+      this.setInlineStyles(svg);
     },
     //set the styles of the diagrams to include them in the pdf
-    setInlineStyles() {
-      this.svg
+    setInlineStyles(svg) {
+      svg
         .selectAll(".line")
         .attr("clip-path", "url(#waterlevel-clip)")
         .selectAll("path")
         .attr("stroke", "steelblue")
         .attr("stroke-width", 2)
         .attr("fill", "none");
-      this.svg
+      svg
         .selectAll(".line")
         .selectAll("path.d3-line-chunked-chunk-gap")
         .attr("stroke-opacity", 0);
-      this.svg
+      svg
         .selectAll(".line")
         .selectAll("circle")
         .attr("fill", "steelblue")
         .attr("stroke-width", 0);
-      this.svg
+      svg
         .selectAll(".line")
         .selectAll("circle.d3-line-chunked-chunk-predicted-point")
         .attr("fill-opacity", 0.6);
 
-      this.svg
+      svg
         .selectAll(".hdc-line, .mw-line, .ldc-line, .rn-line")
         .attr("stroke-width", 1)
         .attr("fill", "none")
         .attr("clip-path", "url(#waterlevel-clip)");
-      this.svg.selectAll(".hdc-line").attr("stroke", "red");
-      this.svg.selectAll(".ldc-line").attr("stroke", "green");
-      this.svg.selectAll(".mw-line").attr("stroke", "grey");
-      this.svg.selectAll(".rn-line").attr("stroke", "grey");
-      this.svg
+      svg.selectAll(".hdc-line").attr("stroke", "red");
+      svg.selectAll(".ldc-line").attr("stroke", "green");
+      svg.selectAll(".mw-line").attr("stroke", "grey");
+      svg.selectAll(".rn-line").attr("stroke", "grey");
+      svg
         .selectAll(".ref-waterlevel-label")
         .style("font-size", "10px")
         .attr("fill", "black");
-      this.svg
+      svg
         .selectAll(".ref-waterlevel-label-background")
         .attr("fill", "rgb(255, 255, 255)")
         .attr("fill-opacity", 0.6);
-      this.svg
+      svg
         .selectAll(".hdc-ldc-area")
         .attr("fill", "rgb(0, 255, 0)")
         .attr("fill-opacity", 0.1);
-      this.svg
+      svg
         .selectAll(".now-line")
         .attr("stroke", "#999")
         .attr("stroke-width", 1)
         .attr("stroke-dasharray", "5, 5")
         .attr("clip-path", "url(#waterlevel-clip)");
-      this.svg
+      svg
         .selectAll(".now-line-label")
         .attr("font-size", "11px")
         .attr("fill", "#999");
-      this.svg
+      svg
         .selectAll(".prediction-area")
         .attr("fill", "steelblue")
         .attr("fill-opacity", 0.2)
         .attr("clip-path", "url(#waterlevel-clip)");
-      this.svg
+      svg
         .selectAll("path.nash-sutcliffe")
         .attr("fill", "none")
         .attr("stroke", "darkgrey")
         .attr("stroke-width", 1)
         .attr("clip-path", "url(#waterlevel-clip)");
-      this.svg
+      svg
         .selectAll("path.nash-sutcliffe.ns72")
         .attr("fill", "rgb(255, 255, 255)")
         .attr("fill-opacity", 0.5);
-      this.svg
+      svg
         .selectAll("text.nash-sutcliffe")
         .style("font-size", "10px")
         .attr("clip-path", "url(#waterlevel-clip)")
         .selectAll("tspan:last-child, tspan:first-child")
         .attr("fill", "#555");
-      this.svg
+      svg
         .selectAll(".tick line")
         .attr("stroke-dasharray", 5)
         .attr("stroke", "#ccc");
-      this.svg.selectAll(".tick text").attr("fill", "black");
-      this.svg.selectAll(".domain").attr("stroke", "black");
-      this.svg
+      svg.selectAll(".tick text").attr("fill", "black");
+      svg.selectAll(".domain").attr("stroke", "black");
+      svg
         .selectAll(".domain")
         .attr("stroke", "black")
         .attr("fill", "none");
-      this.svg
+      svg
         .selectAll(".zoom")
         .attr("cursor", "move")
         .attr("fill", "none")
         .attr("pointer-events", "all");
-      this.svg
+      svg
         .selectAll(".brush .selection")
         .attr("stroke", "none")
         .attr("fill-opacity", 0.2);
-      this.svg
+      svg
         .selectAll(".brush .handle")
         .attr("stroke", "rgba(23, 162, 184, 0.5)")
         .attr("fill", "rgba(23, 162, 184, 0.5)");
-      this.svg.selectAll(".brush .overlay").attr("fill", "none");
-      this.svg
-        .selectAll(".chart-dots")
-        .attr("clip-path", "url(#waterlevel-clip)");
-      this.svg
+      svg.selectAll(".brush .overlay").attr("fill", "none");
+      svg.selectAll(".chart-dots").attr("clip-path", "url(#waterlevel-clip)");
+      svg
         .selectAll(".chart-dots .chart-dot")
         .attr("fill", "steelblue")
         .attr("stroke", "steelblue")
@@ -578,26 +595,21 @@
         .style("pointer-events", "none")
         .transition()
         .attr("fill-opacity", "0.1s");
-      this.svg
+      svg
         .selectAll(".chart-tooltip")
         .attr("fill-opacity", 0)
         .transition()
         .attr("fill-opacity", "0.3s");
-      this.svg
+      svg
         .selectAll(".chart-tooltip rect")
         .attr("fill", "#fff")
         .attr("stroke", "#ccc");
-      this.svg
+      svg
         .selectAll(".chart-tooltip text")
         .attr("fill", "666")
         .style("font-size", "0.8em");
     },
-    getDimensions() {
-      // dimensions and margins
-      const svgWidth = document.querySelector("#" + this.containerId)
-        .clientWidth;
-      const svgHeight = document.querySelector("#" + this.containerId)
-        .clientHeight;
+    getDimensions({ svgWidth, svgHeight }) {
       const mainMargin = { top: 20, right: 20, bottom: 110, left: 80 };
       const navMargin = {
         top: svgHeight - mainMargin.top - 65,
@@ -640,61 +652,61 @@
         )
       };
     },
-    getScale() {
+    getScale({ dimensions, extent }) {
       // 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]);
+      const x = d3.scaleTime().range([0, dimensions.width]);
+      const y = d3.scaleLinear().range([dimensions.mainHeight, 0]);
+      const x2 = d3.scaleTime().range([0, dimensions.width]);
+      const y2 = d3.scaleLinear().range([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);
+      x.domain(d3.extent(extent.date));
+      y.domain(extent.waterlevel);
       x2.domain(x.domain());
       y2.domain(y.domain());
 
       return { x, y, x2, y2 };
     },
-    drawAxes() {
-      this.diagram
+    drawAxes({ diagram, dimensions, axes, navigation }) {
+      diagram
         .append("g")
         .attr("class", "axis--x")
-        .attr("transform", `translate(0, ${this.dimensions.mainHeight})`)
-        .call(this.axes.x)
+        .attr("transform", `translate(0, ${dimensions.mainHeight})`)
+        .call(axes.x)
         .selectAll(".tick text")
         .attr("y", 15);
-      this.diagram // label
+      diagram // label
         .append("text")
         .text(this.$gettext("Waterlevel [m]"))
         .attr("text-anchor", "middle")
         .attr(
           "transform",
-          `translate(-45, ${this.dimensions.mainHeight / 2}) rotate(-90)`
+          `translate(-45, ${dimensions.mainHeight / 2}) rotate(-90)`
         );
-      this.diagram
+      diagram
         .append("g")
-        .call(this.axes.y)
+        .call(axes.y)
         .selectAll(".tick text")
         .attr("x", -25);
 
-      this.navigation
+      navigation
         .append("g")
         .attr("class", "axis axis--x")
-        .attr("transform", `translate(0, ${this.dimensions.navHeight})`)
-        .call(this.axes.x2);
+        .attr("transform", `translate(0, ${dimensions.navHeight})`)
+        .call(axes.x2);
 
       return () => {
-        this.diagram
+        diagram
           .select(".axis--x")
-          .call(this.axes.x)
+          .call(axes.x)
           .selectAll(".tick text")
           .attr("y", 15);
       };
     },
-    drawWaterlevelChart() {
+    drawWaterlevelChart({ scale, diagram }) {
       const waterlevelChartDrawer = () => {
-        let domainLeft = new Date(this.scale.x.domain()[0].getTime());
-        let domainRight = new Date(this.scale.x.domain()[1].getTime());
+        let domainLeft = new Date(scale.x.domain()[0].getTime());
+        let domainRight = new Date(scale.x.domain()[1].getTime());
         domainLeft.setDate(domainLeft.getDate() - 1);
         domainRight.setDate(domainRight.getDate() + 1);
 
@@ -702,10 +714,10 @@
           .lineChunked()
           // render only data points that are visible in the current scale
           .defined(d => d.date > domainLeft && d.date < domainRight)
-          .x(d => this.scale.x(d.date))
-          .y(d => this.scale.y(d.waterlevel))
+          .x(d => scale.x(d.date))
+          .y(d => scale.y(d.waterlevel))
           .curve(d3.curveLinear)
-          .isNext(this.isNext())
+          .isNext(this.isNext(scale))
           .pointAttrs({ r: 1.7 });
         // to avoid creating empty clip-path element
         if (this.hasPredictions) {
@@ -716,23 +728,23 @@
         return lineChunked;
       };
 
-      this.diagram
+      diagram
         .append("g")
         .attr("class", "line")
         .datum(this.waterlevels)
         .call(waterlevelChartDrawer());
 
       return () => {
-        this.diagram.select(".line").call(waterlevelChartDrawer());
+        diagram.select(".line").call(waterlevelChartDrawer());
       };
     },
-    drawNavigationChart() {
+    drawNavigationChart({ scale, navigation }) {
       let lineChunked = d3
         .lineChunked()
-        .x(d => this.scale.x2(d.date))
-        .y(d => this.scale.y2(d.waterlevel))
+        .x(d => scale.x2(d.date))
+        .y(d => scale.y2(d.waterlevel))
         .curve(d3.curveLinear)
-        .isNext(this.isNext())
+        .isNext(this.isNext(scale))
         .pointAttrs({ r: 1.7 });
       // to avoid creating empty clip-path element
       if (this.hasPredictions) {
@@ -740,37 +752,37 @@
           .chunk(d => (d.predicted ? "predicted" : "line"))
           .chunkDefinitions({ predicted: {} });
       }
-      this.navigation
+      navigation
         .append("g")
         .attr("class", "line")
         .datum(this.waterlevels)
         .call(lineChunked);
     },
-    drawNowLines() {
+    drawNowLines({ scale, extent, diagram, navigation }) {
       const nowLine = d3
         .line()
-        .x(d => this.scale.x(d.x))
-        .y(d => this.scale.y(d.y));
+        .x(d => scale.x(d.x))
+        .y(d => scale.y(d.y));
 
       const nowLabel = selection => {
         selection.attr(
           "transform",
-          `translate(${this.scale.x(new Date())}, ${this.scale.y(
-            this.extent.waterlevel[1] - 16
+          `translate(${scale.x(new Date())}, ${scale.y(
+            extent.waterlevel[1] - 16
           )})`
         );
       };
 
       // draw in main
-      this.diagram
+      diagram
         .append("path")
         .datum([
-          { x: new Date(), y: this.extent.waterlevel[0] },
-          { x: new Date(), y: this.extent.waterlevel[1] - 20 }
+          { x: new Date(), y: extent.waterlevel[0] },
+          { x: new Date(), y: extent.waterlevel[1] - 20 }
         ])
         .attr("class", "now-line")
         .attr("d", nowLine);
-      this.diagram // label
+      diagram // label
         .append("text")
         .text(this.$gettext("Now"))
         .attr("class", "now-line-label")
@@ -778,87 +790,87 @@
         .call(nowLabel);
 
       // draw in nav
-      this.navigation
+      navigation
         .append("path")
         .datum([
-          { x: new Date(), y: this.extent.waterlevel[0] },
-          { x: new Date(), y: this.extent.waterlevel[1] - 20 }
+          { x: new Date(), y: extent.waterlevel[0] },
+          { x: new Date(), y: 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))
+            .x(d => scale.x2(d.x))
+            .y(d => scale.y2(d.y))
         );
 
       return () => {
-        this.diagram.select(".now-line").attr("d", nowLine);
-        this.diagram.select(".now-line-label").call(nowLabel);
+        diagram.select(".now-line").attr("d", nowLine);
+        diagram.select(".now-line-label").call(nowLabel);
       };
     },
-    drawPredictionAreas() {
+    drawPredictionAreas({ scale, diagram, navigation }) {
       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));
+          .x(d => scale[isNav ? "x2" : "x"](d.date))
+          .y0(d => scale[isNav ? "y2" : "y"](d.min))
+          .y1(d => scale[isNav ? "y2" : "y"](d.max));
 
-      this.diagram
+      diagram
         .append("path")
         .datum(this.waterlevels)
         .attr("class", "prediction-area")
         .attr("d", predictionArea());
 
-      this.navigation
+      navigation
         .append("path")
         .datum(this.waterlevels)
         .attr("class", "prediction-area")
         .attr("d", predictionArea(true));
 
       return () => {
-        this.diagram.select(".prediction-area").attr("d", predictionArea());
+        diagram.select(".prediction-area").attr("d", predictionArea());
       };
     },
-    drawRefLines(refWaterLevels) {
+    drawRefLines({ refWaterLevels, diagram, scale, dimensions, extent }) {
       // filling area between HDC and LDC
-      this.diagram
+      diagram
         .append("rect")
         .attr("class", "hdc-ldc-area")
         .attr("x", 0)
-        .attr("y", this.scale.y(refWaterLevels.HDC))
-        .attr("width", this.dimensions.width)
+        .attr("y", scale.y(refWaterLevels.HDC))
+        .attr("width", dimensions.width)
         .attr(
           "height",
-          this.scale.y(refWaterLevels.LDC) - this.scale.y(refWaterLevels.HDC)
+          scale.y(refWaterLevels.LDC) - scale.y(refWaterLevels.HDC)
         );
 
       const refWaterlevelLine = d3
         .line()
-        .x(d => this.scale.x(d.x))
-        .y(d => this.scale.y(d.y));
+        .x(d => scale.x(d.x))
+        .y(d => scale.y(d.y));
 
       for (let ref in refWaterLevels) {
         if (refWaterLevels[ref]) {
-          this.diagram
+          diagram
             .append("path")
             .datum([
               { x: 0, y: refWaterLevels[ref] },
-              { x: this.extent.date[1], y: refWaterLevels[ref] }
+              { x: extent.date[1], y: refWaterLevels[ref] }
             ])
             .attr("class", ref.toLowerCase() + "-line")
             .attr("d", refWaterlevelLine);
-          this.diagram // label
+          diagram // label
             .append("rect")
             .attr("class", "ref-waterlevel-label-background")
             .attr("x", 1)
-            .attr("y", this.scale.y(refWaterLevels[ref]) - 13)
+            .attr("y", scale.y(refWaterLevels[ref]) - 13)
             .attr("width", 55)
             .attr("height", 12);
-          this.diagram
+          diagram
             .append("text")
             .text(
               `${ref} (${this.$options.filters.waterlevel(
@@ -867,47 +879,45 @@
             )
             .attr("class", "ref-waterlevel-label")
             .attr("x", 5)
-            .attr("y", this.scale.y(refWaterLevels[ref]) - 3);
+            .attr("y", scale.y(refWaterLevels[ref]) - 3);
         }
       }
     },
-    drawNashSutcliffe(hours) {
+    drawNashSutcliffe({ hours, diagram, scale, dimensions }) {
       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 => {
         // show/hide boxes depending on scale of chart (hide if > 90 days)
-        this.diagram
+        diagram
           .selectAll("path.nash-sutcliffe")
           .attr(
             "stroke-opacity",
-            this.scale.x.domain()[1] - this.scale.x.domain()[0] > 90 * 86400000
-              ? 0
-              : 1
+            scale.x.domain()[1] - scale.x.domain()[0] > 90 * 86400000 ? 0 : 1
           );
 
         return d3
           .area()
-          .x(d => this.scale.x(d))
-          .y0(() => this.dimensions.mainHeight + 0.5)
-          .y1(() => this.dimensions.mainHeight - 15 * (hours / 24));
+          .x(d => scale.x(d))
+          .y0(() => dimensions.mainHeight + 0.5)
+          .y1(() => dimensions.mainHeight - 15 * (hours / 24));
       };
 
       const nashSutcliffeLabel = (label, date, hours) => {
         let days = hours / 24;
         label
-          .attr("x", Math.min(this.scale.x(date), this.dimensions.width) - 4)
-          .attr("y", this.dimensions.mainHeight - (15 * days + 0.5) + 12);
+          .attr("x", Math.min(scale.x(date), dimensions.width) - 4)
+          .attr("y", dimensions.mainHeight - (15 * days + 0.5) + 12);
       };
 
       if (coeff.samples) {
-        this.diagram
+        diagram
           .append("path")
           .datum([dateStart, dateNow])
           .attr("class", "nash-sutcliffe ns" + hours)
           .attr("d", nashSutcliffeBox(hours));
-        this.diagram
+        diagram
           .append("text")
           .attr("class", "nash-sutcliffe ns" + hours)
           .attr("text-anchor", "end")
@@ -927,42 +937,39 @@
       }
 
       return () => {
-        this.diagram
+        diagram
           .select("path.nash-sutcliffe.ns" + hours)
           .attr("d", nashSutcliffeBox(hours));
-        this.diagram
+        diagram
           .select("text.nash-sutcliffe.ns" + hours)
           .call(nashSutcliffeLabel, dateNow, hours);
       };
     },
-    createZoom(updaters, eventRect) {
+    createZoom({ updaters, eventRect, dimensions, scale, navigation, svg }) {
       const brush = d3
         .brushX()
         .handleSize(4)
-        .extent([[0, 0], [this.dimensions.width, this.dimensions.navHeight]]);
+        .extent([[0, 0], [dimensions.width, dimensions.navHeight]]);
 
       const zoom = d3
         .zoom()
         .scaleExtent([1, Infinity])
-        .translateExtent([
-          [0, 0],
-          [this.dimensions.width, this.dimensions.mainHeight]
-        ])
-        .extent([[0, 0], [this.dimensions.width, this.dimensions.mainHeight]]);
+        .translateExtent([[0, 0], [dimensions.width, dimensions.mainHeight]])
+        .extent([[0, 0], [dimensions.width, dimensions.mainHeight]]);
 
       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));
+        let s = d3.event.selection || scale.x2.range();
+        scale.x.domain(s.map(scale.x2.invert, scale.x2));
         updaters.forEach(u => u && u());
-        this.setInlineStyles();
-        this.svg
+        this.setInlineStyles(svg);
+        svg
           .select(".zoom")
           .call(
             zoom.transform,
             d3.zoomIdentity
-              .scale(this.dimensions.width / (s[1] - s[0]))
+              .scale(dimensions.width / (s[1] - s[0]))
               .translate(-s[0], 0)
           );
       });
@@ -971,29 +978,29 @@
         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());
+        scale.x.domain(t.rescaleX(scale.x2).domain());
         updaters.forEach(u => u && u());
-        this.setInlineStyles();
-        this.navigation
+        this.setInlineStyles(svg);
+        navigation
           .select(".brush")
-          .call(brush.move, this.scale.x.range().map(t.invertX, t));
+          .call(brush.move, 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);
+        svg.select(".chart-dot").style("opacity", 0);
+        svg.select(".chart-tooltip").style("opacity", 0);
       });
 
-      this.navigation
+      navigation
         .append("g")
         .attr("class", "brush")
         .call(brush)
-        .call(brush.move, this.scale.x.range());
+        .call(brush.move, scale.x.range());
 
       eventRect.call(zoom);
     },
-    createTooltips(eventRect) {
+    createTooltips({ eventRect, diagram, scale, dimensions }) {
       // create clippable container for the dot
-      this.diagram
+      diagram
         .append("g")
         .attr("class", "chart-dots")
         .append("circle")
@@ -1001,7 +1008,7 @@
         .attr("r", 4);
 
       // create container for the tooltip
-      const tooltip = this.diagram.append("g").attr("class", "chart-tooltip");
+      const tooltip = diagram.append("g").attr("class", "chart-tooltip");
       tooltip
         .append("rect")
         .attr("rx", "0.25em")
@@ -1019,16 +1026,16 @@
 
       eventRect
         .on("mouseover", () => {
-          this.diagram.select(".chart-dot").style("opacity", 1);
-          this.diagram.select(".chart-tooltip").style("opacity", 1);
+          diagram.select(".chart-dot").style("opacity", 1);
+          diagram.select(".chart-tooltip").style("opacity", 1);
         })
         .on("mouseout", () => {
-          this.diagram.select(".chart-dot").style("opacity", 0);
-          this.diagram.select(".chart-tooltip").style("opacity", 0);
+          diagram.select(".chart-dot").style("opacity", 0);
+          diagram.select(".chart-tooltip").style("opacity", 0);
         })
         .on("mousemove", () => {
           // find data point closest to mouse
-          const x0 = this.scale.x.invert(
+          const x0 = scale.x.invert(
               d3.mouse(document.getElementById("zoom-waterlevels"))[0]
             ),
             i = d3.bisector(d => d.date).left(this.waterlevels, x0, 1),
@@ -1037,12 +1044,12 @@
             d = x0 - d0.date > d1.date - x0 ? d1 : d0;
 
           const coords = {
-            x: this.scale.x(d.date),
-            y: this.scale.y(d.waterlevel)
+            x: scale.x(d.date),
+            y: scale.y(d.waterlevel)
           };
 
           // position the dot
-          this.diagram
+          diagram
             .select(".chart-dot")
             .style("opacity", 1)
             .attr("transform", `translate(${coords.x}, ${coords.y})`);
@@ -1106,14 +1113,14 @@
           // get text dimensions
           const textBBox = tooltipText.node().getBBox();
 
-          this.diagram
+          diagram
             .selectAll(".chart-tooltip text tspan")
             .attr("x", textBBox.width / 2 + tooltipPadding)
             .attr("y", tooltipPadding);
 
           // position and scale tooltip box
           const xMax =
-            this.dimensions.width -
+            dimensions.width -
             (textBBox.width + diagramPadding + tooltipPadding * 2);
           const tooltipX = Math.max(
             diagramPadding,
@@ -1124,7 +1131,7 @@
             tooltipY = coords.y + 10;
           }
 
-          this.diagram
+          diagram
             .select(".chart-tooltip")
             .style("opacity", 1)
             .attr("transform", `translate(${tooltipX}, ${tooltipY})`)
@@ -1133,17 +1140,14 @@
             .attr("height", textBBox.height + tooltipPadding * 2);
         });
     },
-    isNext() {
+    isNext(scale) {
       // Check whether points in the chart can be considered "next to each other".
       // For that they need to be exactly 15 minutes apart (for automatically
       // imported gauge measurements). If the chart shows more than 15 days then
       // 1 hour is also valid (for approved gauge measurements).
       return (prev, current) => {
         let difference = (current.date - prev.date) / 1000;
-        if (
-          (this.scale.x.domain()[1] - this.scale.x.domain()[0]) / 86400000 >
-          15
-        )
+        if ((scale.x.domain()[1] - scale.x.domain()[0]) / 86400000 > 15)
           return [900, 3600].includes(difference);
         return difference === 900;
       };