Mercurial > gemma
view client/src/components/fairway/AvailableFairwayDepth.vue @ 5338:f8e7f043d968 extented-report
Fixed example_conf.toml for new report-path.
author | Sascha L. Teichmann <sascha.teichmann@intevation.de> |
---|---|
date | Tue, 15 Jun 2021 22:00:20 +0200 |
parents | 6b054b91d9b2 |
children | 791a372553a0 |
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="`${fileName}.csv`" class="mt-2 btn btn-sm btn-info w-100" ><translate>Download CSV</translate></a > <a @click="downloadImage('AFDpng', title)" id="AFDpng" class="btn btn-sm btn-info text-white d-block w-100 mt-2" :download="`${fileName}.png`" > <translate>Download Image</translate> </a> </div> <div class="btn-group-toggle w-100 mt-2"> <label class="btn btn-outline-secondary btn-sm" :class="{ active: showNumbers }" ><input type="checkbox" v-model="showNumbers" autocomplete="off" /><translate>Numbers</translate> </label> </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> * * Bernhard Reiter <bernhard.reiter@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"; import { localeDateString } from "@/lib/datelocalization"; export default { mixins: [diagram, pdfgen, templateLoader], components: { DiagramLegend: () => import("@/components/DiagramLegend") }, data() { return { frequencyD: null, selectedFairwayAvailabilityFeatureD: null, fromDate: null, toDate: null, depthlimit1D: null, depthlimit2D: null, widthlimit1D: null, widthlimit2D: null, 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, showNumbers: false }; }, 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.initDiagramValues(); 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(error => { let message = "Backend not reachable"; if (error.response) { const { status, data } = error.response; message = `${status}: ${data.message || data}`; } displayError({ title: this.$gettext("Backend Error"), message: message }); }); }, computed: { ...mapState("fairwayavailability", [ "selectedFairwayAvailabilityFeature", "fwData", "from", "to", "frequency", "csv", "depthlimit1", "depthlimit2", "widthlimit1", "widthlimit2" ]), legend() { const d = [this.depthlimit1D, this.depthlimit2D].sort(); const w = [this.widthlimit1D, this.widthlimit2D].sort(); const lowerBound = [d[0] / 100, w[0]].filter(x => x).join(", "); const upperBound = [d[1] / 100, w[1]].filter(x => x).join(", "); let result; if (this.depthlimit1D !== this.depthlimit2D) { result = [ `> LDC`, `>= ${upperBound} [m]`, `< ${upperBound} [m]`, `< ${lowerBound} [m]` ]; } else { result = [`> LDC`, `>= ${upperBound} [m]`, `< ${upperBound} [m]`]; } return result; }, dataLink() { return `data:text/csv;charset=utf-8, ${encodeURIComponent(this.csv)}`; }, fileName() { if (!this.frequencyD) return; return this.downloadFilename( this.$gettext("FairwayAvailability"), this.featureName ); }, csvFileName() { if (!this.frequencyD) return; return ( this.downloadFilename( this.$gettext("FairwayAvailability"), this.featureName ) + ".csv" ); }, frequencyToRange() { if (!this.frequencyD) return; const frequencies = { [FREQUENCIES.MONTHLY]: [-33, 33], [FREQUENCIES.QUARTERLY]: [-93, 93], [FREQUENCIES.YEARLY]: [-370, 370] }; return frequencies[this.frequencyD]; }, availability() { return this.plainAvailability; }, title() { if (!this.frequencyD) return; return `${this.$gettext("Available Fairway Depth:")} ${ this.featureName } (${filters.surveyDate(this.fromDate)} - ${filters.surveyDate( this.toDate )}) ${this.$gettext(this.frequencyD)}`; }, featureName() { if (this.selectedFairwayAvailabilityFeatureD == null) return ""; return this.selectedFairwayAvailabilityFeatureD.properties.name; } }, methods: { addLegendToCanvas(ctx, { width, height }) { let x = width / 20, y = height - 35; ctx.font = "14px sans-serif"; ctx.textAlign = "start"; if (this.legend[3]) { ctx.beginPath(); ctx.fillStyle = this.$options.COLORS.LDC; ctx.strokeStyle = this.$options.COLORS.LDC; ctx.rect(x, y, 35, 20); ctx.fill(); ctx.stroke(); ctx.fillStyle = "black"; ctx.fillText(this.legend[0], x + 40, y + 13); ctx.closePath(); ctx.beginPath(); ctx.fillStyle = this.$options.COLORS.HIGHEST; ctx.strokeStyle = this.$options.COLORS.HIGHEST; ctx.rect(x, (y += 25), 35, 20); ctx.fill(); ctx.stroke(); ctx.fillStyle = "black"; ctx.fillText(this.legend[1], x + 40, y + 13); ctx.closePath(); ctx.beginPath(); ctx.fillStyle = this.$options.COLORS.REST[1]; ctx.strokeStyle = this.$options.COLORS.REST[1]; ctx.rect(x, (y += 25), 35, 20); ctx.fill(); ctx.stroke(); ctx.fillStyle = "black"; ctx.fillText(this.legend[2], x + 40, y + 13); ctx.closePath(); ctx.beginPath(); ctx.fillStyle = this.$options.COLORS.REST[0]; ctx.strokeStyle = this.$options.COLORS.REST[0]; ctx.rect(x, (y += 25), 35, 20); ctx.fill(); ctx.stroke(); ctx.fillStyle = "black"; ctx.fillText(this.legend[3], x + 40, y + 13); ctx.closePath(); } else { ctx.beginPath(); ctx.fillStyle = this.$options.COLORS.LDC; ctx.strokeStyle = this.$options.COLORS.LDC; ctx.rect(x, y, 35, 20); ctx.fill(); ctx.stroke(); ctx.fillStyle = "black"; ctx.fillText(this.legend[0], x + 40, y + 13); ctx.closePath(); ctx.beginPath(); ctx.fillStyle = this.$options.COLORS.HIGHEST; ctx.strokeStyle = this.$options.COLORS.HIGHEST; ctx.rect(x, (y += 25), 35, 20); ctx.fill(); ctx.stroke(); ctx.fillStyle = "black"; ctx.fillText(this.legend[1], x + 40, y + 13); ctx.closePath(); ctx.beginPath(); ctx.fillStyle = this.$options.COLORS.REST[0]; ctx.strokeStyle = this.$options.COLORS.REST[0]; ctx.rect(x, (y += 25), 35, 20); ctx.fill(); ctx.stroke(); ctx.fillStyle = "black"; ctx.fillText(this.legend[2], x + 40, y + 13); ctx.closePath(); } }, initDiagramValues() { this.selectedFairwayAvailabilityFeatureD = this.selectedFairwayAvailabilityFeature; this.fromDate = this.from; this.toDate = this.to; this.depthlimit1D = this.depthlimit1; this.depthlimit2D = this.depthlimit2; this.widthlimit1D = this.widthlimit1; this.widthlimit2D = this.widthlimit2; this.frequencyD = this.frequency; }, 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(error => { let message = "Backend not reachable"; if (error.response) { const { status, data } = error.response; message = `${status}: ${data.message || data}`; } displayError({ title: this.$gettext("Backend Error"), message: message }); }); } }, downloadPDF() { let title = `${this.$gettext("Available Fairway Depth:")} ${ this.featureName }`; this.generatePDF({ templateData: this.templateData, diagramTitle: title }); this.pdf.doc.save(this.fileName + ".pdf"); }, 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, svgWidth) { return { main: { top: 0, right: Math.floor(svgWidth * 0.025), bottom: Math.floor(svgHeight * 0.17), left: Math.floor(svgWidth * 0.025) } }; }, drawDiagram() { d3.timeFormatDefaultLocale(localeDateString); 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, svgWidth); 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"); diagram .append("g") .append("rect") .attr("width", "100%") .attr("height", "100%") .attr("fill", "#ffffff"); 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; d3.select("#tooltip") .text(d.height) .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 => 2 * yScale(0) - yScale(d.translateY) + this.paddingTop) .attr("height", d => { return yScale(0) - yScale(d.height); }) .attr("x", ldcOffset + spaceBetween / 2) .attr("width", widthPerItem - ldcOffset - spaceBetween) .attr("id", "lower") .attr("fill", (d, i) => { return this.$options.COLORS.REST[i]; }); if (this.showNumbers) { everyBar .selectAll("g.bars") .data(d => d.lowerLevels) .enter() .filter(d => d.height > 0) .insert("text") .attr("y", d => { return ( 2 * yScale(0) - yScale(d.translateY) + this.paddingTop + (yScale(0) - yScale(d.height)) + (yScale(0) - yScale(1.9)) //instead o alignment-baseline hanging ); }) .attr("x", widthPerItem / 2) .text(d => d.height) // does not work with svg2pdf .attr("alignment-baseline", "hanging") .attr("text-anchor", "middle") .attr("font-size", "8") .attr("fill", "black"); } }, fnheight({ name, yScale }) { return d => yScale(0) - yScale(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.7"); 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(d.ldc) .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"); if (this.showNumbers) { everyBar .filter(d => d.ldc > 0) .append("text") .attr("y", yScale(0.5)) // some distance from the bar .attr("x", spaceBetween / 2) .text(d => d.ldc) .attr("text-anchor", "left") .attr("font-size", "8") .attr( "transform", d => `translate(0 ${this.paddingTop + -1 * height(d)})` ) .attr("fill", "black"); } }, 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; d3.select("#tooltip") .text(d.highestLevel) .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); if (this.showNumbers) { everyBar .filter(d => d.highestLevel > 0) .append("text") .attr("y", yScale(0.5)) // some distance from the bar .attr("x", widthPerItem / 2) .text(d => d.highestLevel) .attr("text-anchor", "middle") .attr("font-size", "8") .attr( "transform", d => `translate(0 ${this.paddingTop + -1 * height(d)})` ) .attr("fill", "black"); } }, 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.initDiagramValues(); this.drawDiagram(); }, showNumbers() { this.drawDiagram(); } }, LEGEND: app.$gettext("Sum of days"), COLORS: { LDC: "aqua", HIGHEST: "blue", REST: ["hotpink", "darksalmon", "#ffaaaa"] } }; </script>