diff client/src/components/gauge/HydrologicalConditions.vue @ 2761:71e7237110ba

client: spuc8: prepared diagram
author Markus Kottlaender <markus@intevation.de>
date Thu, 21 Mar 2019 17:31:03 +0100
parents
children 2b79c0871138
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/components/gauge/HydrologicalConditions.vue	Thu Mar 21 17:31:03 2019 +0100
@@ -0,0 +1,394 @@
+<template>
+  <div
+    class="d-flex flex-fill justify-content-center align-items-center diagram-container"
+  >
+    <div v-if="!meanWaterlevels.length">
+      <translate>No data available.</translate>
+    </div>
+  </div>
+</template>
+
+<style lang="sass" scoped>
+.diagram-container
+  /deep/
+    .line
+      clip-path: url(#clip)
+
+    .hdc-line,
+    .ldc-line,
+    .mw-line
+      stroke-width: 1
+      fill: none
+      clip-path: url(#clip)
+    .hdc-line
+      stroke: red
+    .ldc-line
+      stroke: green
+    .mw-line
+      stroke: grey
+    .ref-waterlevel-label
+      font-size: 11px
+      fill: #999
+
+    .tick
+      line
+        stroke-dasharray: 5
+        stroke: #ccc
+
+    .zoom
+      cursor: move
+      fill: none
+      pointer-events: all
+    .brush
+      .selection
+        stroke: none
+        fill-opacity: 0.2
+      .handle
+        stroke: rgba($color-info, 0.5)
+        fill: rgba($color-info, 0.5)
+</style>
+
+<script>
+/* This is Free Software under GNU Affero General Public License v >= 3.0
+ * without warranty, see README.md and license for details.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ * License-Filename: LICENSES/AGPL-3.0.txt
+ *
+ * Copyright (C) 2018 by via donau
+ *   – Österreichische Wasserstraßen-Gesellschaft mbH
+ * Software engineering by Intevation GmbH
+ *
+ * Author(s):
+ * Markus Kottländer <markus.kottlaender@intevation.de>
+ */
+
+import { mapState, mapGetters } from "vuex";
+import * as d3Base from "d3";
+import debounce from "debounce";
+import { lineChunked } from "d3-line-chunked";
+import { startOfYear, endOfYear } from "date-fns";
+
+// 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/
+const d3 = Object.assign(d3Base, { lineChunked });
+
+export default {
+  computed: {
+    ...mapState("gauges", ["meanWaterlevels"]),
+    ...mapGetters("gauges", ["selectedGauge", "minMaxWaterlevelForDay"])
+  },
+  watch: {
+    meanWaterlevels() {
+      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
+      d3.select(".diagram-container svg").remove();
+
+      if (!this.selectedGauge || !this.meanWaterlevels.length) return;
+
+      // get HDC/LDC/MW of the gauge
+      let refWaterLevels = JSON.parse(
+        this.selectedGauge.properties.reference_water_levels
+      );
+
+      // CREATE SVG AND SET DIMENSIONS/MARGINS
+
+      let svgWidth = document.querySelector(".diagram-container").clientWidth;
+      let svgHeight = document.querySelector(".diagram-container").clientHeight;
+      let 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/LDC (+/- 1/8 HDC-LDC)
+      let WaterlevelMinMax = d3.extent(
+        [
+          ...this.meanWaterlevels,
+          {
+            waterlevel:
+              refWaterLevels.HDC + (refWaterLevels.HDC - refWaterLevels.LDC) / 8
+          },
+          {
+            waterlevel: Math.max(
+              refWaterLevels.LDC -
+                (refWaterLevels.HDC - refWaterLevels.LDC) / 8,
+              0
+            )
+          }
+        ],
+        d => d.waterlevel
+      );
+      // setting the min and max values for the diagram axes
+      let yearStart = startOfYear(new Date());
+      let yearEnd = endOfYear(new Date());
+      x.domain(d3.extent([yearStart, yearEnd]));
+      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)
+        .tickFormat(date => {
+          return (d3.timeSecond(date) < date
+            ? d3.timeFormat(".%L")
+            : d3.timeMinute(date) < date
+            ? d3.timeFormat(":%S")
+            : d3.timeHour(date) < date
+            ? d3.timeFormat("%I:%M")
+            : d3.timeDay(date) < date
+            ? d3.timeFormat("%I %p")
+            : d3.timeMonth(date) < date
+            ? d3.timeWeek(date) < date
+              ? d3.timeFormat("%a %d")
+              : d3.timeFormat("%b %d")
+            : d3.timeFormat("%B"))(date);
+        });
+      let xAxis2 = d3.axisBottom(x2).tickFormat(d3.timeFormat("%B"));
+      let yAxis = d3
+        .axisRight(y)
+        .tickSizeInner(width)
+        .tickSizeOuter(0);
+
+      // PREPARING CHART FUNCTIONS
+
+      // points are "next to each other" when they are exactly 1 day apart
+      const isNext = (prev, current) =>
+        current.date - prev.date === 24 * 60 * 60 * 1000;
+
+      // 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 });
+      // 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 });
+      // hdc/ldc/mw
+      let refWaterlevelLine = d3
+        .line()
+        .x(d => x(d.x))
+        .y(d => y(d.y));
+
+      // DRAWING MAINCHART
+
+      // define visible chart area
+      // everything outside this area will be hidden (clipped)
+      svg
+        .append("defs")
+        .append("clipPath")
+        .attr("id", "clip")
+        .append("rect")
+        .attr("width", width)
+        .attr("height", mainHeight);
+
+      let mainChart = svg
+        .append("g")
+        .attr("class", "main")
+        .attr("transform", `translate(${mainMargin.left}, ${mainMargin.top})`);
+
+      // axes
+      mainChart
+        .append("g")
+        .attr("class", "axis--x")
+        .attr("transform", `translate(0, ${mainHeight})`)
+        .call(xAxis)
+        .selectAll(".tick text")
+        .attr("y", 15);
+      mainChart // label
+        .append("text")
+        .text(this.$gettext("Waterlevel [cm]"))
+        .attr("text-anchor", "middle")
+        .attr("transform", `translate(-45, ${mainHeight / 2}) rotate(-90)`);
+      mainChart
+        .append("g")
+        .call(yAxis)
+        .selectAll(".tick text")
+        .attr("x", -25);
+
+      // reference waterlevels
+      // HDC
+      mainChart
+        .append("path")
+        .datum([
+          { x: 0, y: refWaterLevels.HDC },
+          { x: yearEnd, y: refWaterLevels.HDC }
+        ])
+        .attr("class", "hdc-line")
+        .attr("d", refWaterlevelLine);
+      mainChart // label
+        .append("text")
+        .text("HDC")
+        .attr("class", "ref-waterlevel-label")
+        .attr("x", x(yearEnd) - 20)
+        .attr("y", y(refWaterLevels.HDC) - 3);
+      // LDC
+      mainChart
+        .append("path")
+        .datum([
+          { x: 0, y: refWaterLevels.LDC },
+          { x: yearEnd, y: refWaterLevels.LDC }
+        ])
+        .attr("class", "ldc-line")
+        .attr("d", refWaterlevelLine);
+      mainChart // label
+        .append("text")
+        .text("LDC")
+        .attr("class", "ref-waterlevel-label")
+        .attr("x", x(yearEnd) - 20)
+        .attr("y", y(refWaterLevels.LDC) - 3);
+      // MW
+      mainChart
+        .append("path")
+        .datum([
+          { x: 0, y: refWaterLevels.MW },
+          { x: yearEnd, y: refWaterLevels.MW }
+        ])
+        .attr("class", "mw-line")
+        .attr("d", refWaterlevelLine);
+      mainChart // label
+        .append("text")
+        .text("MW")
+        .attr("class", "ref-waterlevel-label")
+        .attr("x", x(yearEnd) - 20)
+        .attr("y", y(refWaterLevels.MW) - 3);
+
+      // waterlevel chart
+      mainChart
+        .append("g")
+        .attr("class", "line")
+        .datum([])
+        .call(mainLineChart);
+
+      // DRAWING NAVCHART
+
+      let navChart = svg
+        .append("g")
+        .attr("class", "nav")
+        .attr("transform", `translate(${navMargin.left}, ${navMargin.top})`);
+
+      // axis (nav chart only has x-axis)
+      navChart
+        .append("g")
+        .attr("class", "axis axis--x")
+        .attr("transform", `translate(0, ${navHeight})`)
+        .call(xAxis2);
+
+      // waterlevel chart
+      navChart
+        .append("g")
+        .attr("class", "line")
+        .datum([])
+        .call(navLineChart);
+
+      // INTERACTIVITY
+
+      // selecting time period in nav chart
+      let 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));
+          mainChart.select(".line").call(mainLineChart);
+          mainChart
+            .select(".axis--x")
+            .call(xAxis)
+            .selectAll(".tick text")
+            .attr("y", 15);
+          svg
+            .select(".zoom")
+            .call(
+              zoom.transform,
+              d3.zoomIdentity.scale(width / (s[1] - s[0])).translate(-s[0], 0)
+            );
+        });
+
+      // zooming with mousewheel in main chart
+      let 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());
+          mainChart.select(".line").call(mainLineChart);
+          mainChart
+            .select(".axis--x")
+            .call(xAxis)
+            .selectAll(".tick text")
+            .attr("y", 15);
+          navChart
+            .select(".brush")
+            .call(brush.move, x.range().map(t.invertX, t));
+        });
+
+      navChart
+        .append("g")
+        .attr("class", "brush")
+        .call(brush)
+        .call(brush.move, x.range());
+
+      svg
+        .append("rect")
+        .attr("class", "zoom")
+        .attr("width", width)
+        .attr("height", mainHeight)
+        .attr("transform", `translate(${mainMargin.left}, ${mainMargin.top})`)
+        .call(zoom);
+    }
+  },
+  created() {
+    window.addEventListener("resize", debounce(this.drawDiagram), 100);
+  },
+  mounted() {
+    this.drawDiagram();
+  },
+  updated() {
+    this.drawDiagram();
+  }
+};
+</script>