changeset 3992:2f024d6189ca

Reverted merge with diagram-cleanup -- its not yet ready.
author Sascha Wilde <wilde@intevation.de>
date Wed, 17 Jul 2019 15:12:23 +0200
parents 06096a7ce1c5
children 6672b780722f
files client/src/components/fairway/AvailableFairwayDepth.vue client/src/components/fairway/AvailableFairwayDepthLNWL.vue client/src/components/fairway/Fairwayprofile.vue client/src/components/gauge/HydrologicalConditions.vue client/src/components/gauge/Waterlevel.vue client/src/lib/DefaultDiagramTemplate.js client/src/lib/mixins.js
diffstat 7 files changed, 652 insertions(+), 216 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/components/fairway/AvailableFairwayDepth.vue	Wed Jul 17 14:18:43 2019 +0200
+++ b/client/src/components/fairway/AvailableFairwayDepth.vue	Wed Jul 17 15:12:23 2019 +0200
@@ -1,6 +1,7 @@
 <template>
   <div class="d-flex flex-column flex-fill">
     <UIBoxHeader icon="chart-area" :title="title" :closeCallback="close" />
+    <UISpinnerOverlay v-if="loading" />
     <div class="d-flex flex-fill">
       <DiagramLegend>
         <div v-for="(entry, index) in legend" :key="index" class="legend">
@@ -43,17 +44,16 @@
         </div>
       </DiagramLegend>
       <div
-        class="d-flex flex-fill justify-content-center align-items-center"
+        ref="diagramContainer"
         :id="containerId"
-      >
-        <div v-if="!fwData.length">
-          <translate>No data available.</translate>
-        </div>
-      </div>
+        class="diagram-container flex-fill"
+      ></div>
     </div>
   </div>
 </template>
 
+<style></style>
+
 <script>
 /* This is Free Software under GNU Affero General Public License v >= 3.0
  * without warranty, see README.md and license for details.
@@ -71,30 +71,86 @@
  * * Fadi Abbud <fadi.abbud@intevation.de>
  */
 import * as d3 from "d3";
+import app from "@/main";
+import debounce from "debounce";
 import { mapState } from "vuex";
+import filters from "@/lib/filters.js";
 import { diagram, pdfgen, templateLoader } from "@/lib/mixins";
+import { HTTP } from "@/lib/http";
+import { displayError } from "@/lib/errors";
 import { FREQUENCIES } from "@/store/fairwayavailability";
-import filters from "@/lib/filters.js";
+import { defaultDiagramTemplate } from "@/lib/DefaultDiagramTemplate";
 
-// hoursInDays is a const instead of component method because it's used where
-// the component context is not available
 const hoursInDays = x => x / 24;
-const COLORS = {
-  LDC: "#cdcdcd",
-  HIGHEST: "#3675ff",
-  REST: ["#782121", "#ff6c6c", "#ffaaaa"]
-};
 
 export default {
   mixins: [diagram, pdfgen, templateLoader],
+  components: {
+    DiagramLegend: () => import("@/components/DiagramLegend")
+  },
   data() {
     return {
       containerId: "availablefairwaydepth-diagram-container",
+      resizeListenerFunction: null,
+      loading: false,
       scalePaddingLeft: 60,
       scalePaddingRight: 0,
-      paddingTop: 25
+      paddingTop: 25,
+      pdf: {
+        doc: null,
+        width: null,
+        height: null
+      },
+      form: {
+        template: null
+      },
+      templateData: null,
+      templates: [],
+      defaultTemplate: defaultDiagramTemplate
     };
   },
+  created() {
+    this.resizeListenerFunction = debounce(this.drawDiagram, 100);
+    window.addEventListener("resize", this.resizeListenerFunction);
+  },
+  destroyed() {
+    window.removeEventListener("resize", this.resizeListenerFunction);
+  },
+  mounted() {
+    // Nasty but necessary if we don't want to use the updated hook to re-draw
+    // the diagram because this would re-draw it also for irrelevant reasons.
+    // In this case we need to wait for the child component (DiagramLegend) to
+    // render. According to the docs (https://vuejs.org/v2/api/#mounted) this
+    // should be possible with $nextTick() but it doesn't work because it does
+    // not guarantee that the DOM is not only updated but also re-painted on the
+    // screen.
+    setTimeout(this.drawDiagram, 150);
+
+    this.templates[0] = this.defaultTemplate;
+    this.form.template = this.templates[0];
+    this.templateData = this.form.template;
+    HTTP.get("/templates/diagram", {
+      headers: {
+        "X-Gemma-Auth": localStorage.getItem("token"),
+        "Content-type": "text/xml; charset=UTF-8"
+      }
+    })
+      .then(response => {
+        if (response.data.length) {
+          this.templates = response.data;
+          this.form.template = this.templates[0];
+          this.templates[this.templates.length] = this.defaultTemplate;
+          this.applyChange();
+        }
+      })
+      .catch(e => {
+        const { status, data } = e.response;
+        displayError({
+          title: this.$gettext("Backend Error"),
+          message: `${status}: ${data.message || data}`
+        });
+      });
+  },
   computed: {
     ...mapState("fairwayavailability", [
       "selectedFairwayAvailabilityFeature",
@@ -160,6 +216,40 @@
     }
   },
   methods: {
+    applyChange() {
+      if (this.form.template.hasOwnProperty("properties")) {
+        this.templateData = this.defaultTemplate;
+        return;
+      }
+      if (this.form.template) {
+        this.loadTemplates("/templates/diagram/" + this.form.template.name)
+          .then(response => {
+            this.prepareImages(response.data.template_data.elements).then(
+              values => {
+                values.forEach(v => {
+                  response.data.template_data.elements[v.index].url = v.url;
+                });
+                this.templateData = response.data.template_data;
+              }
+            );
+          })
+          .catch(e => {
+            const { status, data } = e.response;
+            displayError({
+              title: this.$gettext("Backend Error"),
+              message: `${status}: ${data.message || data}`
+            });
+          });
+      }
+    },
+    downloadPDF() {
+      let title = `Available Fairway Depth: ${this.featureName}`;
+      this.generatePDF({
+        templateData: this.templateData,
+        diagramTitle: title
+      });
+      this.pdf.doc.save(`Available Fairway Depth: ${this.featureName}`);
+    },
     addDiagramLegend(position, offset, color) {
       let x = offset.x,
         y = offset.y;
@@ -176,54 +266,60 @@
       }
 
       this.pdf.doc.setTextColor(color);
-      this.pdf.doc.setDrawColor(COLORS.LDC);
-      this.pdf.doc.setFillColor(COLORS.LDC);
+      this.pdf.doc.setDrawColor(this.$options.COLORS.LDC);
+      this.pdf.doc.setFillColor(this.$options.COLORS.LDC);
       this.pdf.doc.roundedRect(x, y, 10, 4, 1.5, 1.5, "FD");
       this.pdf.doc.text(this.legend[0], x + 12, y + 3);
 
-      this.pdf.doc.setDrawColor(COLORS.REST[0]);
-      this.pdf.doc.setFillColor(COLORS.REST[0]);
+      this.pdf.doc.setDrawColor(this.$options.COLORS.REST[0]);
+      this.pdf.doc.setFillColor(this.$options.COLORS.REST[0]);
       this.pdf.doc.roundedRect(x, y + 5, 10, 4, 1.5, 1.5, "FD");
       this.pdf.doc.text(this.legend[1], x + 12, y + 8);
 
-      this.pdf.doc.setDrawColor(COLORS.REST[1]);
-      this.pdf.doc.setFillColor(COLORS.REST[1]);
+      this.pdf.doc.setDrawColor(this.$options.COLORS.REST[1]);
+      this.pdf.doc.setFillColor(this.$options.COLORS.REST[1]);
       this.pdf.doc.roundedRect(x, y + 10, 10, 4, 1.5, 1.5, "FD");
       this.pdf.doc.text(this.legend[2], x + 12, y + 13);
 
-      this.pdf.doc.setDrawColor(COLORS.HIGHEST);
-      this.pdf.doc.setFillColor(COLORS.HIGHEST);
+      this.pdf.doc.setDrawColor(this.$options.COLORS.HIGHEST);
+      this.pdf.doc.setFillColor(this.$options.COLORS.HIGHEST);
       this.pdf.doc.roundedRect(x, y + 15, 10, 4, 1.5, 1.5, "FD");
       this.pdf.doc.text(this.legend[3], x + 12, y + 18);
     },
     legendStyle(index) {
       return [
-        `background-color: ${COLORS.LDC};`,
-        `background-color: ${COLORS.HIGHEST};`,
-        `background-color: ${COLORS.REST[1]};`,
-        `background-color: ${COLORS.REST[0]};`
+        `background-color: ${this.$options.COLORS.LDC};`,
+        `background-color: ${this.$options.COLORS.HIGHEST};`,
+        `background-color: ${this.$options.COLORS.REST[1]};`,
+        `background-color: ${this.$options.COLORS.REST[0]};`
       ][index];
     },
     close() {
       this.$store.commit("application/paneSetup", "DEFAULT");
     },
-    getPrintLayout() {
+    getPrintLayout(svgHeight) {
       return {
-        main: { top: 0, right: 20, bottom: 50, left: 20 }
+        main: { top: 0, right: 20, bottom: 50, left: 20 },
+        nav: {
+          top: svgHeight - 65,
+          right: 20,
+          bottom: 30,
+          left: 80
+        }
       };
     },
     drawDiagram() {
       const elem = document.querySelector("#" + this.containerId);
       const svgWidth = elem != null ? elem.clientWidth : 0;
       const svgHeight = elem != null ? elem.clientHeight : 0;
-      const layout = this.getPrintLayout();
+      const layout = this.getPrintLayout(svgHeight);
       const dimensions = this.getDimensions({
         svgHeight,
         svgWidth,
         ...layout
       });
-      d3.select("#" + this.containerId + " svg").remove();
-      this.renderTo({ element: "#" + this.containerId, dimensions });
+      d3.select(".diagram-container svg").remove();
+      this.renderTo({ element: ".diagram-container", dimensions });
     },
     renderTo({ element, dimensions }) {
       const diagram = d3
@@ -344,7 +440,7 @@
         .attr("x", ldcOffset + spaceBetween / 2)
         .attr("width", widthPerItem - ldcOffset - spaceBetween)
         .attr("fill", (d, i) => {
-          return COLORS.REST[i];
+          return this.$options.COLORS.REST[i];
         });
     },
     fnheight({ name, yScale }) {
@@ -383,7 +479,7 @@
           "transform",
           d => `translate(0 ${this.paddingTop + -1 * height(d)})`
         )
-        .attr("fill", COLORS.LDC)
+        .attr("fill", this.$options.COLORS.LDC)
         .attr("id", "ldc");
     },
     drawHighestLevel({
@@ -427,7 +523,7 @@
           "transform",
           d => `translate(0 ${this.paddingTop + -1 * height(d)})`
         )
-        .attr("fill", COLORS.HIGHEST);
+        .attr("fill", this.$options.COLORS.HIGHEST);
     },
     drawLabelPerBar({ everyBar, dimensions, widthPerItem }) {
       everyBar
@@ -441,7 +537,7 @@
     drawScaleLabel({ diagram, dimensions }) {
       diagram
         .append("text")
-        .text(this.$gettext("Sum of days"))
+        .text(this.$options.LEGEND)
         .attr("text-anchor", "middle")
         .attr("x", 0)
         .attr("y", 0)
@@ -519,6 +615,12 @@
     fwData() {
       this.drawDiagram();
     }
+  },
+  LEGEND: app.$gettext("Sum of days"),
+  COLORS: {
+    LDC: "#cdcdcd",
+    HIGHEST: "#3675ff",
+    REST: ["#782121", "#ff6c6c", "#ffaaaa"]
   }
 };
 </script>
--- a/client/src/components/fairway/AvailableFairwayDepthLNWL.vue	Wed Jul 17 14:18:43 2019 +0200
+++ b/client/src/components/fairway/AvailableFairwayDepthLNWL.vue	Wed Jul 17 15:12:23 2019 +0200
@@ -1,6 +1,7 @@
 <template>
   <div class="d-flex flex-column flex-fill">
     <UIBoxHeader icon="chart-area" :title="title" :closeCallback="close" />
+    <UISpinnerOverlay v-if="loading" />
     <div class="d-flex flex-fill">
       <DiagramLegend>
         <div v-for="(entry, index) in legendLNWL" :key="index" class="legend">
@@ -43,17 +44,16 @@
         </div>
       </DiagramLegend>
       <div
-        class="d-flex flex-fill justify-content-center align-items-center"
+        ref="diagramContainer"
         :id="containerId"
-      >
-        <div v-if="!fwLNWLData.length">
-          <translate>No data available.</translate>
-        </div>
-      </div>
+        class="diagram-container flex-fill"
+      ></div>
     </div>
   </div>
 </template>
 
+<style></style>
+
 <script>
 /* This is Free Software under GNU Affero General Public License v >= 3.0
  * without warranty, see README.md and license for details.
@@ -71,23 +71,82 @@
  * * Fadi Abbud <fadi.abbud@intevation.de>
  */
 import * as d3 from "d3";
+import app from "@/main";
+import debounce from "debounce";
 import { mapState } from "vuex";
+import filters from "@/lib/filters.js";
 import { diagram, pdfgen, templateLoader } from "@/lib/mixins";
-import filters from "@/lib/filters.js";
+import { HTTP } from "@/lib/http";
+import { displayError } from "@/lib/errors";
+import { defaultDiagramTemplate } from "@/lib/DefaultDiagramTemplate";
 
 export default {
   mixins: [diagram, pdfgen, templateLoader],
+  components: {
+    DiagramLegend: () => import("@/components/DiagramLegend")
+  },
   data() {
     return {
       containerId: "availablefairwaydepthlnwl-diagram-container",
+      resizeListenerFunction: null,
+      loading: false,
       scalePaddingLeft: 60,
       scalePaddingRight: 0,
       paddingTop: 25,
-      colors: {
-        afd: ["#3636ff", "#f49b7f", "#e15472"],
-        lnwl: "#97ddf3"
+      pdf: {
+        doc: null,
+        width: null,
+        height: null
+      },
+      form: {
+        template: null
+      },
+      templateData: null,
+      templates: [],
+      defaultTemplate: defaultDiagramTemplate
+    };
+  },
+  created() {
+    this.resizeListenerFunction = debounce(this.drawDiagram, 100);
+    window.addEventListener("resize", this.resizeListenerFunction);
+  },
+  destroyed() {
+    window.removeEventListener("resize", this.resizeListenerFunction);
+  },
+  mounted() {
+    // Nasty but necessary if we don't want to use the updated hook to re-draw
+    // the diagram because this would re-draw it also for irrelevant reasons.
+    // In this case we need to wait for the child component (DiagramLegend) to
+    // render. According to the docs (https://vuejs.org/v2/api/#mounted) this
+    // should be possible with $nextTick() but it doesn't work because it does
+    // not guarantee that the DOM is not only updated but also re-painted on the
+    // screen.
+    setTimeout(this.drawDiagram, 150);
+
+    this.templates[0] = this.defaultTemplate;
+    this.form.template = this.templates[0];
+    this.templateData = this.form.template;
+    HTTP.get("/templates/diagram", {
+      headers: {
+        "X-Gemma-Auth": localStorage.getItem("token"),
+        "Content-type": "text/xml; charset=UTF-8"
       }
-    };
+    })
+      .then(response => {
+        if (response.data.length) {
+          this.templates = response.data;
+          this.form.template = this.templates[0];
+          this.templates[this.templates.length] = this.defaultTemplate;
+          this.applyChange();
+        }
+      })
+      .catch(e => {
+        const { status, data } = e.response;
+        displayError({
+          title: this.$gettext("Backend Error"),
+          message: `${status}: ${data.message || data}`
+        });
+      });
   },
   computed: {
     ...mapState("fairwayavailability", [
@@ -148,13 +207,47 @@
   methods: {
     legendStyle(index) {
       const style = {
-        0: `background-color: ${this.colors.lnwl};`,
-        1: `background-color: ${this.colors.afd[2]};`,
-        2: `background-color: ${this.colors.afd[1]};`,
-        3: `background-color: ${this.colors.afd[0]};`
+        0: `background-color: ${this.$options.LWNLCOLORS.LDC};`,
+        1: `background-color: ${this.$options.AFDCOLORS[2]};`,
+        2: `background-color: ${this.$options.AFDCOLORS[1]};`,
+        3: `background-color: ${this.$options.AFDCOLORS[0]};`
       };
       return style[index];
     },
+    applyChange() {
+      if (this.form.template.hasOwnProperty("properties")) {
+        this.templateData = this.defaultTemplate;
+        return;
+      }
+      if (this.form.template) {
+        this.loadTemplates("/templates/diagram/" + this.form.template.name)
+          .then(response => {
+            this.prepareImages(response.data.template_data.elements).then(
+              values => {
+                values.forEach(v => {
+                  response.data.template_data.elements[v.index].url = v.url;
+                });
+                this.templateData = response.data.template_data;
+              }
+            );
+          })
+          .catch(e => {
+            const { status, data } = e.response;
+            displayError({
+              title: this.$gettext("Backend Error"),
+              message: `${status}: ${data.message || data}`
+            });
+          });
+      }
+    },
+    downloadPDF() {
+      let title = `Available Fairway Depth vs LNWL: ${this.featureName}`;
+      this.generatePDF({
+        templateData: this.templateData,
+        diagramTitle: title
+      });
+      this.pdf.doc.save(`Available Fairway Depth LNWL: ${this.featureName}`);
+    },
     addDiagramLegend(position, offset, color) {
       let x = offset.x,
         y = offset.y;
@@ -170,46 +263,52 @@
         y = this.pdf.height - offset.y - this.getTextHeight(6);
       }
       this.pdf.doc.setTextColor(color);
-      this.pdf.doc.setDrawColor(this.colors.lnwl);
-      this.pdf.doc.setFillColor(this.colors.lnwl);
+      this.pdf.doc.setDrawColor(this.$options.LWNLCOLORS.LDC);
+      this.pdf.doc.setFillColor(this.$options.LWNLCOLORS.LDC);
       this.pdf.doc.roundedRect(x, y, 10, 4, 1.5, 1.5, "FD");
       this.pdf.doc.text(this.legendLNWL[0], x + 12, y + 3);
 
-      this.pdf.doc.setDrawColor(this.colors.afd[2]);
-      this.pdf.doc.setFillColor(this.colors.afd[2]);
+      this.pdf.doc.setDrawColor(this.$options.AFDCOLORS[2]);
+      this.pdf.doc.setFillColor(this.$options.AFDCOLORS[2]);
       this.pdf.doc.roundedRect(x, y + 5, 10, 4, 1.5, 1.5, "FD");
       this.pdf.doc.text(this.legendLNWL[1], x + 12, y + 8);
 
-      this.pdf.doc.setDrawColor(this.colors.afd[1]);
-      this.pdf.doc.setFillColor(this.colors.afd[1]);
+      this.pdf.doc.setDrawColor(this.$options.AFDCOLORS[1]);
+      this.pdf.doc.setFillColor(this.$options.AFDCOLORS[1]);
       this.pdf.doc.roundedRect(x, y + 10, 10, 4, 1.5, 1.5, "FD");
       this.pdf.doc.text(this.legendLNWL[2], x + 12, y + 13);
 
-      this.pdf.doc.setDrawColor(this.colors.afd[0]);
-      this.pdf.doc.setFillColor(this.colors.afd[0]);
+      this.pdf.doc.setDrawColor(this.$options.AFDCOLORS[0]);
+      this.pdf.doc.setFillColor(this.$options.AFDCOLORS[0]);
       this.pdf.doc.roundedRect(x, y + 15, 10, 4, 1.5, 1.5, "FD");
       this.pdf.doc.text(this.legendLNWL[3], x + 12, y + 18);
     },
     close() {
       this.$store.commit("application/paneSetup", "DEFAULT");
     },
-    getPrintLayout() {
+    getPrintLayout(svgHeight) {
       return {
-        main: { top: 0, right: 20, bottom: 50, left: 20 }
+        main: { top: 0, right: 20, bottom: 50, left: 20 },
+        nav: {
+          top: svgHeight - 65,
+          right: 20,
+          bottom: 30,
+          left: 80
+        }
       };
     },
     drawDiagram() {
       const elem = document.querySelector("#" + this.containerId);
       const svgWidth = elem != null ? elem.clientWidth : 0;
       const svgHeight = elem != null ? elem.clientHeight : 0;
-      const layout = this.getPrintLayout();
+      const layout = this.getPrintLayout(svgHeight);
       const dimensions = this.getDimensions({
         svgHeight,
         svgWidth,
         ...layout
       });
-      d3.select("#" + this.containerId + " svg").remove();
-      this.renderTo({ element: "#" + this.containerId, dimensions });
+      d3.select(".diagram-container svg").remove();
+      this.renderTo({ element: ".diagram-container", dimensions });
     },
     drawTooltip(diagram) {
       diagram
@@ -343,7 +442,7 @@
         .attr("transform", `translate(0 ${this.paddingTop})`)
         .attr("width", afdWidth)
         .attr("fill", (d, i) => {
-          return this.colors.afd[i];
+          return this.$options.AFDCOLORS[i];
         });
     },
     drawLNWL(data, i, diagram, spaceBetween, widthPerItem, ldcWidth, yScale) {
@@ -387,13 +486,13 @@
         .attr("transform", `translate(0 ${this.paddingTop})`)
         .attr("width", ldcWidth)
         .attr("fill", () => {
-          return this.colors.lnwl;
+          return this.$options.LWNLCOLORS.LDC;
         });
     },
     drawScaleLabel({ diagram, dimensions }) {
       diagram
         .append("text")
-        .text(this.$gettext("Percent"))
+        .text(this.$options.LEGEND)
         .attr("text-anchor", "middle")
         .attr("x", 0)
         .attr("y", 0)
@@ -472,6 +571,12 @@
     fwLNWLData() {
       this.drawDiagram();
     }
+  },
+  LEGEND: app.$gettext("Percent"),
+  AFDCOLORS: ["#3636ff", "#f49b7f", "#e15472"],
+  LWNLCOLORS: {
+    LDC: "#97ddf3",
+    HDC: "#43FFE1"
   }
 };
 </script>
--- a/client/src/components/fairway/Fairwayprofile.vue	Wed Jul 17 14:18:43 2019 +0200
+++ b/client/src/components/fairway/Fairwayprofile.vue	Wed Jul 17 15:12:23 2019 +0200
@@ -93,11 +93,12 @@
         </div>
       </DiagramLegend>
       <div
-        class="d-flex flex-fill justify-content-center align-items-center position-relative"
-        :id="containerId"
+        ref="pdfContainer"
+        id="pdfContainer"
+        class="d-flex flex-fill justify-content-center align-items-center diagram-container position-relative"
       >
         <div class="direction-indicator"></div>
-        <div v-if="!fairwayData.length">
+        <div v-if="!fairwayData">
           <translate>No data available.</translate>
         </div>
       </div>
@@ -147,16 +148,37 @@
  */
 import * as d3 from "d3";
 import { mapState, mapGetters } from "vuex";
+import debounce from "debounce";
 import { diagram, pdfgen, templateLoader } from "@/lib/mixins";
+import { HTTP } from "@/lib/http";
+import { displayError } from "@/lib/errors";
+import { defaultDiagramTemplate } from "@/lib/DefaultDiagramTemplate";
 
 const GROUND_COLOR = "#4A2F06";
 const WATER_COLOR = "#005DFF";
 
 export default {
   mixins: [diagram, pdfgen, templateLoader],
+  name: "fairwayprofile",
+  components: {
+    DiagramLegend: () => import("@/components/DiagramLegend")
+  },
   data() {
     return {
-      containerId: "fairwayprofile-diagram-container"
+      resizeListenerFunction: null,
+      width: null,
+      height: null,
+      form: {
+        template: null
+      },
+      templates: [],
+      defaultTemplate: defaultDiagramTemplate,
+      pdf: {
+        doc: null,
+        width: 32,
+        height: 297
+      },
+      templateData: null
     };
   },
   computed: {
@@ -271,6 +293,46 @@
 
       return { fillColor, fillOpacity, strokeColor, strokeOpacity, strokeDash };
     },
+    applyChange() {
+      if (this.form.template.hasOwnProperty("properties")) {
+        this.templateData = this.defaultTemplate;
+        return;
+      }
+      if (this.form.template) {
+        this.loadTemplates("/templates/diagram/" + this.form.template.name)
+          .then(response => {
+            this.prepareImages(response.data.template_data.elements).then(
+              values => {
+                values.forEach(v => {
+                  response.data.template_data.elements[v.index].url = v.url;
+                });
+                this.templateData = response.data.template_data;
+              }
+            );
+          })
+          .catch(e => {
+            const { status, data } = e.response;
+            displayError({
+              title: this.$gettext("Backend Error"),
+              message: `${status}: ${data.message || data}`
+            });
+          });
+      }
+    },
+    downloadPDF() {
+      let fairwayInfo =
+        this.selectedBottleneck + " (" + this.selectedSurvey.date_info + ")";
+
+      this.generatePDF({
+        templateData: this.templateData,
+        diagramTitle: fairwayInfo
+      });
+
+      this.pdf.doc.save(
+        this.title.replace(/\s/g, "_").replace(/[():,]/g, "") + ".pdf"
+      );
+    },
+
     // Diagram legend
     addDiagramLegend(position, offset, color) {
       let x = offset.x,
@@ -321,21 +383,24 @@
     },
     getPrintLayout() {
       return {
-        main: { top: 20, right: 80, bottom: 60, left: 80 }
+        main: {
+          top: 20,
+          right: 80,
+          bottom: 60,
+          left: 80
+        }
       };
     },
     drawDiagram() {
-      // remove old diagram and exit if necessary data is missing
-      d3.select("#" + this.containerId + " svg").remove();
-      const elem = document.querySelector("#" + this.containerId);
-      const svgWidth = elem.clientWidth;
-      const svgHeight = elem.clientHeight;
-      const layout = this.getPrintLayout();
+      d3.select(".diagram-container svg").remove();
+      this.scaleFairwayProfile();
+      if (!this.height || !this.width) return; // do not try to render when height and width are unknown
+      const layout = this.getPrintLayout(this.height);
       this.renderTo({
-        element: "#" + this.containerId,
+        element: ".diagram-container",
         dimensions: this.getDimensions({
-          svgWidth: svgWidth,
-          svgHeight: svgHeight,
+          svgWidth: this.width,
+          svgHeight: this.height,
           ...layout
         })
       });
@@ -607,7 +672,62 @@
           .attr("fill-opacity", 0)
           .attr("d", profileLine);
       }
+    },
+    scaleFairwayProfile() {
+      if (!document.querySelector(".diagram-container")) return;
+      const clientHeight = document.querySelector(".diagram-container")
+        .clientHeight;
+      const clientWidth = document.querySelector(".diagram-container")
+        .clientWidth;
+      if (!clientHeight || !clientWidth) return;
+      this.height = clientHeight;
+      this.width = clientWidth;
     }
+  },
+  created() {
+    this.resizeListenerFunction = debounce(this.drawDiagram, 100);
+    window.addEventListener("resize", this.resizeListenerFunction);
+  },
+  mounted() {
+    // Nasty but necessary if we don't want to use the updated hook to re-draw
+    // the diagram because this would re-draw it also for irrelevant reasons.
+    // In this case we need to wait for the child component (DiagramLegend) to
+    // render. According to the docs (https://vuejs.org/v2/api/#mounted) this
+    // should be possible with $nextTick() but it doesn't work because it does
+    // not guarantee that the DOM is not only updated but also re-painted on the
+    // screen.
+    setTimeout(this.drawDiagram, 150);
+
+    this.templates[0] = this.defaultTemplate;
+    this.form.template = this.templates[0];
+    this.templateData = this.form.template;
+    HTTP.get("/templates/diagram", {
+      headers: {
+        "X-Gemma-Auth": localStorage.getItem("token"),
+        "Content-type": "text/xml; charset=UTF-8"
+      }
+    })
+      .then(response => {
+        if (response.data.length) {
+          this.templates = response.data;
+          this.form.template = this.templates[0];
+          this.templates[this.templates.length] = this.defaultTemplate;
+          this.applyChange();
+        }
+      })
+      .catch(e => {
+        const { status, data } = e.response;
+        displayError({
+          title: this.$gettext("Backend Error"),
+          message: `${status}: ${data.message || data}`
+        });
+      });
+  },
+  updated() {
+    this.drawDiagram();
+  },
+  destroyed() {
+    window.removeEventListener("resize", this.resizeListenerFunction);
   }
 };
 </script>
--- a/client/src/components/gauge/HydrologicalConditions.vue	Wed Jul 17 14:18:43 2019 +0200
+++ b/client/src/components/gauge/HydrologicalConditions.vue	Wed Jul 17 15:12:23 2019 +0200
@@ -1,8 +1,13 @@
 <template>
   <div class="d-flex flex-column flex-fill">
-    <UIBoxHeader icon="ruler-vertical" :title="title" :closeCallback="close" />
+    <UIBoxHeader
+      icon="ruler-vertical"
+      :title="title"
+      :closeCallback="close"
+      class="rounded-0"
+    />
     <div class="d-flex flex-fill">
-      <DiagramLegend>
+      <DiagramLegend id="diagramlegendId">
         <div class="legend">
           <span
             style="background-color: red; width: 20px; height: 20px;"
@@ -97,14 +102,34 @@
 
 import { mapState, mapGetters } from "vuex";
 import * as d3 from "d3";
+import debounce from "debounce";
 import { startOfYear, endOfYear } from "date-fns";
 import { diagram, pdfgen, templateLoader } from "@/lib/mixins";
+import { HTTP } from "@/lib/http";
+import { displayError } from "@/lib/errors";
+import { defaultDiagramTemplate } from "@/lib/DefaultDiagramTemplate";
 
 export default {
   mixins: [diagram, pdfgen, templateLoader],
+  components: {
+    DiagramLegend: () => import("@/components/DiagramLegend")
+  },
   data() {
     return {
-      containerId: "hydrologicalconditions-diagram-container"
+      containerId: "hydrologicalconditions-diagram-container",
+      resizeListenerFunction: null,
+      templateData: null,
+      form: {
+        template: null,
+        form: null
+      },
+      templates: [],
+      defaultTemplate: defaultDiagramTemplate,
+      pdf: {
+        doc: null,
+        width: 420,
+        height: 297
+      }
     };
   },
   computed: {
@@ -172,6 +197,48 @@
           : "DEFAULT"
       );
     },
+    downloadPDF() {
+      let diagramTitle =
+        this.gaugeInfo(this.selectedGauge) +
+        ": Hydrological Conditions " +
+        this.longtermInterval.join(" - ");
+
+      this.generatePDF({
+        templateData: this.templateData,
+        diagramTitle: diagramTitle
+      });
+
+      this.pdf.doc.save(
+        this.selectedGauge.properties.objname +
+          " Hydrological-condition Diagram.pdf"
+      );
+    },
+    applyChange() {
+      if (this.form.template.hasOwnProperty("properties")) {
+        this.templateData = this.defaultTemplate;
+        return;
+      }
+      if (this.form.template) {
+        this.loadTemplates("/templates/diagram/" + this.form.template.name)
+          .then(response => {
+            this.prepareImages(response.data.template_data.elements).then(
+              values => {
+                values.forEach(v => {
+                  response.data.template_data.elements[v.index].url = v.url;
+                });
+                this.templateData = response.data.template_data;
+              }
+            );
+          })
+          .catch(e => {
+            const { status, data } = e.response;
+            displayError({
+              title: this.$gettext("Backend Error"),
+              message: `${status}: ${data.message || data}`
+            });
+          });
+      }
+    },
     // Diagram legend
     addDiagramLegend(position, offset, color) {
       let x = offset.x,
@@ -902,6 +969,48 @@
             .attr("height", textBBox.height + tooltipPadding * 2);
         });
     }
+  },
+  created() {
+    this.resizeListenerFunction = debounce(this.drawDiagram, 100);
+    window.addEventListener("resize", this.resizeListenerFunction);
+  },
+  mounted() {
+    // Nasty but necessary if we don't want to use the updated hook to re-draw
+    // the diagram because this would re-draw it also for irrelevant reasons.
+    // In this case we need to wait for the child component (DiagramLegend) to
+    // render. According to the docs (https://vuejs.org/v2/api/#mounted) this
+    // should be possible with $nextTick() but it doesn't work because it does
+    // not guarantee that the DOM is not only updated but also re-painted on the
+    // screen.
+    setTimeout(this.drawDiagram, 150);
+
+    this.templates[0] = this.defaultTemplate;
+    this.form.template = this.templates[0];
+    this.templateData = this.form.template;
+    HTTP.get("/templates/diagram", {
+      headers: {
+        "X-Gemma-Auth": localStorage.getItem("token"),
+        "Content-type": "text/xml; charset=UTF-8"
+      }
+    })
+      .then(response => {
+        if (response.data.length) {
+          this.templates = response.data;
+          this.form.template = this.templates[0];
+          this.templates[this.templates.length] = this.defaultTemplate;
+          this.applyChange();
+        }
+      })
+      .catch(e => {
+        const { status, data } = e.response;
+        displayError({
+          title: this.$gettext("Backend Error"),
+          message: `${status}: ${data.message || data}`
+        });
+      });
+  },
+  destroyed() {
+    window.removeEventListener("resize", this.resizeListenerFunction);
   }
 };
 </script>
--- a/client/src/components/gauge/Waterlevel.vue	Wed Jul 17 14:18:43 2019 +0200
+++ b/client/src/components/gauge/Waterlevel.vue	Wed Jul 17 15:12:23 2019 +0200
@@ -1,8 +1,13 @@
 <template>
   <div class="d-flex flex-column flex-fill">
-    <UIBoxHeader icon="ruler-vertical" :title="title" :closeCallback="close" />
+    <UIBoxHeader
+      icon="ruler-vertical"
+      :title="title"
+      :closeCallback="close"
+      class="rounded-0"
+    />
     <div class="d-flex flex-fill">
-      <DiagramLegend>
+      <DiagramLegend id="diagramlegendId">
         <div class="legend">
           <span
             style="background-color: steelblue; width: 20px; height: 20px;"
@@ -94,12 +99,17 @@
  * * Markus Kottländer <markus.kottlaender@intevation.de>
  * * Fadi Abbud <fadi.abbud@intevation.de>
  */
+
 import { mapState, mapGetters } from "vuex";
 import * as d3Base from "d3";
 import { lineChunked } from "d3-line-chunked";
 import { endOfDay } from "date-fns";
+import debounce from "debounce";
 import { saveAs } from "file-saver";
 import { diagram, pdfgen, templateLoader } from "@/lib/mixins";
+import { HTTP } from "@/lib/http";
+import { displayError } from "@/lib/errors";
+import { defaultDiagramTemplate } from "@/lib/DefaultDiagramTemplate";
 // 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
@@ -107,9 +117,24 @@
 
 export default {
   mixins: [diagram, pdfgen, templateLoader],
+  components: {
+    DiagramLegend: () => import("@/components/DiagramLegend")
+  },
   data() {
     return {
-      containerId: "waterlevel-diagram-container"
+      containerId: "waterlevel-diagram-container",
+      resizeListenerFunction: null,
+      form: {
+        template: null
+      },
+      templates: [],
+      defaultTemplate: defaultDiagramTemplate,
+      pdf: {
+        doc: null,
+        width: 420,
+        height: 297
+      },
+      templateData: null
     };
   },
   computed: {
@@ -177,6 +202,49 @@
         this.selectedGauge.properties.objname + "-waterlevel-diagram.svg";
       saveAs(blog, filename);
     },
+    downloadPDF() {
+      let diagramTitle =
+        this.gaugeInfo(this.selectedGauge) +
+        ": Waterlevel " +
+        this.dateFrom.toLocaleDateString() +
+        " - " +
+        this.dateTo.toLocaleDateString();
+
+      this.generatePDF({
+        templateData: this.templateData,
+        diagramTitle: diagramTitle
+      });
+
+      this.pdf.doc.save(
+        this.selectedGauge.properties.objname + " Waterlevel-Diagram.pdf"
+      );
+    },
+    applyChange() {
+      if (this.form.template.hasOwnProperty("properties")) {
+        this.templateData = this.defaultTemplate;
+        return;
+      }
+      if (this.form.template) {
+        this.loadTemplates("/templates/diagram/" + this.form.template.name)
+          .then(response => {
+            this.prepareImages(response.data.template_data.elements).then(
+              values => {
+                values.forEach(v => {
+                  response.data.template_data.elements[v.index].url = v.url;
+                });
+                this.templateData = response.data.template_data;
+              }
+            );
+          })
+          .catch(e => {
+            const { status, data } = e.response;
+            displayError({
+              title: this.$gettext("Backend Error"),
+              message: `${status}: ${data.message || data}`
+            });
+          });
+      }
+    },
     // Diagram legend
     addDiagramLegend(position, offset, color) {
       let x = offset.x;
@@ -882,8 +950,8 @@
         .append("rect")
         .attr("rx", "0.25em")
         .attr("ry", "0.25em")
-        .attr("width", "0")
-        .attr("height", "0");
+        .attr("width", "0px")
+        .attr("height", "0px");
 
       // create container for multiple text rows
       const tooltipText = tooltip.append("text").attr("text-anchor", "middle");
@@ -1021,6 +1089,48 @@
         return difference === 900;
       };
     }
+  },
+  created() {
+    this.resizeListenerFunction = debounce(this.drawDiagram, 100);
+    window.addEventListener("resize", this.resizeListenerFunction);
+  },
+  mounted() {
+    // Nasty but necessary if we don't want to use the updated hook to re-draw
+    // the diagram because this would re-draw it also for irrelevant reasons.
+    // In this case we need to wait for the child component (DiagramLegend) to
+    // render. According to the docs (https://vuejs.org/v2/api/#mounted) this
+    // should be possible with $nextTick() but it doesn't work because it does
+    // not guarantee that the DOM is not only updated but also re-painted on the
+    // screen.
+    setTimeout(this.drawDiagram, 150);
+
+    this.templates[0] = this.defaultTemplate;
+    this.form.template = this.templates[0];
+    this.templateData = this.form.template;
+    HTTP.get("/templates/diagram", {
+      headers: {
+        "X-Gemma-Auth": localStorage.getItem("token"),
+        "Content-type": "text/xml; charset=UTF-8"
+      }
+    })
+      .then(response => {
+        if (response.data.length) {
+          this.templates = response.data;
+          this.form.template = this.templates[0];
+          this.templates[this.templates.length] = this.defaultTemplate;
+          this.applyChange();
+        }
+      })
+      .catch(e => {
+        const { status, data } = e.response;
+        displayError({
+          title: this.$gettext("Backend Error"),
+          message: `${status}: ${data.message || data}`
+        });
+      });
+  },
+  destroyed() {
+    window.removeEventListener("resize", this.resizeListenerFunction);
   }
 };
 </script>
--- a/client/src/lib/DefaultDiagramTemplate.js	Wed Jul 17 14:18:43 2019 +0200
+++ b/client/src/lib/DefaultDiagramTemplate.js	Wed Jul 17 15:12:23 2019 +0200
@@ -1,4 +1,4 @@
-export default {
+const defaultDiagramTemplate = {
   name: "Default",
   properties: {
     paperSize: "a4"
@@ -26,3 +26,5 @@
     }
   ]
 };
+
+export { defaultDiagramTemplate };
--- a/client/src/lib/mixins.js	Wed Jul 17 14:18:43 2019 +0200
+++ b/client/src/lib/mixins.js	Wed Jul 17 15:12:23 2019 +0200
@@ -16,11 +16,8 @@
 import jsPDF from "jspdf-yworks";
 import svg2pdf from "svg2pdf.js";
 import locale2 from "locale2";
-import debounce from "debounce";
 import { mapState } from "vuex";
 import { HTTP } from "@/lib/http";
-import { displayError } from "@/lib/errors";
-import defaultDiagramTemplate from "@/lib/DefaultDiagramTemplate";
 
 export const sortTable = {
   data() {
@@ -40,58 +37,25 @@
 };
 
 export const diagram = {
-  components: {
-    DiagramLegend: () => import("@/components/DiagramLegend")
-  },
-  data() {
-    return {
-      resizeListenerFunction: null
-    };
-  },
   methods: {
-    getDimensions({ svgWidth, svgHeight, main, nav, DPI }) {
-      const mainMargin = {
+    getDimensions({ svgWidth, svgHeight, main, nav }) {
+      const mainMargin = main || {
         top: 20,
         right: 80,
         bottom: 60,
-        left: 80,
-        ...main
+        left: 80
       };
-      const navMargin = {
+      const navMargin = nav || {
         top: svgHeight - mainMargin.top - 65,
         right: 20,
         bottom: 30,
-        left: 80,
-        ...nav
+        left: 80
       };
-      if (DPI) {
-        ["top", "right", "bottom", "left"].forEach(x => {
-          mainMargin[x] = this.millimeter2pixels(mainMargin[x], DPI);
-          navMargin[x] = this.millimeter2pixels(navMargin[x], DPI);
-        });
-      }
       const width = Number(svgWidth) - mainMargin.left - mainMargin.right;
       const mainHeight = Number(svgHeight) - mainMargin.top - mainMargin.bottom;
       const navHeight = Number(svgHeight) - navMargin.top - navMargin.bottom;
       return { width, mainHeight, navHeight, mainMargin, navMargin };
     }
-  },
-  created() {
-    this.resizeListenerFunction = debounce(this.drawDiagram, 100);
-    window.addEventListener("resize", this.resizeListenerFunction);
-  },
-  mounted() {
-    // Nasty but necessary if we don't want to use the updated hook to re-draw
-    // the diagram because this would re-draw it also for irrelevant reasons.
-    // In this case we need to wait for the child component (DiagramLegend) to
-    // render. According to the docs (https://vuejs.org/v2/api/#mounted) this
-    // should be possible with $nextTick() but it doesn't work because it does
-    // not guarantee that the DOM is not only updated but also re-painted on the
-    // screen.
-    setTimeout(this.drawDiagram, 150);
-  },
-  destroyed() {
-    window.removeEventListener("resize", this.resizeListenerFunction);
   }
 };
 
@@ -165,39 +129,16 @@
 };
 
 export const pdfgen = {
-  data() {
-    return {
-      pdf: {
-        doc: null,
-        width: null,
-        height: null
-      },
-      templates: [],
-      defaultTemplate: defaultDiagramTemplate,
-      templateData: null,
-      form: {
-        template: null
-      }
-    };
-  },
   computed: {
     ...mapState("application", ["logoForPDF"]),
     ...mapState("user", ["user"])
   },
   methods: {
-    downloadPDF() {
-      this.generatePDF();
-
-      this.pdf.doc.save(
-        this.title.replace(/\s/g, "_").replace(/[():,]/g, "") + ".pdf"
-      );
-    },
     addDiagram(position, offset, width, height) {
       let x = offset.x,
         y = offset.y;
-      const DPI = this.templateData.properties.resoltion || 80;
-      const svgWidth = this.millimeter2pixels(width, DPI);
-      const svgHeight = this.millimeter2pixels(height, DPI);
+      const svgWidth = this.millimeter2pixels(width, 80);
+      const svgHeight = this.millimeter2pixels(height, 80);
       // draw the diagram in a separated html element to get the full size
       const offScreen = document.querySelector("#offScreen");
       offScreen.style.width = `${svgWidth}px`;
@@ -208,8 +149,7 @@
         dimensions: this.getDimensions({
           svgWidth: svgWidth,
           svgHeight: svgHeight,
-          ...layout,
-          DPI: DPI
+          ...layout
         })
       });
       var svg = offScreen.querySelector("svg");
@@ -222,7 +162,7 @@
       svg2pdf(svg, this.pdf.doc, {
         xOffset: x,
         yOffset: y,
-        scale: this.pixel2millimeter(1, DPI)
+        scale: 0.354
       });
       offScreen.removeChild(svg);
     },
@@ -257,12 +197,12 @@
         ")"
       );
     },
-    generatePDF() {
+    generatePDF(params) {
       // creates a new jsPDF object into this.pdf.doc
       // will call functions that the calling context has to provide
       // as specified in the templateData
-      const templateData = this.templateData;
-      const diagramTitle = this.title;
+      let templateData = params["templateData"];
+      let diagramTitle = params["diagramTitle"];
 
       this.pdf.doc = new jsPDF("l", "mm", templateData.properties.paperSize);
       // pdf width and height in millimeter (landscape)
@@ -293,9 +233,9 @@
                 e.offset || defaultOffset,
                 // use default width,height if they are missing in template definition
                 e.width ||
-                  (templateData.properties.paperSize === "a3" ? 318 : 230),
+                  (this.templateData.properties.paperSize === "a3" ? 318 : 230),
                 e.height ||
-                  (templateData.properties.paperSize === "a3" ? 104 : 110)
+                  (this.templateData.properties.paperSize === "a3" ? 104 : 110)
               );
               break;
             }
@@ -528,58 +468,6 @@
         color,
         text
       );
-    },
-    applyChange() {
-      if (this.form.template.hasOwnProperty("properties")) {
-        this.templateData = this.defaultTemplate;
-        return;
-      }
-      if (this.form.template) {
-        this.loadTemplates("/templates/diagram/" + this.form.template.name)
-          .then(response => {
-            this.prepareImages(response.data.template_data.elements).then(
-              values => {
-                values.forEach(v => {
-                  response.data.template_data.elements[v.index].url = v.url;
-                });
-                this.templateData = response.data.template_data;
-              }
-            );
-          })
-          .catch(e => {
-            const { status, data } = e.response;
-            displayError({
-              title: this.$gettext("Backend Error"),
-              message: `${status}: ${data.message || data}`
-            });
-          });
-      }
     }
-  },
-  mounted() {
-    this.templates[0] = this.defaultTemplate;
-    this.form.template = this.templates[0];
-    this.templateData = this.form.template;
-    HTTP.get("/templates/diagram", {
-      headers: {
-        "X-Gemma-Auth": localStorage.getItem("token"),
-        "Content-type": "text/xml; charset=UTF-8"
-      }
-    })
-      .then(response => {
-        if (response.data.length) {
-          this.templates = response.data;
-          this.form.template = this.templates[0];
-          this.templates[this.templates.length] = this.defaultTemplate;
-          this.applyChange();
-        }
-      })
-      .catch(e => {
-        const { status, data } = e.response;
-        displayError({
-          title: this.$gettext("Backend Error"),
-          message: `${status}: ${data.message || data}`
-        });
-      });
   }
 };