view client/src/components/fairway/Fairwayprofile.vue @ 4809:b6d8570b8480

client: image-export: add diagram legend
author Fadi Abbud <fadi.abbud@intevation.de>
date Tue, 29 Oct 2019 16:29:29 +0100
parents db450fcc8ed7
children ad2ad7bae4a6
line wrap: on
line source

<template>
  <div class="d-flex flex-column flex-fill">
    <UIBoxHeader icon="chart-area" :title="title" :closeCallback="close" />
    <div class="d-flex flex-fill" v-if="openLayersMap()">
      <DiagramLegend>
        <div class="legend">
          <span
            style="background-color: #5995ff; width: 20px; height: 20px;"
          ></span>
          <span class="fix-trans-space" style="display:inline;" v-translate
            >Water</span
          >
        </div>
        <div class="legend">
          <span
            :style="
              'width: 16px; height: 16px; background:' +
                this.getLayerStyle(1).fillColor +
                '; border: dotted 2px ' +
                this.getLayerStyle(1).strokeColor +
                '; background-clip: padding-box; box-sizing: content-box;'
            "
          ></span>
          <span class="fix-trans-space" style="display:inline;" v-translate
            >Fairway (LOS 1)</span
          >
        </div>
        <div class="legend">
          <span
            :style="
              'width: 16px; height: 16px; background:' +
                this.getLayerStyle(2).fillColor +
                '; border: dashed 2px ' +
                this.getLayerStyle(2).strokeColor +
                '; background-clip: padding-box; box-sizing: content-box;'
            "
          ></span>
          <span class="fix-trans-space" style="display:inline;" v-translate
            >Fairway (LOS 2)</span
          >
        </div>
        <div class="legend">
          <span
            :style="
              'width: 16px; height: 16px; background:' +
                this.getLayerStyle(3).fillColor +
                '; border: solid 2px ' +
                this.getLayerStyle(3).strokeColor +
                '; background-clip: padding-box; box-sizing: content-box;'
            "
          ></span>
          <span class="fix-trans-space" style="display:inline;" v-translate
            >Fairway (LOS 3)</span
          >
        </div>
        <div class="legend">
          <span
            style="width: 14px; height: 14px; background-color: #4a2f06; border: solid 3px black; background-clip: padding-box; box-sizing: content-box;"
          ></span>
          <span class="fix-trans-space" style="display:inline;" v-translate
            >Sediment</span
          >
        </div>
        <div class="legend">
          <span
            style="width: 14px; height: 14px; background-color: rgba(74, 47, 6, 0.6); border: solid 3px #943007; background-clip: padding-box; box-sizing: content-box;"
          ></span>
          <span class="fix-trans-space" style="display:inline;" v-translate
            >Sediment (Compare)</span
          >
        </div>
        <div>
          <select
            v-model="form.template"
            @change="applyChange"
            class="form-control d-block custom-select-sm w-100"
          >
            <option
              v-for="template in templates"
              :value="template"
              :key="template.name"
            >
              {{ template.name }}
            </option>
          </select>
          <button
            @click="downloadPDF"
            type="button"
            class="btn btn-sm btn-info d-block w-100 mt-2"
          >
            <translate>Export to PDF</translate>
          </button>
          <a
            @click="downloadImage('fairwaypng', title)"
            id="fairwaypng"
            class="btn btn-sm btn-info text-white d-block w-100 mt-2"
            :download="`${fileName}.png`"
          >
            <translate>Export as Image</translate>
          </a>
        </div>
      </DiagramLegend>
      <div
        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">
          <translate>No data available.</translate>
        </div>
      </div>
    </div>
  </div>
</template>

<style lang="sass" scoped>
.direction-indicator
  width: 70px
  height: 0
  border-top: dashed 2px #333
  position: absolute
  bottom: 50px
  left: 115px
  margin-left: -35px
  &::after
    content: ""
    width: 0
    height: 0
    border-width: 10px
    border-top-width: 5px
    border-bottom-width: 5px
    border-style: solid
    border-color: transparent
    border-left-color: #333
    position: absolute
    right: -20px
    top: -6px
</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, 2019 by via donau
 *   – Österreichische Wasserstraßen-Gesellschaft mbH
 * Software engineering by Intevation GmbH
 *
 * Author(s):
 * Thomas Junk <thomas.junk@intevation.de>
 * Markus Kottländer <markus.kottlaender@intevation.de>
 * Fadi Abbud <fadi.abbud@intevation.de>
 */
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 {
      resizeListenerFunction: null,
      width: null,
      height: null,
      form: {
        template: null
      },
      templates: [],
      defaultTemplate: defaultDiagramTemplate,
      pdf: {
        doc: null,
        width: 32,
        height: 297
      },
      templateData: null
    };
  },
  computed: {
    ...mapGetters("map", ["openLayersMap"]),
    ...mapGetters("fairwayprofile", ["totalLength"]),
    ...mapState("fairwayprofile", [
      "additionalSurvey",
      "currentProfile",
      "startPoint",
      "endPoint",
      "fairwayData",
      "maxAlt",
      "selectedWaterLevel",
      "depth",
      "useCustomDepth"
    ]),
    ...mapState("bottlenecks", ["selectedSurvey", "selectedBottleneck"]),
    ...mapState("application", ["paneSetup"]),
    title() {
      let dates = [this.selectedSurvey.date_info];
      let waterlevelLabel =
        this.selectedWaterLevel === "ref"
          ? this.selectedSurvey.depth_reference
          : "Current";
      if (this.additionalSurvey) dates.push(this.additionalSurvey.date_info);
      dates.map(d => this.$options.filters.dateTime(d, true));
      return `${this.$gettext("Fairwayprofile")}: ${
        this.selectedBottleneck
      } (${dates.join(
        ", "
      )}) WL: ${waterlevelLabel} (${this.$options.filters.waterlevel(
        this.waterlevel
      )} m)`;
    },
    currentData() {
      if (
        !this.selectedSurvey ||
        !this.currentProfile.hasOwnProperty(this.selectedSurvey.date_info)
      )
        return [];
      return this.currentProfile[this.selectedSurvey.date_info].points;
    },
    additionalData() {
      if (
        !this.additionalSurvey ||
        !this.currentProfile.hasOwnProperty(this.additionalSurvey.date_info)
      )
        return [];
      return this.currentProfile[this.additionalSurvey.date_info].points;
    },
    bottleneck() {
      return this.openLayersMap()
        .getLayer("BOTTLENECKS")
        .getSource()
        .getFeatures()
        .find(f => f.get("objnam") === this.selectedBottleneck);
    },
    waterlevel() {
      return this.selectedWaterLevel === "ref"
        ? this.refWaterlevel
        : this.bottleneck.get("gm_waterlevel");
    },
    refWaterlevel() {
      if (!this.selectedSurvey) return 0;
      return this.selectedSurvey.waterlevel_value;
    },
    fileName() {
      return this.downloadFilename(
        this.$gettext("Fairwayprofile"),
        this.selectedBottleneck
      );
    }
  },
  watch: {
    depth() {
      if (!this.useCustomDepth) return;
      this.drawDiagram();
    },
    useCustomDepth() {
      this.drawDiagram();
    },
    currentData() {
      this.drawDiagram();
    },
    additionalData() {
      this.drawDiagram();
    },
    width() {
      this.drawDiagram();
    },
    height() {
      this.drawDiagram();
    },
    waterLevels() {
      this.drawDiagram();
    },
    selectedWaterLevel() {
      this.drawDiagram();
    },
    fairwayData() {
      this.drawDiagram();
    },
    selectedBottleneck() {
      this.$store.commit("application/paneSetup", "DEFAULT");
    }
  },
  methods: {
    addLegendToCanvas(ctx, { width, height }) {
      let x = width / 12,
        y = height - 55;
      ctx.font = "12px sans-serif";
      ctx.textAlign = "start";

      ctx.beginPath();
      ctx.fillStyle = "#5995ff";
      ctx.strokeStyle = "#5995ff";
      ctx.arc(x, y, 8, 0, 2 * Math.PI);
      ctx.fill();
      ctx.stroke();
      ctx.fillStyle = "black";
      ctx.fillText(this.$gettext("Water"), x + 14, y + 5);
      ctx.closePath();

      ctx.beginPath();
      ctx.fillStyle = this.getLayerStyle(2).fillColor;
      ctx.strokeStyle = this.getLayerStyle(2).strokeColor;
      ctx.arc(x, (y += 20), 8, 0, 2 * Math.PI);
      ctx.setLineDash([0.8], 0);
      ctx.fill();
      ctx.stroke();
      ctx.fillStyle = "black";
      ctx.fillText(this.$gettext("Fairway (LOS 1)"), x + 14, y + 5);
      ctx.closePath();

      ctx.beginPath();
      ctx.fillStyle = this.getLayerStyle(2).fillColor;
      ctx.strokeStyle = this.getLayerStyle(2).strokeColor;
      ctx.arc(x, (y += 20), 8, 0, 2 * Math.PI);
      ctx.setLineDash([1.8], 0);
      ctx.fill();
      ctx.stroke();
      ctx.fillStyle = "black";
      ctx.fillText(this.$gettext("Fairway (LOS 2)"), x + 14, y + 5);
      ctx.closePath();

      ctx.beginPath();
      ctx.fillStyle = this.getLayerStyle(3).fillColor;
      ctx.strokeStyle = this.getLayerStyle(3).strokeColor;
      ctx.arc(x, (y += 20), 8, 0, 2 * Math.PI);
      ctx.setLineDash([]);
      ctx.fill();
      ctx.stroke();
      ctx.fillStyle = "black";
      ctx.fillText(this.$gettext("Fairway (LOS 3)"), x + 14, y + 5);
      ctx.closePath();

      ctx.beginPath();
      ctx.fillStyle = "#4a2e06";
      ctx.strokeStyle = "black";
      ctx.arc(x, (y += 20), 8, 0, 2 * Math.PI);
      ctx.setLineDash([]);
      ctx.fill();
      ctx.stroke();
      ctx.fillStyle = "black";
      ctx.fillText(this.$gettext("Sediment"), x + 14, y + 5);
      ctx.closePath();

      ctx.beginPath();
      ctx.fillStyle = "rgba(74, 47, 6, 0.6)";
      ctx.strokeStyle = "#943007";
      ctx.arc(x, (y += 20), 8, 0, 2 * Math.PI);
      ctx.setLineDash([]);
      ctx.fill();
      ctx.stroke();
      ctx.fillStyle = "black";
      ctx.fillText(this.$gettext("Sediment (compare)"), x + 14, y + 5);
      ctx.closePath();
    },
    close() {
      this.$store.commit(
        "application/paneSetup",
        this.paneSetup === "COMPARESURVEYS_FAIRWAYPROFILE"
          ? "COMPARESURVEYS"
          : "DEFAULT"
      );
      this.$store.dispatch("fairwayprofile/clearCurrentProfile");
    },
    getLayerStyle(los) {
      let style = this.openLayersMap()
        .getLayer("FAIRWAYDIMENSIONSLOS" + los)
        .getStyle()()[0];
      // use spread operator to clone arrays
      let fillColor = style.getFill().getColor();
      let strokeColor = style.getStroke().getColor();
      let strokeDash = style.getStroke().getLineDash();
      return { fillColor, strokeColor, 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.fileName + ".pdf");
    },

    // Diagram legend
    addDiagramLegend(position, offset, color) {
      let x = offset.x,
        y = offset.y;
      this.pdf.doc.setFontSize(10);
      let width =
        (this.pdf.doc.getStringUnitWidth("Sediment (Compare)") * 10) /
          (72 / 25.6) +
        4;
      if (["topright", "bottomright"].indexOf(position) !== -1) {
        x = this.pdf.width - offset.x - width;
      }
      if (["bottomright", "bottomleft"].indexOf(position) !== -1) {
        y = this.pdf.height - offset.y - this.getTextHeight(8);
      }

      this.pdf.doc.setTextColor(color);
      this.pdf.doc.setDrawColor("#5995ff");
      this.pdf.doc.setFillColor("#5995ff");
      this.pdf.doc.circle(x, y, 2, "FD");
      this.pdf.doc.text(x + 3, y + 1, this.$gettext("Water"));

      const toRGB = s => {
        let [, r, g, b] = s.match(/.*?(\d+), (\d+), (\d+), .*/);
        const toHex = n => {
          let val = parseInt(n).toString(16);
          if (val.length === 1) return `0${val}`;
          return val;
        };
        return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
      };

      const los1Color = toRGB(this.getLayerStyle(1).strokeColor);
      const los1Fill = toRGB(this.getLayerStyle(1).fillColor);
      const los2Color = toRGB(this.getLayerStyle(2).strokeColor);
      const los2Fill = toRGB(this.getLayerStyle(2).fillColor);
      const los3Color = toRGB(this.getLayerStyle(3).strokeColor);
      const los3Fill = toRGB(this.getLayerStyle(3).fillColor);

      this.pdf.doc.setLineDashPattern([0.8], 0);
      this.pdf.doc.setDrawColor(los1Color);
      this.pdf.doc.setFillColor(los1Fill);
      this.pdf.doc.circle(x, y + 5, 2, "FD");
      this.pdf.doc.text(x + 3, y + 6, this.$gettext("Fairway (LOS 1)"));

      this.pdf.doc.setLineDashPattern([1.8], 0);
      this.pdf.doc.setFillColor(los2Fill);
      this.pdf.doc.setDrawColor(los2Color);
      this.pdf.doc.circle(x, y + 10, 2, "FD");
      this.pdf.doc.text(x + 3, y + 11, this.$gettext("Fairway (LOS 2)"));

      this.pdf.doc.setLineDashPattern([], 0);
      this.pdf.doc.setFillColor(los3Fill);
      this.pdf.doc.setDrawColor(los3Color);
      this.pdf.doc.circle(x, y + 15, 2, "FD");
      this.pdf.doc.text(x + 3, y + 16, this.$gettext("Fairway (LOS 3)"));

      this.pdf.doc.setDrawColor("black");
      this.pdf.doc.setFillColor("#4a2e06");
      this.pdf.doc.circle(x, y + 20, 2, "FD");
      this.pdf.doc.text(x + 3, y + 21, this.$gettext("Sediment"));

      this.pdf.doc.setDrawColor("#943007");
      this.pdf.doc.setFillColor("#928269");
      this.pdf.doc.circle(x, y + 25, 2, "FD");
      this.pdf.doc.text(x + 3, y + 26, this.$gettext("Sediment (Compare)"));
    },
    getPrintLayout(svgHeight, svgWidth) {
      return {
        main: {
          top: Math.floor(0.08 * svgHeight),
          right: Math.floor(0.08 * svgWidth),
          bottom: Math.floor(0.2 * svgHeight),
          left: Math.floor(0.08 * svgWidth)
        }
      };
    },
    drawDiagram() {
      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.width);
      this.renderTo({
        element: ".diagram-container",
        dimensions: this.getDimensions({
          svgWidth: this.width,
          svgHeight: this.height,
          ...layout
        })
      });
    },
    renderTo({ element, dimensions }) {
      let svg = d3.select(element).append("svg");
      svg.attr("width", "100%");
      svg.attr("height", "100%");
      svg
        .append("g")
        .append("rect")
        .attr("width", "100%")
        .attr("height", "100%")
        .attr("fill", "#ffffff");
      const width = dimensions.width;
      const height = dimensions.mainHeight;
      const offsetY = 15;
      const currentData = this.currentData;
      const additionalData = this.additionalData;
      const { xScale, yScaleRight, graph } = this.generateScalesAndGraph({
        svg,
        height,
        width,
        dimensions,
        offsetY
      });
      this.drawWaterlevel({ graph, xScale, yScaleRight, height, offsetY });
      this.drawLabels({ graph, dimensions });
      if (currentData) {
        this.drawProfile({
          graph,
          xScale,
          yScaleRight,
          currentData,
          height,
          color: GROUND_COLOR,
          strokeColor: "black",
          opacity: 1,
          offsetY
        });
      }
      if (additionalData) {
        this.drawProfile({
          graph,
          xScale,
          yScaleRight,
          currentData: additionalData,
          height,
          color: GROUND_COLOR,
          strokeColor: "#943007",
          opacity: 0.6,
          offsetY
        });
      }
      this.drawFairway({ graph, xScale, yScaleRight, offsetY });
    },
    drawFairway({ graph, xScale, yScaleRight, offsetY }) {
      if (this.fairwayData === undefined) {
        return;
      }
      for (let data of this.fairwayData) {
        data.coordinates.forEach(coordinates => {
          const [startPoint, endPoint, depth] = coordinates;
          const referenceDepth =
            this.maxAlt * 1.1 + (this.waterlevel - this.refWaterlevel) / 100;
          let customdepth =
            this.depth < referenceDepth ? this.depth : referenceDepth;
          let fairwayArea = d3
            .area()
            .x(function(d) {
              return xScale(d.x);
            })
            .y0(yScaleRight(0))
            .y1(function(d) {
              return yScaleRight(d.y);
            });
          let strokColor = this.getLayerStyle(data.los).strokeColor;
          // Convert stroke value to rgb() and opacity to pass them separately
          let [r, g, b, opacity] = strokColor
            .substring(5, strokColor.length - 1)
            .split(",");
          let rgb = `rgb(${r}, ${g}, ${b})`;
          graph
            .append("path")
            .datum([
              { x: startPoint, y: this.useCustomDepth ? customdepth : depth },
              { x: endPoint, y: this.useCustomDepth ? customdepth : depth }
            ])
            .attr("fill", `${this.getLayerStyle(data.los).fillColor}`)
            .attr("stroke", rgb)
            .attr("stroke-opacity", opacity)
            .attr("stroke-dasharray", this.getLayerStyle(data.los).strokeDash)
            .attr("d", fairwayArea)
            .attr("transform", `translate(0 ${-offsetY})`);
        });
      }
    },
    drawLabels({ graph, dimensions }) {
      graph
        .append("text")
        .attr("transform", ["rotate(-90)"])
        .attr("y", dimensions.width + Math.floor(0.06 * dimensions.width))
        .attr("x", -dimensions.mainHeight / 2)
        .attr("fill", "black")
        .style("text-anchor", "middle")
        .text(this.$gettext("Depth [m]"));
      graph
        .append("text")
        .attr("transform", ["rotate(-90)"])
        .attr("y", -1 * Math.floor(0.065 * dimensions.width))
        .attr("x", -dimensions.mainHeight / 2)
        .attr("fill", "black")
        .style("text-anchor", "middle")
        .text(this.$gettext("Waterlevel [m]"));
      graph
        .append("text")
        .attr("y", 0)
        .attr("x", 0)
        .attr("dy", "1em")
        .attr("fill", "black")
        .style("text-anchor", "middle")
        .attr("transform", [
          `translate(${dimensions.width / 2} ${dimensions.mainHeight})`,
          "rotate(0)"
        ])
        .text(this.$gettext("Width [m]"));
    },
    generateScalesAndGraph({ svg, height, width, dimensions, offsetY }) {
      let xScale = d3
        .scaleLinear()
        .domain([0, this.totalLength])
        .rangeRound([0, width]);

      let yScaleRight = d3
        .scaleLinear()
        .domain([
          this.maxAlt * 1.1 + (this.waterlevel - this.refWaterlevel) / 100,
          -(this.maxAlt * 0.1)
        ])
        .rangeRound([height, 0]);

      let yScaleLeft = d3
        .scaleLinear()
        .domain([
          this.waterlevel -
            (this.maxAlt * 100 + (this.waterlevel - this.refWaterlevel)),
          this.waterlevel + this.maxAlt * 0.1 * 100
        ])
        .rangeRound([height, 0]);

      let xAxis = d3
        .axisBottom(xScale)
        .tickSizeOuter(0)
        .ticks(5);
      let yAxisRight = d3
        .axisRight()
        .tickSizeOuter(0)
        .tickSizeInner(5)
        .scale(yScaleRight);
      let yAxisLeft = d3
        .axisLeft()
        .tickSizeOuter(0)
        .tickSizeInner(5)
        .scale(yScaleLeft)
        .tickFormat(d => this.$options.filters.waterlevel(d));

      let graph = svg
        .append("g")
        .attr(
          "transform",
          "translate(" +
            dimensions.mainMargin.left +
            "," +
            dimensions.mainMargin.top +
            ")"
        );
      graph
        .append("g")
        .attr("transform", `translate(0 ${height - offsetY})`)
        .call(xAxis)
        .selectAll(".tick text")
        .attr("fill", "black")
        .select(function() {
          return this.parentNode;
        })
        .selectAll(".tick line")
        .attr("stroke", "black");
      graph
        .append("g")
        .attr("transform", `translate(${width} ${-offsetY})`)
        .call(yAxisRight)
        .selectAll(".tick text")
        .attr("fill", "black")
        .select(function() {
          return this.parentNode;
        })
        .selectAll(".tick line")
        .attr("stroke", "black");
      graph
        .append("g")
        .attr("transform", `translate(0 ${-offsetY})`)
        .call(yAxisLeft)
        .selectAll(".tick text")
        .attr("fill", "black")
        .select(function() {
          return this.parentNode;
        })
        .selectAll(".tick line")
        .attr("stroke", "black");

      graph.selectAll(".domain").attr("stroke", "black");
      return { xScale, yScaleRight, graph };
    },
    drawWaterlevel({ graph, xScale, yScaleRight, height, offsetY }) {
      let waterArea = d3
        .area()
        .x(function(d) {
          return xScale(d.x);
        })
        .y0(height)
        .y1(function(d) {
          return yScaleRight(d.y);
        });
      graph
        .append("path")
        .datum([{ x: 0, y: 0 }, { x: this.totalLength, y: 0 }])
        .attr("fill-opacity", 0.65)
        .attr("fill", WATER_COLOR)
        .attr("stroke", "transparent")
        .attr("d", waterArea)
        .attr("transform", `translate(0 ${-offsetY})`);
    },
    drawProfile({
      graph,
      xScale,
      yScaleRight,
      currentData,
      height,
      color,
      strokeColor,
      opacity,
      offsetY
    }) {
      for (let part of currentData) {
        let profileLine = d3
          .line()
          .x(d => {
            return xScale(d.x);
          })
          .y(d =>
            yScaleRight(d.y + (this.waterlevel - this.refWaterlevel) / 100)
          );
        let profileArea = d3
          .area()
          .x(function(d) {
            return xScale(d.x);
          })
          .y0(height)
          .y1(d =>
            yScaleRight(d.y + (this.waterlevel - this.refWaterlevel) / 100)
          );
        graph
          .append("path")
          .datum(part)
          .attr("fill", color)
          .attr("stroke", "transparent")
          .attr("fill-opacity", opacity)
          .attr("d", profileArea)
          .attr("transform", `translate(0 ${-offsetY})`);
        graph
          .append("path")
          .datum(part)
          .attr("fill", "none")
          .attr("stroke", strokeColor)
          .attr("stroke-linejoin", "round")
          .attr("stroke-linecap", "round")
          .attr("stroke-width", 3)
          .attr("stroke-opacity", opacity)
          .attr("fill-opacity", 0)
          .attr("d", profileLine)
          .attr("transform", `translate(0 ${-offsetY})`);
      }
    },
    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>