changeset 3782:d36ccff8de5f yworks-svg2pdf

pdf: waterlevel diagram with svg2pdf
author Thomas Junk <thomas.junk@intevation.de>
date Tue, 02 Jul 2019 14:53:57 +0200
parents 217fe7b4f909
children 40b962c4190b
files client/src/components/gauge/Waterlevel.vue
diffstat 1 files changed, 136 insertions(+), 138 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/components/gauge/Waterlevel.vue	Mon Jul 01 17:23:07 2019 +0200
+++ b/client/src/components/gauge/Waterlevel.vue	Tue Jul 02 14:53:57 2019 +0200
@@ -59,7 +59,6 @@
             <translate>Export as CSV</translate>
           </a>
 
-          <!--
           <button
             @click="downloadSVG"
             type="button"
@@ -68,7 +67,6 @@
           >
             <translate>Export as SVG</translate>
           </button>
-          -->
         </div>
       </DiagramLegend>
       <div
@@ -82,7 +80,7 @@
     </div>
     <div
       id="pdfContainer"
-      style="position: absolute; z-index: -1; top: -9999px;"
+      style="position: absolute; z-index: -1; top: -9999px; width: 387mm; height: 100mm;"
     ></div>
   </div>
 </template>
@@ -110,7 +108,7 @@
 import { endOfDay } from "date-fns";
 import debounce from "debounce";
 import jsPDF from "jspdf-yworks";
-import canvg from "canvg";
+import svg2pdf from "svg2pdf.js";
 import { saveAs } from "file-saver";
 import { pdfgen } from "@/lib/mixins";
 import { HTTP } from "@/lib/http";
@@ -128,7 +126,6 @@
   data() {
     return {
       containerId: "waterlevel-diagram-container",
-      resizeListenerFunction: null,
       svg: null,
       diagram: null,
       navigation: null,
@@ -253,40 +250,35 @@
     downloadPDF() {
       this.pdf.doc = new jsPDF(
         "l",
-        "mm",
+        "pt",
         this.templateData.properties.paperSize
       );
       // pdf width and height in millimeter (landscape)
       this.pdf.width =
-        this.templateData.properties.paperSize === "a3" ? 420 : 297;
+        this.templateData.properties.paperSize === "a3" ? 420 : 595.28;
       this.pdf.height =
-        this.templateData.properties.paperSize === "a3" ? 297 : 210;
+        this.templateData.properties.paperSize === "a3" ? 841.89 : 595.28;
       // check the template elements
       if (this.templateData) {
         let defaultFontSize = 11,
           defaultColor = "black",
-          defaultWidth = 70,
+          defaultWidth = 70 * 2.83,
           defaultTextColor = "black",
           defaultBorderColor = "white",
           defaultBgColor = "white",
           defaultRounding = 2,
-          defaultPadding = 2,
-          defaultOffset = { x: 0, y: 0 };
+          defaultPadding = 2;
+        //defaultOffset = { x: 0, y: 0 };
         this.templateData.elements.forEach(e => {
           switch (e.type) {
             case "diagram": {
-              this.addDiagram(
-                e.position,
-                e.offset || defaultOffset,
-                e.width,
-                e.height
-              );
+              this.addDiagram(e.position, this.calculateOffset(e.offset));
               break;
             }
             case "diagramlegend": {
               this.addDiagramLegend(
                 e.position,
-                e.offset || defaultOffset,
+                this.calculateOffset(e.offset),
                 e.color || defaultColor
               );
               break;
@@ -306,7 +298,7 @@
                 this.dateTo.toLocaleDateString();
               this.addDiagramTitle(
                 e.position,
-                e.offset || defaultOffset,
+                this.calculateOffset(e.offset),
                 e.fontsize || defaultFontSize,
                 e.color || defaultColor,
                 gaugeInfo
@@ -316,8 +308,8 @@
             case "text": {
               this.addText(
                 e.position,
-                e.offset || defaultOffset,
-                e.width || defaultWidth,
+                this.calculateOffset(e.offset),
+                e.width * 2.83 || defaultWidth,
                 e.fontsize || defaultFontSize,
                 e.color || defaultTextColor,
                 e.text || ""
@@ -329,18 +321,18 @@
                 e.url,
                 e.format || "",
                 e.position,
-                e.offset || defaultOffset,
-                e.width || 90,
-                e.height || 60
+                this.calculateOffset(e.offset),
+                e.width * 2.83 || 90 * 2.83,
+                e.height * 2.83 || 60 * 2.83
               );
               break;
             }
             case "box": {
               this.addBox(
                 e.position,
-                e.offset || defaultOffset,
-                e.width || 90,
-                e.height || 60,
+                this.calculateOffset(e.offset),
+                e.width * 2.83 || 90 * 2.83,
+                e.height * 2.83 || 60 * 2.83,
                 e.rounding === 0 || e.rounding ? e.rounding : defaultRounding,
                 e.color || defaultBgColor,
                 e.brcolor || defaultBorderColor
@@ -350,9 +342,9 @@
             case "textbox": {
               this.addTextBox(
                 e.position,
-                e.offset || defaultOffset,
-                e.width,
-                e.height,
+                this.calculateOffset(e.offset),
+                e.width * 2.83,
+                e.height * 2.83,
                 e.rounding === 0 || e.rounding ? e.rounding : defaultRounding,
                 e.padding || defaultPadding,
                 e.fontsize || defaultFontSize,
@@ -370,6 +362,19 @@
         this.selectedGauge.properties.objname + " Waterlevel-Diagram.pdf"
       );
     },
+    calculateOffset(offset) {
+      let tmp;
+      let defaultOffset = { x: 0, y: 0 };
+      if (offset) {
+        tmp = {
+          x: offset.x * 2.83,
+          y: offset.y * 2.83
+        };
+      } else {
+        tmp = defaultOffset;
+      }
+      return tmp;
+    },
     applyChange() {
       if (this.form.template.hasOwnProperty("properties")) {
         this.templateData = this.defaultTemplate;
@@ -394,54 +399,36 @@
           });
       }
     },
-    addDiagram(position, offset, width, height) {
+    addDiagram(position, offset) {
       let x = offset.x,
         y = offset.y;
-      // check if there are tow diagrams on screen
-      if (
-        ["GAUGE_WATERLEVEL_HYDROLOGICALCONDITIONS"].indexOf(this.paneSetup) !==
-        -1
-      ) {
-        this.containerId = "pdfContainer";
-        // set width and height
-        document.querySelector("#pdfContainer").style.width =
-          document.querySelector("#waterlevel-diagram-container").clientWidth *
-            2 +
-          document.querySelector("#diagramlegendId").clientWidth +
-          "px";
-        document.querySelector("#pdfContainer").style.height =
-          document.querySelector("#waterlevel-diagram-container").clientHeight +
-          "px";
-        this.drawDiagram();
-      }
-      var svg = document.getElementById(this.containerId).innerHTML;
-      if (svg) {
-        svg = svg.replace(/\r?\n|\r/g, "").trim();
-      }
-      this.containerId = "waterlevel-diagram-container";
-      var canvas = document.createElement("canvas");
-      canvas.width = window.innerWidth;
-      canvas.height = window.innerHeight / 2;
-      canvg(canvas, svg, {
-        ignoreMouse: true,
-        ignoreAnimation: true,
-        ignoreDimensions: true
-      });
-      var imgData = canvas.toDataURL("image/png");
+
       // 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 - width;
+        x =
+          this.pdf.width -
+          offset.x -
+          document.getElementById(this.containerId).clientWidth;
       }
       if (["bottomright", "bottomleft"].indexOf(position) !== -1) {
-        y = this.pdf.height - offset.y - height;
+        y =
+          this.pdf.height -
+          offset.y -
+          document.getElementById(this.containerId).clientHeight;
       }
-      this.pdf.doc.addImage(imgData, "PNG", x, y, width, height);
+
+      let svg = document.getElementById(this.containerId).firstElementChild;
+      svg2pdf(svg, this.pdf.doc, {
+        xOffset: x,
+        yOffset: y,
+        scale:
+          this.templateData.properties.paperSize === "a3"
+            ? 2 * (25.4 / 72)
+            : 1.5 * (25.4 / 72)
+      });
+
+      this.containerId = "waterlevel-diagram-container";
     },
     // Diagram legend
     addDiagramLegend(position, offset, color) {
@@ -461,17 +448,17 @@
       this.pdf.doc.setTextColor(color);
       this.pdf.doc.setDrawColor("white");
       this.pdf.doc.setFillColor("steelblue");
-      this.pdf.doc.circle(x, y, 2, "FD");
-      this.pdf.doc.text(x + 3, y + 1, "Waterlevel");
+      this.pdf.doc.circle(x, y, 2 * 2.8, "FD");
+      this.pdf.doc.text(x + 3 * 2.83, y + 1 * 2.83, "Waterlevel");
       this.pdf.doc.setFillColor("#dae6f0");
-      this.pdf.doc.circle(x, y + 5, 2, "FD");
+      this.pdf.doc.circle(x, y + 5 * 2.83, 2 * 2.83, "FD");
       this.pdf.doc.setFillColor("#e5ffe5");
-      this.pdf.doc.circle(x, y + 10, 2, "FD");
-      this.pdf.doc.text(x + 3, y + 11, "Navigable Range");
+      this.pdf.doc.circle(x, y + 10 * 2.83, 2 * 2.83, "FD");
+      this.pdf.doc.text(x + 3 * 2.83, y + 11 * 2.83, "Navigable Range");
       this.pdf.doc.setDrawColor("#90b4d2");
       this.pdf.doc.setFillColor("#90b4d2");
-      this.pdf.doc.circle(x, y + 5, 0.6, "FD");
-      this.pdf.doc.text(x + 3, y + 6, "Prediction");
+      this.pdf.doc.circle(x, y + 5 * 2.83, 0.6 * 2.83, "FD");
+      this.pdf.doc.text(x + 3 * 2.83, y + 6 * 2.83, "Prediction");
     },
     drawDiagram() {
       // remove old diagram and exit if necessary data is missing
@@ -565,9 +552,7 @@
 
       // static, don't need updater
       this.drawNavigationChart();
-      if (refWaterLevels) {
-        this.drawRefLines(refWaterLevels);
-      }
+      this.drawRefLines(refWaterLevels);
 
       updaters.push(this.drawNashSutcliffe(72));
       updaters.push(this.drawNashSutcliffe(48));
@@ -675,6 +660,10 @@
       this.svg.selectAll(".tick text").attr("fill", "black");
       this.svg.selectAll(".domain").attr("stroke", "black");
       this.svg
+        .selectAll(".domain")
+        .attr("stroke", "black")
+        .attr("fill", "none");
+      this.svg
         .selectAll(".zoom")
         .attr("cursor", "move")
         .attr("fill", "none")
@@ -687,6 +676,7 @@
         .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)");
@@ -732,23 +722,6 @@
       return { width, mainHeight, navHeight, mainMargin, navMargin };
     },
     getExtent(refWaterLevels) {
-      let waterlevelValues = [...this.waterlevels.map(wl => wl.waterlevel)];
-      if (refWaterLevels) {
-        waterlevelValues.push(
-          refWaterLevels.HDC + (refWaterLevels.HDC - refWaterLevels.LDC) / 8,
-          Math.max(
-            refWaterLevels.LDC - (refWaterLevels.HDC - refWaterLevels.LDC) / 4,
-            0
-          )
-        );
-      } else {
-        let delta = d3.max(waterlevelValues) - d3.min(waterlevelValues);
-        waterlevelValues.push(
-          d3.max(waterlevelValues) + delta * 0.1,
-          d3.min(waterlevelValues) - delta * 0.1
-        );
-      }
-
       return {
         // set min/max values for the date axis
         date: [
@@ -757,8 +730,24 @@
         ],
         // set min/max values for the waterlevel axis
         // including HDC (+ 1/8 HDC-LDC) and LDC (- 1/4 HDC-LDC)
-        // or, if no refWaterlevels exist, +-10% of delta between min and max wl
-        waterlevel: d3.extent(waterlevelValues)
+        waterlevel: d3.extent(
+          [
+            ...this.waterlevels,
+            {
+              waterlevel:
+                refWaterLevels.HDC +
+                (refWaterLevels.HDC - refWaterLevels.LDC) / 8
+            },
+            {
+              waterlevel: Math.max(
+                refWaterLevels.LDC -
+                  (refWaterLevels.HDC - refWaterLevels.LDC) / 4,
+                0
+              )
+            }
+          ],
+          d => d.waterlevel
+        )
       };
     },
     getScale() {
@@ -819,19 +808,22 @@
         domainLeft.setDate(domainLeft.getDate() - 1);
         domainRight.setDate(domainRight.getDate() + 1);
 
-        return (
-          d3
-            .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))
-            .curve(d3.curveLinear)
-            .isNext(this.isNext())
-            .pointAttrs({ r: 1.7 })
+        let lineChunked = d3
+          .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))
+          .curve(d3.curveLinear)
+          .isNext(this.isNext())
+          .pointAttrs({ r: 1.7 });
+        // to avoid creating empty clip-path element
+        if (this.hasPredictions) {
+          lineChunked
             .chunk(d => (d.predicted ? "predicted" : "line"))
-            .chunkDefinitions({ predicted: {} })
-        );
+            .chunkDefinitions({ predicted: {} });
+        }
+        return lineChunked;
       };
 
       this.diagram
@@ -845,37 +837,36 @@
       };
     },
     drawNavigationChart() {
+      let lineChunked = d3
+        .lineChunked()
+        .x(d => this.scale.x2(d.date))
+        .y(d => this.scale.y2(d.waterlevel))
+        .curve(d3.curveLinear)
+        .isNext(this.isNext())
+        .pointAttrs({ r: 1.7 });
+      // to avoid creating empty clip-path element
+      if (this.hasPredictions) {
+        lineChunked
+          .chunk(d => (d.predicted ? "predicted" : "line"))
+          .chunkDefinitions({ predicted: {} });
+      }
       this.navigation
         .append("g")
         .attr("class", "line")
         .datum(this.waterlevels)
-        .call(
-          d3
-            .lineChunked()
-            .x(d => this.scale.x2(d.date))
-            .y(d => this.scale.y2(d.waterlevel))
-            .curve(d3.curveLinear)
-            .isNext(this.isNext())
-            .pointAttrs({ r: 1.7 })
-            .chunk(d => (d.predicted ? "predicted" : "line"))
-            .chunkDefinitions({ predicted: {} })
-        );
+        .call(lineChunked);
     },
     drawNowLines() {
-      const now = new Date();
-      const nowCoords = [
-        { x: now, y: this.extent.waterlevel[0] },
-        { x: now, y: this.extent.waterlevel[1] }
-      ];
       const nowLine = d3
         .line()
         .x(d => this.scale.x(d.x))
         .y(d => this.scale.y(d.y));
+
       const nowLabel = selection => {
         selection.attr(
           "transform",
-          `translate(${this.scale.x(now)}, ${this.scale.y(
-            this.extent.waterlevel[1]
+          `translate(${this.scale.x(new Date())}, ${this.scale.y(
+            this.extent.waterlevel[1] - 16
           )})`
         );
       };
@@ -883,7 +874,10 @@
       // draw in main
       this.diagram
         .append("path")
-        .datum(nowCoords)
+        .datum([
+          { x: new Date(), y: this.extent.waterlevel[0] },
+          { x: new Date(), y: this.extent.waterlevel[1] - 20 }
+        ])
         .attr("class", "now-line")
         .attr("d", nowLine);
       this.diagram // label
@@ -896,7 +890,10 @@
       // draw in nav
       this.navigation
         .append("path")
-        .datum(nowCoords)
+        .datum([
+          { x: new Date(), y: this.extent.waterlevel[0] },
+          { x: new Date(), y: this.extent.waterlevel[1] - 20 }
+        ])
         .attr("class", "now-line")
         .attr(
           "d",
@@ -959,7 +956,7 @@
           this.diagram
             .append("path")
             .datum([
-              { x: this.extent.date[0], y: refWaterLevels[ref] },
+              { x: 0, y: refWaterLevels[ref] },
               { x: this.extent.date[1], y: refWaterLevels[ref] }
             ])
             .attr("class", ref.toLowerCase() + "-line")
@@ -1118,7 +1115,9 @@
       tooltip
         .append("rect")
         .attr("rx", "0.25em")
-        .attr("ry", "0.25em");
+        .attr("ry", "0.25em")
+        .attr("width", "0px")
+        .attr("height", "0px");
 
       // create container for multiple text rows
       const tooltipText = tooltip.append("text").attr("text-anchor", "middle");
@@ -1261,8 +1260,7 @@
     }
   },
   created() {
-    this.resizeListenerFunction = debounce(this.drawDiagram, 100);
-    window.addEventListener("resize", this.resizeListenerFunction);
+    window.addEventListener("resize", debounce(this.drawDiagram), 100);
   },
   mounted() {
     // Nasty but necessary if we don't want to use the updated hook to re-draw
@@ -1272,7 +1270,7 @@
     // 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);
+    setTimeout(this.drawDiagram, 50);
 
     this.templates[0] = this.defaultTemplate;
     this.form.template = this.templates[0];
@@ -1300,7 +1298,7 @@
       });
   },
   destroyed() {
-    window.removeEventListener("resize", this.resizeListenerFunction);
+    window.removeEventListener("resize", debounce(this.drawDiagram));
   }
 };
 </script>