view client/src/components/fairway/AvailableFairwayDepthLNWL.vue @ 3965:2aaa1948b525 diagram-cleanup

client: diagrams: moved downloadPDF function to mixin and thereby unified how titles are created
author Markus Kottlaender <markus@intevation.de>
date Fri, 12 Jul 2019 15:32:15 +0200
parents afc7bca44df4
children 2f024d6189ca
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">
      <DiagramLegend>
        <div v-for="(entry, index) in legendLNWL" :key="index" class="legend">
          <span
            :style="
              `${legendStyle(
                index
              )}; border-radius: 0.25rem; width: 40px; height: 20px;`
            "
          ></span>
          {{ entry }}
        </div>
        <div>
          <select
            @change="applyChange"
            v-model="form.template"
            class="form-control d-block custom-select-sm w-100 mt-2"
          >
            <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
            :href="dataLink"
            :download="csvFileName"
            class="mt-2 btn btn-sm btn-info w-100"
            >Download CSV</a
          >
        </div>
      </DiagramLegend>
      <div
        class="d-flex flex-fill justify-content-center align-items-center"
        :id="containerId"
      >
        <div v-if="!fwLNWLData.length">
          <translate>No data available.</translate>
        </div>
      </div>
    </div>
  </div>
</template>

<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 } from "vuex";
import { diagram, pdfgen, templateLoader } from "@/lib/mixins";
import filters from "@/lib/filters.js";

export default {
  mixins: [diagram, pdfgen, templateLoader],
  data() {
    return {
      containerId: "availablefairwaydepthlnwl-diagram-container",
      scalePaddingLeft: 60,
      scalePaddingRight: 0,
      paddingTop: 25,
      colors: {
        afd: ["#3636ff", "#f49b7f", "#e15472"],
        lnwl: "#97ddf3"
      }
    };
  },
  computed: {
    ...mapState("fairwayavailability", [
      "selectedFairwayAvailabilityFeature",
      "fwLNWLData",
      "from",
      "to",
      "frequency",
      "csv",
      "depthlimit1",
      "depthlimit2",
      "widthlimit1",
      "widthlimit2"
    ]),
    legendLNWL() {
      const d = [this.depthlimit1, this.depthlimit2].sort();
      const w = [this.widthlimit1, this.widthlimit2].sort();
      const lowerBound = [d[0], w[0]].filter(x => x).join(", ");
      const upperBound = [d[1], w[1]].filter(x => x).join(", ");
      return [
        `> LDC`,
        `< ${lowerBound}`,
        `< ${upperBound}`,
        `>= ${upperBound}`
      ];
    },
    dataLink() {
      return `data:text/csv;charset=utf-8, ${encodeURIComponent(this.csv)}`;
    },
    csvFileName() {
      return `${this.$gettext("fairwayavailabilityLNWL")}-${
        this.featureName
      }-${filters.surveyDate(this.fromDate)}-${filters.surveyDate(
        this.toDate
      )}-${this.$gettext(this.frequency)}-.csv`;
    },
    fromDate() {
      return this.from;
    },
    toDate() {
      return this.to;
    },
    availability() {
      return this.plainAvailability;
    },
    title() {
      return `Available Fairway Depth vs LNWL: ${
        this.featureName
      } (${filters.surveyDate(this.fromDate)} - ${filters.surveyDate(
        this.toDate
      )}) ${this.$gettext(this.frequency)}`;
    },
    featureName() {
      if (this.selectedFairwayAvailabilityFeature == null) return "";
      return this.selectedFairwayAvailabilityFeature.properties.name;
    }
  },
  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]};`
      };
      return style[index];
    },
    addDiagramLegend(position, offset, color) {
      let x = offset.x,
        y = offset.y;
      this.pdf.doc.setFontSize(10);
      let width =
        (this.pdf.doc.getStringUnitWidth(">= LDC") * 10) / (72 / 25.6) + 15;
      // if position is on the right, x needs to be calculate with pdf width and
      // the size of the element
      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(6);
      }
      this.pdf.doc.setTextColor(color);
      this.pdf.doc.setDrawColor(this.colors.lnwl);
      this.pdf.doc.setFillColor(this.colors.lnwl);
      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.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.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.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() {
      return {
        main: { top: 0, right: 20, bottom: 50, left: 20 }
      };
    },
    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 dimensions = this.getDimensions({
        svgHeight,
        svgWidth,
        ...layout
      });
      d3.select("#" + this.containerId + " svg").remove();
      this.renderTo({ element: "#" + this.containerId, dimensions });
    },
    drawTooltip(diagram) {
      diagram
        .append("text")
        .text("")
        .attr("font-size", "0.8em")
        .attr("opacity", 0)
        .attr("id", "tooltip");
    },
    renderTo({ element, dimensions }) {
      let diagram = d3
        .select(element)
        .append("svg")
        .attr("width", "100%")
        .attr("height", "100%");
      diagram = diagram.append("g");
      const yScale = d3
        .scaleLinear()
        .domain([0, 100])
        .range([dimensions.mainHeight - 30, 0]);
      this.drawScaleLabel({ diagram, dimensions });
      this.drawScale({ diagram, dimensions, yScale });
      this.drawBars({ diagram, yScale, dimensions });
      this.drawTooltip(diagram);
    },
    drawBars({ diagram, yScale, dimensions }) {
      if (this.fwLNWLData) {
        const widthPerItem = Math.min(
          (dimensions.width - this.scalePaddingLeft - this.scalePaddingRight) /
            this.fwLNWLData.length,
          180
        );
        const spaceBetween = widthPerItem * 0.2;
        const afdWidth = widthPerItem * 0.5;
        const ldcWidth = widthPerItem * 0.3;
        this.fwLNWLData.forEach((data, i) => {
          this.drawLNWL(
            data,
            i,
            diagram,
            spaceBetween,
            widthPerItem,
            ldcWidth,
            yScale
          );
          this.drawAFD(
            data,
            i,
            diagram,
            spaceBetween,
            widthPerItem,
            ldcWidth,
            yScale,
            afdWidth
          );
          this.drawLabel(data.date, i, diagram, widthPerItem, dimensions);
        });
      }
    },
    drawLabel(date, i, diagram, widthPerItem, dimensions) {
      diagram
        .append("text")
        .text(date)
        .attr("text-anchor", "middle")
        .attr("font-size", 10)
        .attr(
          "transform",
          `translate(${this.scalePaddingLeft +
            widthPerItem * i +
            widthPerItem / 2} ${dimensions.mainHeight + this.paddingTop - 5})`
        );
    },
    drawAFD(
      data,
      i,
      diagram,
      spaceBetween,
      widthPerItem,
      ldcWidth,
      yScale,
      afdWidth
    ) {
      let afd = diagram
        .append("g")
        .attr(
          "transform",
          `translate(${this.scalePaddingLeft +
            spaceBetween / 2 +
            widthPerItem * i +
            ldcWidth})`
        );
      afd
        .selectAll("rect")
        .data([data.above, data.between, data.below])
        .enter()
        .append("rect")
        .on("mouseover", function() {
          d3.select(this).attr("opacity", "0.8");
          d3.select("#tooltip").attr("opacity", 1);
        })
        .on("mouseout", function() {
          d3.select(this).attr("opacity", 1);
          d3.select("#tooltip").attr("opacity", 0);
        })
        .on("mousemove", function(d) {
          let y = d3.mouse(this)[1];
          const dy = document
            .querySelector(".diagram-container")
            .getBoundingClientRect().left;
          d3.select("#tooltip")
            .text(Math.round(d))
            .attr("y", y - 10)
            .attr("x", d3.event.pageX - dy);
          //d3.event.pageX gives coordinates relative to SVG
          //dy gives offset of svg on page
        })
        .attr("height", d => {
          return yScale(0) - yScale(d);
        })
        .attr("y", (d, i) => {
          if (i === 0) {
            return yScale(d);
          }
          if (i === 1) {
            return yScale(data.above + d);
          }
          if (i === 2) {
            return yScale(data.above + data.between + d);
          }
        })
        .attr("transform", `translate(0 ${this.paddingTop})`)
        .attr("width", afdWidth)
        .attr("fill", (d, i) => {
          return this.colors.afd[i];
        });
    },
    drawLNWL(data, i, diagram, spaceBetween, widthPerItem, ldcWidth, yScale) {
      let lnwl = diagram
        .append("g")
        .attr(
          "transform",
          `translate(${this.scalePaddingLeft +
            spaceBetween / 2 +
            widthPerItem * i})`
        );
      lnwl
        .append("rect")
        .datum([data.ldc])
        .on("mouseover", function() {
          d3.select(this).attr("opacity", "0.8");
          d3.select("#tooltip").attr("opacity", 1);
        })
        .on("mouseout", function() {
          d3.select(this).attr("opacity", 1);
          d3.select("#tooltip").attr("opacity", 0);
        })
        .on("mousemove", function(d) {
          let y = d3.mouse(this)[1];
          const dy = document
            .querySelector(".diagram-container")
            .getBoundingClientRect().left;
          d3.select("#tooltip")
            .text(Math.round(d[0]))
            .attr("y", y - 10)
            .attr("x", d3.event.pageX - dy);
          //d3.event.pageX gives coordinates relative to SVG
          //dy gives offset of svg on page
        })
        .attr("height", d => {
          return yScale(0) - yScale(d);
        })
        .attr("y", d => {
          return yScale(d);
        })
        .attr("transform", `translate(0 ${this.paddingTop})`)
        .attr("width", ldcWidth)
        .attr("fill", () => {
          return this.colors.lnwl;
        });
    },
    drawScaleLabel({ diagram, dimensions }) {
      diagram
        .append("text")
        .text(this.$gettext("Percent"))
        .attr("text-anchor", "middle")
        .attr("x", 0)
        .attr("y", 0)
        .attr("dy", "20")
        // translate a few mm to the right to allow for slightly higher letters
        .attr(
          "transform",
          `translate(2, ${(dimensions.mainHeight + this.paddingTop) /
            2}), rotate(-90)`
        );
    },
    drawScale({ diagram, dimensions, yScale }) {
      const yAxisLeft = d3
        .axisLeft()
        .tickSizeInner(
          dimensions.width - this.scalePaddingLeft - this.scalePaddingRight
        )
        .tickSizeOuter(0)
        .scale(yScale);
      const yAxisRight = d3
        .axisRight()
        .tickSizeInner(
          dimensions.width - this.scalePaddingLeft - this.scalePaddingRight
        )
        .tickSizeOuter(0)
        .scale(yScale);

      diagram
        .append("g")
        .attr(
          "transform",
          `translate(${dimensions.width - this.scalePaddingRight} ${
            this.paddingTop
          })`
        )
        .call(yAxisLeft)
        .selectAll(".tick text")
        .attr("fill", "black")
        .attr("font-size", 10)
        .attr("dy", 3)
        .attr("dx", -3)
        .select(function() {
          return this.parentNode;
        })
        .selectAll(".tick line")
        .attr("stroke-dasharray", 5)
        .attr("stroke", "#ccc")
        .select(function() {
          return this.parentNode;
        })
        .filter(d => d === 0)
        .selectAll(".tick line")
        .attr("stroke-dasharray", "none")
        .attr("stroke", "#333");
      diagram
        .append("g")
        .attr(
          "transform",
          `translate(${this.scalePaddingLeft} ${this.paddingTop})`
        )
        .call(yAxisRight)
        .selectAll(".tick text")
        .attr("fill", "black")
        .attr("font-size", 10)
        .attr("dy", 3)
        .attr("dx", 3)
        .select(function() {
          return this.parentNode;
        })
        .selectAll(".tick line")
        .attr("stroke", "transparent");
      diagram.selectAll(".domain").attr("stroke", "black");
    }
  },
  watch: {
    fwLNWLData() {
      this.drawDiagram();
    }
  }
};
</script>