Mercurial > gemma
changeset 2761:71e7237110ba
client: spuc8: prepared diagram
author | Markus Kottlaender <markus@intevation.de> |
---|---|
date | Thu, 21 Mar 2019 17:31:03 +0100 |
parents | c6fba10926cc |
children | f95ec0bb565c |
files | client/src/components/gauge/Gauges.vue client/src/components/gauge/HydrologicalConditions.vue client/src/components/gauge/Waterlevel.vue client/src/components/splitscreen/Splitscreen.vue client/src/store/gauges.js |
diffstat | 5 files changed, 478 insertions(+), 3 deletions(-) [+] |
line wrap: on
line diff
--- a/client/src/components/gauge/Gauges.vue Thu Mar 21 17:04:37 2019 +0100 +++ b/client/src/components/gauge/Gauges.vue Thu Mar 21 17:31:03 2019 +0100 @@ -68,7 +68,11 @@ <translate>Show Waterlevels</translate> </button> <hr /> - <button class="btn btn-sm btn-info d-block w-100 mt-2" disabled> + <button + @click="showHydrologicalConditionsDiagram()" + class="btn btn-sm btn-info d-block w-100 mt-2" + disabled + > <translate>Show Hydrological Conditions</translate> </button> </div> @@ -149,6 +153,9 @@ if (this.activeSplitscreenId === "gauge-waterlevel") { this.showWaterlevelDiagram(); } + if (this.activeSplitscreenId === "gauge-hydrological-conditions") { + this.showHydrologicalConditionsDiagram(); + } } }, methods: { @@ -212,6 +219,53 @@ this.loading = false; }); }, + showHydrologicalConditionsDiagram() { + // for panning the map to the gauge on opening the diagram: needs to be + // set outside of the expandCallback to not always refer to the currently + // selectedGauge + let coordinates = this.selectedGauge.geometry.coordinates; + + // configure splitscreen + let splitscreenConf = { + id: "gauge-hydrological-conditions", + component: "hydrological-conditions", + title: + this.$gettext("Hydrological Conditions") + + ": " + + this.gaugeLabel(this.selectedGauge), + icon: "ruler-vertical", + closeCallback: () => { + this.$store.commit("gauges/selectedGaugeISRS", null); + this.$store.commit("gauges/meanWaterlevels", []); + }, + expandCallback: () => { + this.$store.commit("map/moveMap", { + coordinates, + zoom: 17, + preventZoomOut: true + }); + } + }; + this.$store.commit("application/addSplitscreen", splitscreenConf); + this.$store.commit("application/activeSplitscreenId", splitscreenConf.id); + this.$store.commit("application/splitscreenLoading", true); + this.loading = true; + this.$store.commit("application/showSplitscreen", true); + + this.$store + .dispatch("gauges/loadMeanWaterlevels") + .catch(error => { + const { status, data } = error.response; + displayError({ + title: "Backend Error", + message: `${status}: ${data.message || data}` + }); + }) + .finally(() => { + this.$store.commit("application/splitscreenLoading", false); + this.loading = false; + }); + }, gaugeLabel(gauge) { return `${gauge.properties.objname} (${this.isrsInfo(gauge).orc})`; },
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/gauge/HydrologicalConditions.vue Thu Mar 21 17:31:03 2019 +0100 @@ -0,0 +1,394 @@ +<template> + <div + class="d-flex flex-fill justify-content-center align-items-center diagram-container" + > + <div v-if="!meanWaterlevels.length"> + <translate>No data available.</translate> + </div> + </div> +</template> + +<style lang="sass" scoped> +.diagram-container + /deep/ + .line + clip-path: url(#clip) + + .hdc-line, + .ldc-line, + .mw-line + stroke-width: 1 + fill: none + clip-path: url(#clip) + .hdc-line + stroke: red + .ldc-line + stroke: green + .mw-line + stroke: grey + .ref-waterlevel-label + font-size: 11px + fill: #999 + + .tick + line + stroke-dasharray: 5 + stroke: #ccc + + .zoom + cursor: move + fill: none + pointer-events: all + .brush + .selection + stroke: none + fill-opacity: 0.2 + .handle + stroke: rgba($color-info, 0.5) + fill: rgba($color-info, 0.5) +</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 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ + +import { mapState, mapGetters } from "vuex"; +import * as d3Base from "d3"; +import debounce from "debounce"; +import { lineChunked } from "d3-line-chunked"; +import { startOfYear, endOfYear } from "date-fns"; + +// we should load only d3 modules we need but for now we'll go with the lazy way +// https://www.giacomodebidda.com/how-to-import-d3-plugins-with-webpack/ +const d3 = Object.assign(d3Base, { lineChunked }); + +export default { + computed: { + ...mapState("gauges", ["meanWaterlevels"]), + ...mapGetters("gauges", ["selectedGauge", "minMaxWaterlevelForDay"]) + }, + watch: { + meanWaterlevels() { + this.drawDiagram(); + } + }, + methods: { + drawDiagram() { + // TODO: Optimize code. I'm pretty sure all of this can be done in a much + // more elegant way and with less lines of code. + + // remove old diagram + d3.select(".diagram-container svg").remove(); + + if (!this.selectedGauge || !this.meanWaterlevels.length) return; + + // get HDC/LDC/MW of the gauge + let refWaterLevels = JSON.parse( + this.selectedGauge.properties.reference_water_levels + ); + + // CREATE SVG AND SET DIMENSIONS/MARGINS + + let svgWidth = document.querySelector(".diagram-container").clientWidth; + let svgHeight = document.querySelector(".diagram-container").clientHeight; + let svg = d3 + .select(".diagram-container") + .append("svg") + .attr("width", "100%") + .attr("height", "100%"); + let mainMargin = { top: 20, right: 20, bottom: 110, left: 80 }, + navMargin = { + top: svgHeight - mainMargin.top - 65, + right: 20, + bottom: 30, + left: 80 + }, + width = +svgWidth - mainMargin.left - mainMargin.right, + mainHeight = +svgHeight - mainMargin.top - mainMargin.bottom, + navHeight = +svgHeight - navMargin.top - navMargin.bottom; + + // PREPARING AXES/SCALING + + // scaling helpers to convert real values to pixels + // based on the diagrams dimensions + let x = d3.scaleTime().range([0, width]), + x2 = d3.scaleTime().range([0, width]), + y = d3.scaleLinear().range([mainHeight, 0]), + y2 = d3.scaleLinear().range([navHeight, 0]); + // find min/max values for the waterlevel axis + // including HDC/LDC (+/- 1/8 HDC-LDC) + let WaterlevelMinMax = d3.extent( + [ + ...this.meanWaterlevels, + { + waterlevel: + refWaterLevels.HDC + (refWaterLevels.HDC - refWaterLevels.LDC) / 8 + }, + { + waterlevel: Math.max( + refWaterLevels.LDC - + (refWaterLevels.HDC - refWaterLevels.LDC) / 8, + 0 + ) + } + ], + d => d.waterlevel + ); + // setting the min and max values for the diagram axes + let yearStart = startOfYear(new Date()); + let yearEnd = endOfYear(new Date()); + x.domain(d3.extent([yearStart, yearEnd])); + y.domain(WaterlevelMinMax); + x2.domain(x.domain()); + y2.domain(y.domain()); + // creating the axes based on these scales + let xAxis = d3 + .axisTop(x) + .tickSizeInner(mainHeight) + .tickSizeOuter(0) + .tickFormat(date => { + return (d3.timeSecond(date) < date + ? d3.timeFormat(".%L") + : d3.timeMinute(date) < date + ? d3.timeFormat(":%S") + : d3.timeHour(date) < date + ? d3.timeFormat("%I:%M") + : d3.timeDay(date) < date + ? d3.timeFormat("%I %p") + : d3.timeMonth(date) < date + ? d3.timeWeek(date) < date + ? d3.timeFormat("%a %d") + : d3.timeFormat("%b %d") + : d3.timeFormat("%B"))(date); + }); + let xAxis2 = d3.axisBottom(x2).tickFormat(d3.timeFormat("%B")); + let yAxis = d3 + .axisRight(y) + .tickSizeInner(width) + .tickSizeOuter(0); + + // PREPARING CHART FUNCTIONS + + // points are "next to each other" when they are exactly 1 day apart + const isNext = (prev, current) => + current.date - prev.date === 24 * 60 * 60 * 1000; + + // waterlevel line in big chart + // d3-line-chunked plugin: https://github.com/pbeshai/d3-line-chunked + var mainLineChart = d3 + .lineChunked() + .x(d => x(d.date)) + .y(d => y(d.waterlevel)) + .curve(d3.curveLinear) + .isNext(isNext) + .pointAttrs({ r: 2.2 }); + // waterlevel line in small chart + let navLineChart = d3 + .lineChunked() + .x(d => x2(d.date)) + .y(d => y2(d.waterlevel)) + .curve(d3.curveMonotoneX) + .isNext(isNext) + .pointAttrs({ r: 1.7 }); + // hdc/ldc/mw + let refWaterlevelLine = d3 + .line() + .x(d => x(d.x)) + .y(d => y(d.y)); + + // DRAWING MAINCHART + + // define visible chart area + // everything outside this area will be hidden (clipped) + svg + .append("defs") + .append("clipPath") + .attr("id", "clip") + .append("rect") + .attr("width", width) + .attr("height", mainHeight); + + let mainChart = svg + .append("g") + .attr("class", "main") + .attr("transform", `translate(${mainMargin.left}, ${mainMargin.top})`); + + // axes + mainChart + .append("g") + .attr("class", "axis--x") + .attr("transform", `translate(0, ${mainHeight})`) + .call(xAxis) + .selectAll(".tick text") + .attr("y", 15); + mainChart // label + .append("text") + .text(this.$gettext("Waterlevel [cm]")) + .attr("text-anchor", "middle") + .attr("transform", `translate(-45, ${mainHeight / 2}) rotate(-90)`); + mainChart + .append("g") + .call(yAxis) + .selectAll(".tick text") + .attr("x", -25); + + // reference waterlevels + // HDC + mainChart + .append("path") + .datum([ + { x: 0, y: refWaterLevels.HDC }, + { x: yearEnd, y: refWaterLevels.HDC } + ]) + .attr("class", "hdc-line") + .attr("d", refWaterlevelLine); + mainChart // label + .append("text") + .text("HDC") + .attr("class", "ref-waterlevel-label") + .attr("x", x(yearEnd) - 20) + .attr("y", y(refWaterLevels.HDC) - 3); + // LDC + mainChart + .append("path") + .datum([ + { x: 0, y: refWaterLevels.LDC }, + { x: yearEnd, y: refWaterLevels.LDC } + ]) + .attr("class", "ldc-line") + .attr("d", refWaterlevelLine); + mainChart // label + .append("text") + .text("LDC") + .attr("class", "ref-waterlevel-label") + .attr("x", x(yearEnd) - 20) + .attr("y", y(refWaterLevels.LDC) - 3); + // MW + mainChart + .append("path") + .datum([ + { x: 0, y: refWaterLevels.MW }, + { x: yearEnd, y: refWaterLevels.MW } + ]) + .attr("class", "mw-line") + .attr("d", refWaterlevelLine); + mainChart // label + .append("text") + .text("MW") + .attr("class", "ref-waterlevel-label") + .attr("x", x(yearEnd) - 20) + .attr("y", y(refWaterLevels.MW) - 3); + + // waterlevel chart + mainChart + .append("g") + .attr("class", "line") + .datum([]) + .call(mainLineChart); + + // DRAWING NAVCHART + + let navChart = svg + .append("g") + .attr("class", "nav") + .attr("transform", `translate(${navMargin.left}, ${navMargin.top})`); + + // axis (nav chart only has x-axis) + navChart + .append("g") + .attr("class", "axis axis--x") + .attr("transform", `translate(0, ${navHeight})`) + .call(xAxis2); + + // waterlevel chart + navChart + .append("g") + .attr("class", "line") + .datum([]) + .call(navLineChart); + + // INTERACTIVITY + + // selecting time period in nav chart + let brush = d3 + .brushX() + .handleSize(4) + .extent([[0, 0], [width, navHeight]]) + .on("brush end", () => { + if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") + return; // ignore brush-by-zoom + let s = d3.event.selection || x2.range(); + x.domain(s.map(x2.invert, x2)); + mainChart.select(".line").call(mainLineChart); + mainChart + .select(".axis--x") + .call(xAxis) + .selectAll(".tick text") + .attr("y", 15); + svg + .select(".zoom") + .call( + zoom.transform, + d3.zoomIdentity.scale(width / (s[1] - s[0])).translate(-s[0], 0) + ); + }); + + // zooming with mousewheel in main chart + let zoom = d3 + .zoom() + .scaleExtent([1, Infinity]) + .translateExtent([[0, 0], [width, mainHeight]]) + .extent([[0, 0], [width, mainHeight]]) + .on("zoom", () => { + if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") + return; // ignore zoom-by-brush + let t = d3.event.transform; + x.domain(t.rescaleX(x2).domain()); + mainChart.select(".line").call(mainLineChart); + mainChart + .select(".axis--x") + .call(xAxis) + .selectAll(".tick text") + .attr("y", 15); + navChart + .select(".brush") + .call(brush.move, x.range().map(t.invertX, t)); + }); + + navChart + .append("g") + .attr("class", "brush") + .call(brush) + .call(brush.move, x.range()); + + svg + .append("rect") + .attr("class", "zoom") + .attr("width", width) + .attr("height", mainHeight) + .attr("transform", `translate(${mainMargin.left}, ${mainMargin.top})`) + .call(zoom); + } + }, + created() { + window.addEventListener("resize", debounce(this.drawDiagram), 100); + }, + mounted() { + this.drawDiagram(); + }, + updated() { + this.drawDiagram(); + } +}; +</script>
--- a/client/src/components/gauge/Waterlevel.vue Thu Mar 21 17:04:37 2019 +0100 +++ b/client/src/components/gauge/Waterlevel.vue Thu Mar 21 17:31:03 2019 +0100 @@ -178,7 +178,7 @@ y = d3.scaleLinear().range([mainHeight, 0]), y2 = d3.scaleLinear().range([navHeight, 0]); // find min/max values for the waterlevel axis - // including hdc/ldc (+/- 100 cm) + // including HDC (+ 1/8 HDC-LDC) and LDC (- 1/4 HDC-LDC) let WaterlevelMinMax = d3.extent( [ ...this.waterlevels,
--- a/client/src/components/splitscreen/Splitscreen.vue Thu Mar 21 17:04:37 2019 +0100 +++ b/client/src/components/splitscreen/Splitscreen.vue Thu Mar 21 17:31:03 2019 +0100 @@ -63,7 +63,9 @@ export default { components: { Fairwayprofile: () => import("@/components/fairway/Fairwayprofile"), - Waterlevel: () => import("@/components/gauge/Waterlevel") + Waterlevel: () => import("@/components/gauge/Waterlevel"), + HydrologicalConditions: () => + import("@/components/gauge/HydrologicalConditions") }, computed: { ...mapState("application", ["showSplitscreen", "splitscreenLoading"]),
--- a/client/src/store/gauges.js Thu Mar 21 17:04:37 2019 +0100 +++ b/client/src/store/gauges.js Thu Mar 21 17:31:03 2019 +0100 @@ -22,6 +22,7 @@ gauges: [], selectedGaugeISRS: null, waterlevels: [], + meanWaterlevels: [], nashSutcliffe: null, dateFrom: dateFrom, dateTo: new Date() @@ -49,6 +50,9 @@ waterlevels: (state, data) => { state.waterlevels = data; }, + meanWaterlevels: (state, data) => { + state.meanWaterlevels = data; + }, nashSutcliffe: (state, data) => { state.nashSutcliffe = data; }, @@ -129,6 +133,27 @@ }); }); }, + loadMeanWaterlevels({ /*state,*/ commit }) { + return new Promise(resolve => { + setTimeout(() => { + commit("meanWaterlevels", [1]); + resolve(); + }, 2000); + }); + // return new Promise((resolve, reject) => { + // HTTP.get(`/data/mean-waterlevels/${state.selectedGaugeISRS}`, { + // headers: { "X-Gemma-Auth": localStorage.getItem("token") } + // }) + // .then(response => { + // commit("meanWaterlevels", response.data); + // resolve(response.data); + // }) + // .catch(error => { + // commit("meanWaterlevels", []); + // reject(error); + // }) + // }); + }, loadNashSutcliffe({ state, commit }) { return new Promise((resolve, reject) => { HTTP.get(`/data/nash-sutcliffe/${state.selectedGaugeISRS}`, {