Mercurial > gemma
view client/src/components/fairway/AvailableFairwayDepth.vue @ 4056:1ecfe5d015b0
Fixup rev. 4bf1c8d91bac: take new column into account in tests
author | Tom Gottfried <tom@intevation.de> |
---|---|
date | Thu, 25 Jul 2019 08:06:22 +0200 |
parents | 5eaa3d45757e |
children | 630b817d12b3 |
line wrap: on
line source
<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"> <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-1" > <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 ref="diagramContainer" :id="containerId" 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. * * 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 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 { defaultDiagramTemplate } from "@/lib/DefaultDiagramTemplate"; const hoursInDays = x => Math.round(x / 24); 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, 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", "fwData", "from", "to", "frequency", "csv", "depthlimit1", "depthlimit2", "widthlimit1", "widthlimit2" ]), legend() { 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(", "); let result; if (this.depthlimit1 !== this.depthlimit2) { result = [ `> LDC`, `>= ${upperBound}`, `< ${upperBound}`, `< ${lowerBound}` ]; } else { result = [`> LDC`, `>= ${upperBound}`, `< ${upperBound}`]; } return result; }, dataLink() { return `data:text/csv;charset=utf-8, ${encodeURIComponent(this.csv)}`; }, csvFileName() { return `${this.$gettext("fairwayavailability")}-${ this.featureName }-${filters.surveyDate(this.fromDate)}-${filters.surveyDate( this.toDate )}-${this.$gettext(this.frequency)}-.csv`; }, frequencyToRange() { const frequencies = { [FREQUENCIES.MONTHLY]: [-33, 33], [FREQUENCIES.QUARTERLY]: [-93, 93], [FREQUENCIES.YEARLY]: [-370, 370] }; return frequencies[this.frequency]; }, fromDate() { return this.from; }, toDate() { return this.to; }, availability() { return this.plainAvailability; }, title() { return `Available Fairway Depth: ${ 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: { 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; 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); } if (this.legend[3]) { this.pdf.doc.setTextColor(color); 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(this.$options.COLORS.HIGHEST); this.pdf.doc.setFillColor(this.$options.COLORS.HIGHEST); 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(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(this.$options.COLORS.REST[0]); this.pdf.doc.setFillColor(this.$options.COLORS.REST[0]); 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); } else { this.pdf.doc.setTextColor(color); 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(this.$options.COLORS.HIGHEST); this.pdf.doc.setFillColor(this.$options.COLORS.HIGHEST); 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(this.$options.COLORS.REST[0]); this.pdf.doc.setFillColor(this.$options.COLORS.REST[0]); 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); } }, legendStyle(index) { if (this.depthlimit1 === this.depthlimit2) { let result = [ `background-color: ${this.$options.COLORS.LDC};`, `background-color: ${this.$options.COLORS.HIGHEST};` ]; this.fwData[0].lowerLevels.forEach((e, i) => { result.push(`background-color: ${this.$options.COLORS.REST[i]};`); }); return result[index]; } return [ `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(svgHeight) { return { 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(svgHeight); const dimensions = this.getDimensions({ svgHeight, svgWidth, ...layout }); d3.select(".diagram-container svg").remove(); this.renderTo({ element: ".diagram-container", dimensions }); }, renderTo({ element, dimensions }) { const diagram = d3 .select(element) .append("svg") .attr("width", "100%") .attr("height", "100%"); diagram.append("g"); const yScale = d3 .scaleLinear() .domain(this.frequencyToRange) .range([dimensions.mainHeight - 30, 0]); this.drawScaleLabel({ diagram, dimensions }); this.drawScale({ diagram, dimensions, yScale }); this.drawBars({ diagram, yScale, dimensions }); this.drawTooltip(diagram); }, drawTooltip(diagram) { diagram .append("text") .text("") .attr("font-size", "0.8em") .attr("opacity", 0) .attr("id", "tooltip"); }, drawBars({ diagram, yScale, dimensions }) { const widthPerItem = Math.min( (dimensions.width - this.scalePaddingLeft - this.scalePaddingRight) / this.fwData.length, 180 ); const spaceBetween = widthPerItem * 0.2; const ldcOffset = widthPerItem * 0.1; const everyBar = diagram .selectAll("g.bars") .data(this.fwData) .enter() .append("g") .attr("class", "bars") .attr("transform", (d, i) => { const dx = this.scalePaddingLeft + i * widthPerItem; return `translate(${dx})`; }); this.drawSingleBars({ everyBar, yScale, dimensions, widthPerItem, spaceBetween, ldcOffset }); this.drawLabelPerBar({ everyBar, dimensions, widthPerItem }); }, drawSingleBars({ everyBar, yScale, widthPerItem, spaceBetween, ldcOffset }) { this.drawLDC({ everyBar, yScale, widthPerItem, spaceBetween, ldcOffset }); this.drawHighestLevel({ everyBar, yScale, widthPerItem, spaceBetween, ldcOffset }); this.drawLowerLevels({ everyBar, yScale, widthPerItem, spaceBetween, ldcOffset }); }, drawLowerLevels({ everyBar, yScale, widthPerItem, spaceBetween, ldcOffset }) { everyBar .selectAll("g.bars") .data(d => d.lowerLevels) .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; const value = Number.parseFloat(hoursInDays(d.height)).toFixed(2); d3.select("#tooltip") .text(Math.round(value)) .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("y", d => { return ( 2 * yScale(0) - yScale(hoursInDays(d.translateY)) + this.paddingTop ); }) .attr("height", d => { return yScale(0) - yScale(hoursInDays(d.height)); }) .attr("x", ldcOffset + spaceBetween / 2) .attr("width", widthPerItem - ldcOffset - spaceBetween) .attr("fill", (d, i) => { return this.$options.COLORS.REST[i]; }); }, fnheight({ name, yScale }) { return d => yScale(0) - yScale(hoursInDays(d[name])); }, drawLDC({ everyBar, yScale, widthPerItem, spaceBetween, ldcOffset }) { const height = this.fnheight({ name: "ldc", yScale }); everyBar .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; const value = Number.parseFloat(hoursInDays(d.ldc)).toFixed(2); d3.select("#tooltip") .text(Math.round(value)) .attr("y", y - 50) .attr("x", d3.event.pageX - dy); //d3.event.pageX gives coordinates relative to SVG //dy gives offset of svg on page }) .attr("y", yScale(0)) .attr("height", height) .attr("x", spaceBetween / 2) .attr("width", widthPerItem - ldcOffset - spaceBetween) .attr( "transform", d => `translate(0 ${this.paddingTop + -1 * height(d)})` ) .attr("fill", this.$options.COLORS.LDC) .attr("id", "ldc"); }, drawHighestLevel({ everyBar, yScale, widthPerItem, spaceBetween, ldcOffset }) { const height = this.fnheight({ name: "highestLevel", yScale }); everyBar .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; const value = Number.parseFloat(hoursInDays(d.highestLevel)).toFixed( 2 ); d3.select("#tooltip") .text(Math.round(value)) .attr("y", y - 50) .attr("x", d3.event.pageX - dy); //d3.event.pageX gives coordinates relative to SVG //dy gives offset of svg on page }) .attr("y", yScale(0)) .attr("height", height) .attr("x", ldcOffset + spaceBetween / 2) .attr("width", widthPerItem - ldcOffset - spaceBetween) .attr( "transform", d => `translate(0 ${this.paddingTop + -1 * height(d)})` ) .attr("fill", this.$options.COLORS.HIGHEST); }, drawLabelPerBar({ everyBar, dimensions, widthPerItem }) { everyBar .append("text") .text(d => d.label) .attr("y", dimensions.mainHeight + this.paddingTop - 5) .attr("x", widthPerItem / 2) .attr("text-anchor", "middle") .attr("font-size", 10); }, drawScaleLabel({ diagram, dimensions }) { diagram .append("text") .text(this.$options.LEGEND) .attr("text-anchor", "middle") .attr("x", 0) .attr("y", 0) .attr("dy", "20") .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: { fwData() { this.drawDiagram(); } }, LEGEND: app.$gettext("Sum of days"), COLORS: { LDC: "#cdcdcd", HIGHEST: "#3675ff", REST: ["#782121", "#ff6c6c", "#ffaaaa"] } }; </script>