changeset 2761:71e7237110ba

client: spuc8: prepared diagram
author Markus Kottlaender <markus@intevation.de>
date Thu, 21 Mar 2019 17:31:03 +0100
parents c6fba10926cc
children f95ec0bb565c
files client/src/components/gauge/Gauges.vue client/src/components/gauge/HydrologicalConditions.vue client/src/components/gauge/Waterlevel.vue client/src/components/splitscreen/Splitscreen.vue client/src/store/gauges.js
diffstat 5 files changed, 478 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/components/gauge/Gauges.vue	Thu Mar 21 17:04:37 2019 +0100
+++ b/client/src/components/gauge/Gauges.vue	Thu Mar 21 17:31:03 2019 +0100
@@ -68,7 +68,11 @@
             <translate>Show Waterlevels</translate>
           </button>
           <hr />
-          <button class="btn btn-sm btn-info d-block w-100 mt-2" disabled>
+          <button
+            @click="showHydrologicalConditionsDiagram()"
+            class="btn btn-sm btn-info d-block w-100 mt-2"
+            disabled
+          >
             <translate>Show Hydrological Conditions</translate>
           </button>
         </div>
@@ -149,6 +153,9 @@
       if (this.activeSplitscreenId === "gauge-waterlevel") {
         this.showWaterlevelDiagram();
       }
+      if (this.activeSplitscreenId === "gauge-hydrological-conditions") {
+        this.showHydrologicalConditionsDiagram();
+      }
     }
   },
   methods: {
@@ -212,6 +219,53 @@
           this.loading = false;
         });
     },
+    showHydrologicalConditionsDiagram() {
+      // for panning the map to the gauge on opening the diagram: needs to be
+      // set outside of the expandCallback to not always refer to the currently
+      // selectedGauge
+      let coordinates = this.selectedGauge.geometry.coordinates;
+
+      // configure splitscreen
+      let splitscreenConf = {
+        id: "gauge-hydrological-conditions",
+        component: "hydrological-conditions",
+        title:
+          this.$gettext("Hydrological Conditions") +
+          ": " +
+          this.gaugeLabel(this.selectedGauge),
+        icon: "ruler-vertical",
+        closeCallback: () => {
+          this.$store.commit("gauges/selectedGaugeISRS", null);
+          this.$store.commit("gauges/meanWaterlevels", []);
+        },
+        expandCallback: () => {
+          this.$store.commit("map/moveMap", {
+            coordinates,
+            zoom: 17,
+            preventZoomOut: true
+          });
+        }
+      };
+      this.$store.commit("application/addSplitscreen", splitscreenConf);
+      this.$store.commit("application/activeSplitscreenId", splitscreenConf.id);
+      this.$store.commit("application/splitscreenLoading", true);
+      this.loading = true;
+      this.$store.commit("application/showSplitscreen", true);
+
+      this.$store
+        .dispatch("gauges/loadMeanWaterlevels")
+        .catch(error => {
+          const { status, data } = error.response;
+          displayError({
+            title: "Backend Error",
+            message: `${status}: ${data.message || data}`
+          });
+        })
+        .finally(() => {
+          this.$store.commit("application/splitscreenLoading", false);
+          this.loading = false;
+        });
+    },
     gaugeLabel(gauge) {
       return `${gauge.properties.objname} (${this.isrsInfo(gauge).orc})`;
     },
--- /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>
--- a/client/src/components/gauge/Waterlevel.vue	Thu Mar 21 17:04:37 2019 +0100
+++ b/client/src/components/gauge/Waterlevel.vue	Thu Mar 21 17:31:03 2019 +0100
@@ -178,7 +178,7 @@
         y = d3.scaleLinear().range([mainHeight, 0]),
         y2 = d3.scaleLinear().range([navHeight, 0]);
       // find min/max values for the waterlevel axis
-      // including hdc/ldc (+/- 100 cm)
+      // including HDC (+ 1/8 HDC-LDC) and LDC (- 1/4 HDC-LDC)
       let WaterlevelMinMax = d3.extent(
         [
           ...this.waterlevels,
--- a/client/src/components/splitscreen/Splitscreen.vue	Thu Mar 21 17:04:37 2019 +0100
+++ b/client/src/components/splitscreen/Splitscreen.vue	Thu Mar 21 17:31:03 2019 +0100
@@ -63,7 +63,9 @@
 export default {
   components: {
     Fairwayprofile: () => import("@/components/fairway/Fairwayprofile"),
-    Waterlevel: () => import("@/components/gauge/Waterlevel")
+    Waterlevel: () => import("@/components/gauge/Waterlevel"),
+    HydrologicalConditions: () =>
+      import("@/components/gauge/HydrologicalConditions")
   },
   computed: {
     ...mapState("application", ["showSplitscreen", "splitscreenLoading"]),
--- a/client/src/store/gauges.js	Thu Mar 21 17:04:37 2019 +0100
+++ b/client/src/store/gauges.js	Thu Mar 21 17:31:03 2019 +0100
@@ -22,6 +22,7 @@
     gauges: [],
     selectedGaugeISRS: null,
     waterlevels: [],
+    meanWaterlevels: [],
     nashSutcliffe: null,
     dateFrom: dateFrom,
     dateTo: new Date()
@@ -49,6 +50,9 @@
     waterlevels: (state, data) => {
       state.waterlevels = data;
     },
+    meanWaterlevels: (state, data) => {
+      state.meanWaterlevels = data;
+    },
     nashSutcliffe: (state, data) => {
       state.nashSutcliffe = data;
     },
@@ -129,6 +133,27 @@
           });
       });
     },
+    loadMeanWaterlevels({ /*state,*/ commit }) {
+      return new Promise(resolve => {
+        setTimeout(() => {
+          commit("meanWaterlevels", [1]);
+          resolve();
+        }, 2000);
+      });
+      // return new Promise((resolve, reject) => {
+      //   HTTP.get(`/data/mean-waterlevels/${state.selectedGaugeISRS}`, {
+      //     headers: { "X-Gemma-Auth": localStorage.getItem("token") }
+      //   })
+      //     .then(response => {
+      //       commit("meanWaterlevels", response.data);
+      //       resolve(response.data);
+      //     })
+      //     .catch(error => {
+      //       commit("meanWaterlevels", []);
+      //       reject(error);
+      //     })
+      // });
+    },
     loadNashSutcliffe({ state, commit }) {
       return new Promise((resolve, reject) => {
         HTTP.get(`/data/nash-sutcliffe/${state.selectedGaugeISRS}`, {