Mercurial > gemma
changeset 3885:d42158f77841
waterlevel: factor out side-effects from diagram rendering
author | Thomas Junk <thomas.junk@intevation.de> |
---|---|
date | Wed, 10 Jul 2019 12:08:12 +0200 |
parents | db24b4347604 |
children | 9fa9a485c182 |
files | client/src/components/gauge/Waterlevel.vue |
diffstat | 1 files changed, 229 insertions(+), 225 deletions(-) [+] |
line wrap: on
line diff
--- a/client/src/components/gauge/Waterlevel.vue Wed Jul 10 12:00:58 2019 +0200 +++ b/client/src/components/gauge/Waterlevel.vue Wed Jul 10 12:08:12 2019 +0200 @@ -127,13 +127,6 @@ data() { return { containerId: "waterlevel-diagram-container", - svg: null, - diagram: null, - navigation: null, - dimensions: null, - extent: null, - scale: null, - axes: null, form: { template: null }, @@ -291,34 +284,44 @@ }); } }, - addDiagram(position, offset) { + addDiagram(position, offset, width, height) { let x = offset.x, y = offset.y; - + const svgWidth = 1550; + const svgHeight = 400; + // check if there are tow diagrams on the screen + // draw the diagram in a separated html element to get the full size + const offScreen = document.querySelector("#offScreen"); + offScreen.style.width = `${svgWidth}px`; + offScreen.style.height = `${svgHeight}px`; + this.renderTo({ + element: offScreen, + dimensions: this.getDimensions({ + svgWidth: svgWidth, + svgHeight: svgHeight + }) + }); + var svg = offScreen.querySelector("svg"); // use default width,height if they are missing in the template definition - + if (!width) { + width = this.templateData.properties.paperSize === "a3" ? 380 : 290; + } + if (!height) { + height = this.templateData.properties.paperSize === "a3" ? 130 : 100; + } if (["topright", "bottomright"].indexOf(position) !== -1) { - x = - this.pdf.width - - offset.x - - document.getElementById(this.containerId).clientWidth; + x = this.pdf.width - offset.x - width; } if (["bottomright", "bottomleft"].indexOf(position) !== -1) { - y = - this.pdf.height - - offset.y - - document.getElementById(this.containerId).clientHeight; + y = this.pdf.height - offset.y - height; } - - let svg = document.getElementById(this.containerId).firstElementChild; svg2pdf(svg, this.pdf.doc, { xOffset: x, yOffset: y, // TODO depend on the size and aspect ration on paper scale: this.templateData.properties.paperSize === "a3" ? 0.45 : 0.18 }); - - this.containerId = "waterlevel-diagram-container"; + offScreen.removeChild(svg); }, // Diagram legend addDiagramLegend(position, offset, color) { @@ -354,7 +357,15 @@ // remove old diagram and exit if necessary data is missing d3.select("#" + this.containerId + " svg").remove(); if (!this.selectedGauge || !this.waterlevels.length) return; - + this.renderTo({ + element: `#${this.containerId}`, + dimensions: this.getDimensions({ + svgWidth: document.querySelector("#" + this.containerId).clientWidth, + svgHeight: document.querySelector("#" + this.containerId).clientHeight + }) + }); + }, + renderTo({ element, dimensions }) { // PREPARE HELPERS // HDC/LDC/MW for the selected gauge @@ -362,68 +373,63 @@ this.selectedGauge.properties.reference_water_levels ); - // dimensions (widths, heights, margins) - this.dimensions = this.getDimensions(); - // get min/max values for date and waterlevel axis - this.extent = this.getExtent(refWaterLevels); + const extent = this.getExtent(refWaterLevels); // scaling helpers - this.scale = this.getScale(); + const scale = this.getScale({ dimensions, extent }); // creating the axes based on the scales - this.axes = { + const axes = { x: d3 - .axisTop(this.scale.x) - .tickSizeInner(this.dimensions.mainHeight) + .axisTop(scale.x) + .tickSizeInner(dimensions.mainHeight) .tickSizeOuter(0), y: d3 - .axisRight(this.scale.y) - .tickSizeInner(this.dimensions.width) + .axisRight(scale.y) + .tickSizeInner(dimensions.width) .tickSizeOuter(0) .tickFormat(d => this.$options.filters.waterlevel(d)), - x2: d3.axisBottom(this.scale.x2) + x2: d3.axisBottom(scale.x2) }; // DRAW DIAGRAM/NAVIGATION AREAS // create svg - this.svg = d3 - .select("#" + this.containerId) + const svg = d3 + .select(element) .append("svg") .attr("width", "100%") .attr("height", "100%"); // create container for main diagram - this.diagram = this.svg + const diagram = svg .append("g") .attr("class", "main") .attr( "transform", - `translate(${this.dimensions.mainMargin.left}, ${ - this.dimensions.mainMargin.top + `translate(${dimensions.mainMargin.left}, ${ + dimensions.mainMargin.top })` ); // create container for navigation diagram - this.navigation = this.svg + const navigation = svg .append("g") .attr("class", "nav") .attr( "transform", - `translate(${this.dimensions.navMargin.left}, ${ - this.dimensions.navMargin.top - })` + `translate(${dimensions.navMargin.left}, ${dimensions.navMargin.top})` ); // define visible area, everything outside this area will be hidden - this.svg + svg .append("defs") .append("clipPath") .attr("id", "waterlevel-clip") .append("rect") - .attr("width", this.dimensions.width) - .attr("height", this.dimensions.mainHeight); + .attr("width", dimensions.width) + .attr("height", dimensions.mainHeight); // DRAW DIAGRAM PARTS @@ -433,144 +439,155 @@ const updaters = []; // draw (order matters) - updaters.push(this.drawAxes()); - updaters.push(this.drawWaterlevelChart()); + updaters.push(this.drawAxes({ diagram, dimensions, axes, navigation })); + updaters.push(this.drawWaterlevelChart({ scale, diagram })); if (this.hasPredictions) { - updaters.push(this.drawPredictionAreas()); + updaters.push(this.drawPredictionAreas({ scale, diagram, navigation })); } - updaters.push(this.drawNowLines()); + updaters.push(this.drawNowLines({ scale, extent, diagram, navigation })); // static, don't need updater - this.drawNavigationChart(); - this.drawRefLines(refWaterLevels); + this.drawNavigationChart({ scale, navigation }); + this.drawRefLines({ refWaterLevels, diagram, scale, dimensions, extent }); - updaters.push(this.drawNashSutcliffe(72)); - updaters.push(this.drawNashSutcliffe(48)); - updaters.push(this.drawNashSutcliffe(24)); + updaters.push( + this.drawNashSutcliffe({ hours: 72, diagram, scale, dimensions }) + ); + updaters.push( + this.drawNashSutcliffe({ hours: 48, diagram, scale, dimensions }) + ); + updaters.push( + this.drawNashSutcliffe({ hours: 24, diagram, scale, dimensions }) + ); // INTERACTIONS // create rectanlge on the main chart area to capture mouse events - const eventRect = this.svg + const eventRect = svg .append("rect") .attr("id", "zoom-waterlevels") .attr("class", "zoom") - .attr("width", this.dimensions.width) - .attr("height", this.dimensions.mainHeight) + .attr("width", dimensions.width) + .attr("height", dimensions.mainHeight) .attr( "transform", - `translate(${this.dimensions.mainMargin.left}, ${ - this.dimensions.mainMargin.top + `translate(${dimensions.mainMargin.left}, ${ + dimensions.mainMargin.top })` ); - this.createZoom(updaters, eventRect); - this.createTooltips(eventRect); - this.setInlineStyles(); + this.createZoom({ + updaters, + eventRect, + dimensions, + scale, + navigation, + svg + }); + this.createTooltips({ eventRect, diagram, scale, dimensions }); + this.setInlineStyles(svg); }, //set the styles of the diagrams to include them in the pdf - setInlineStyles() { - this.svg + setInlineStyles(svg) { + svg .selectAll(".line") .attr("clip-path", "url(#waterlevel-clip)") .selectAll("path") .attr("stroke", "steelblue") .attr("stroke-width", 2) .attr("fill", "none"); - this.svg + svg .selectAll(".line") .selectAll("path.d3-line-chunked-chunk-gap") .attr("stroke-opacity", 0); - this.svg + svg .selectAll(".line") .selectAll("circle") .attr("fill", "steelblue") .attr("stroke-width", 0); - this.svg + svg .selectAll(".line") .selectAll("circle.d3-line-chunked-chunk-predicted-point") .attr("fill-opacity", 0.6); - this.svg + svg .selectAll(".hdc-line, .mw-line, .ldc-line, .rn-line") .attr("stroke-width", 1) .attr("fill", "none") .attr("clip-path", "url(#waterlevel-clip)"); - this.svg.selectAll(".hdc-line").attr("stroke", "red"); - this.svg.selectAll(".ldc-line").attr("stroke", "green"); - this.svg.selectAll(".mw-line").attr("stroke", "grey"); - this.svg.selectAll(".rn-line").attr("stroke", "grey"); - this.svg + svg.selectAll(".hdc-line").attr("stroke", "red"); + svg.selectAll(".ldc-line").attr("stroke", "green"); + svg.selectAll(".mw-line").attr("stroke", "grey"); + svg.selectAll(".rn-line").attr("stroke", "grey"); + svg .selectAll(".ref-waterlevel-label") .style("font-size", "10px") .attr("fill", "black"); - this.svg + svg .selectAll(".ref-waterlevel-label-background") .attr("fill", "rgb(255, 255, 255)") .attr("fill-opacity", 0.6); - this.svg + svg .selectAll(".hdc-ldc-area") .attr("fill", "rgb(0, 255, 0)") .attr("fill-opacity", 0.1); - this.svg + svg .selectAll(".now-line") .attr("stroke", "#999") .attr("stroke-width", 1) .attr("stroke-dasharray", "5, 5") .attr("clip-path", "url(#waterlevel-clip)"); - this.svg + svg .selectAll(".now-line-label") .attr("font-size", "11px") .attr("fill", "#999"); - this.svg + svg .selectAll(".prediction-area") .attr("fill", "steelblue") .attr("fill-opacity", 0.2) .attr("clip-path", "url(#waterlevel-clip)"); - this.svg + svg .selectAll("path.nash-sutcliffe") .attr("fill", "none") .attr("stroke", "darkgrey") .attr("stroke-width", 1) .attr("clip-path", "url(#waterlevel-clip)"); - this.svg + svg .selectAll("path.nash-sutcliffe.ns72") .attr("fill", "rgb(255, 255, 255)") .attr("fill-opacity", 0.5); - this.svg + svg .selectAll("text.nash-sutcliffe") .style("font-size", "10px") .attr("clip-path", "url(#waterlevel-clip)") .selectAll("tspan:last-child, tspan:first-child") .attr("fill", "#555"); - this.svg + svg .selectAll(".tick line") .attr("stroke-dasharray", 5) .attr("stroke", "#ccc"); - this.svg.selectAll(".tick text").attr("fill", "black"); - this.svg.selectAll(".domain").attr("stroke", "black"); - this.svg + svg.selectAll(".tick text").attr("fill", "black"); + svg.selectAll(".domain").attr("stroke", "black"); + svg .selectAll(".domain") .attr("stroke", "black") .attr("fill", "none"); - this.svg + svg .selectAll(".zoom") .attr("cursor", "move") .attr("fill", "none") .attr("pointer-events", "all"); - this.svg + svg .selectAll(".brush .selection") .attr("stroke", "none") .attr("fill-opacity", 0.2); - this.svg + svg .selectAll(".brush .handle") .attr("stroke", "rgba(23, 162, 184, 0.5)") .attr("fill", "rgba(23, 162, 184, 0.5)"); - this.svg.selectAll(".brush .overlay").attr("fill", "none"); - this.svg - .selectAll(".chart-dots") - .attr("clip-path", "url(#waterlevel-clip)"); - this.svg + svg.selectAll(".brush .overlay").attr("fill", "none"); + svg.selectAll(".chart-dots").attr("clip-path", "url(#waterlevel-clip)"); + svg .selectAll(".chart-dots .chart-dot") .attr("fill", "steelblue") .attr("stroke", "steelblue") @@ -578,26 +595,21 @@ .style("pointer-events", "none") .transition() .attr("fill-opacity", "0.1s"); - this.svg + svg .selectAll(".chart-tooltip") .attr("fill-opacity", 0) .transition() .attr("fill-opacity", "0.3s"); - this.svg + svg .selectAll(".chart-tooltip rect") .attr("fill", "#fff") .attr("stroke", "#ccc"); - this.svg + svg .selectAll(".chart-tooltip text") .attr("fill", "666") .style("font-size", "0.8em"); }, - getDimensions() { - // dimensions and margins - const svgWidth = document.querySelector("#" + this.containerId) - .clientWidth; - const svgHeight = document.querySelector("#" + this.containerId) - .clientHeight; + getDimensions({ svgWidth, svgHeight }) { const mainMargin = { top: 20, right: 20, bottom: 110, left: 80 }; const navMargin = { top: svgHeight - mainMargin.top - 65, @@ -640,61 +652,61 @@ ) }; }, - getScale() { + getScale({ dimensions, extent }) { // scaling helpers to convert real world values into pixels - const x = d3.scaleTime().range([0, this.dimensions.width]); - const y = d3.scaleLinear().range([this.dimensions.mainHeight, 0]); - const x2 = d3.scaleTime().range([0, this.dimensions.width]); - const y2 = d3.scaleLinear().range([this.dimensions.navHeight, 0]); + const x = d3.scaleTime().range([0, dimensions.width]); + const y = d3.scaleLinear().range([dimensions.mainHeight, 0]); + const x2 = d3.scaleTime().range([0, dimensions.width]); + const y2 = d3.scaleLinear().range([dimensions.navHeight, 0]); // setting the min and max values for the diagram axes - x.domain(d3.extent(this.extent.date)); - y.domain(this.extent.waterlevel); + x.domain(d3.extent(extent.date)); + y.domain(extent.waterlevel); x2.domain(x.domain()); y2.domain(y.domain()); return { x, y, x2, y2 }; }, - drawAxes() { - this.diagram + drawAxes({ diagram, dimensions, axes, navigation }) { + diagram .append("g") .attr("class", "axis--x") - .attr("transform", `translate(0, ${this.dimensions.mainHeight})`) - .call(this.axes.x) + .attr("transform", `translate(0, ${dimensions.mainHeight})`) + .call(axes.x) .selectAll(".tick text") .attr("y", 15); - this.diagram // label + diagram // label .append("text") .text(this.$gettext("Waterlevel [m]")) .attr("text-anchor", "middle") .attr( "transform", - `translate(-45, ${this.dimensions.mainHeight / 2}) rotate(-90)` + `translate(-45, ${dimensions.mainHeight / 2}) rotate(-90)` ); - this.diagram + diagram .append("g") - .call(this.axes.y) + .call(axes.y) .selectAll(".tick text") .attr("x", -25); - this.navigation + navigation .append("g") .attr("class", "axis axis--x") - .attr("transform", `translate(0, ${this.dimensions.navHeight})`) - .call(this.axes.x2); + .attr("transform", `translate(0, ${dimensions.navHeight})`) + .call(axes.x2); return () => { - this.diagram + diagram .select(".axis--x") - .call(this.axes.x) + .call(axes.x) .selectAll(".tick text") .attr("y", 15); }; }, - drawWaterlevelChart() { + drawWaterlevelChart({ scale, diagram }) { const waterlevelChartDrawer = () => { - let domainLeft = new Date(this.scale.x.domain()[0].getTime()); - let domainRight = new Date(this.scale.x.domain()[1].getTime()); + let domainLeft = new Date(scale.x.domain()[0].getTime()); + let domainRight = new Date(scale.x.domain()[1].getTime()); domainLeft.setDate(domainLeft.getDate() - 1); domainRight.setDate(domainRight.getDate() + 1); @@ -702,10 +714,10 @@ .lineChunked() // render only data points that are visible in the current scale .defined(d => d.date > domainLeft && d.date < domainRight) - .x(d => this.scale.x(d.date)) - .y(d => this.scale.y(d.waterlevel)) + .x(d => scale.x(d.date)) + .y(d => scale.y(d.waterlevel)) .curve(d3.curveLinear) - .isNext(this.isNext()) + .isNext(this.isNext(scale)) .pointAttrs({ r: 1.7 }); // to avoid creating empty clip-path element if (this.hasPredictions) { @@ -716,23 +728,23 @@ return lineChunked; }; - this.diagram + diagram .append("g") .attr("class", "line") .datum(this.waterlevels) .call(waterlevelChartDrawer()); return () => { - this.diagram.select(".line").call(waterlevelChartDrawer()); + diagram.select(".line").call(waterlevelChartDrawer()); }; }, - drawNavigationChart() { + drawNavigationChart({ scale, navigation }) { let lineChunked = d3 .lineChunked() - .x(d => this.scale.x2(d.date)) - .y(d => this.scale.y2(d.waterlevel)) + .x(d => scale.x2(d.date)) + .y(d => scale.y2(d.waterlevel)) .curve(d3.curveLinear) - .isNext(this.isNext()) + .isNext(this.isNext(scale)) .pointAttrs({ r: 1.7 }); // to avoid creating empty clip-path element if (this.hasPredictions) { @@ -740,37 +752,37 @@ .chunk(d => (d.predicted ? "predicted" : "line")) .chunkDefinitions({ predicted: {} }); } - this.navigation + navigation .append("g") .attr("class", "line") .datum(this.waterlevels) .call(lineChunked); }, - drawNowLines() { + drawNowLines({ scale, extent, diagram, navigation }) { const nowLine = d3 .line() - .x(d => this.scale.x(d.x)) - .y(d => this.scale.y(d.y)); + .x(d => scale.x(d.x)) + .y(d => scale.y(d.y)); const nowLabel = selection => { selection.attr( "transform", - `translate(${this.scale.x(new Date())}, ${this.scale.y( - this.extent.waterlevel[1] - 16 + `translate(${scale.x(new Date())}, ${scale.y( + extent.waterlevel[1] - 16 )})` ); }; // draw in main - this.diagram + diagram .append("path") .datum([ - { x: new Date(), y: this.extent.waterlevel[0] }, - { x: new Date(), y: this.extent.waterlevel[1] - 20 } + { x: new Date(), y: extent.waterlevel[0] }, + { x: new Date(), y: extent.waterlevel[1] - 20 } ]) .attr("class", "now-line") .attr("d", nowLine); - this.diagram // label + diagram // label .append("text") .text(this.$gettext("Now")) .attr("class", "now-line-label") @@ -778,87 +790,87 @@ .call(nowLabel); // draw in nav - this.navigation + navigation .append("path") .datum([ - { x: new Date(), y: this.extent.waterlevel[0] }, - { x: new Date(), y: this.extent.waterlevel[1] - 20 } + { x: new Date(), y: extent.waterlevel[0] }, + { x: new Date(), y: extent.waterlevel[1] - 20 } ]) .attr("class", "now-line") .attr( "d", d3 .line() - .x(d => this.scale.x2(d.x)) - .y(d => this.scale.y2(d.y)) + .x(d => scale.x2(d.x)) + .y(d => scale.y2(d.y)) ); return () => { - this.diagram.select(".now-line").attr("d", nowLine); - this.diagram.select(".now-line-label").call(nowLabel); + diagram.select(".now-line").attr("d", nowLine); + diagram.select(".now-line-label").call(nowLabel); }; }, - drawPredictionAreas() { + drawPredictionAreas({ scale, diagram, navigation }) { const predictionArea = isNav => d3 .area() .defined(d => d.predicted && d.min && d.max) - .x(d => this.scale[isNav ? "x2" : "x"](d.date)) - .y0(d => this.scale[isNav ? "y2" : "y"](d.min)) - .y1(d => this.scale[isNav ? "y2" : "y"](d.max)); + .x(d => scale[isNav ? "x2" : "x"](d.date)) + .y0(d => scale[isNav ? "y2" : "y"](d.min)) + .y1(d => scale[isNav ? "y2" : "y"](d.max)); - this.diagram + diagram .append("path") .datum(this.waterlevels) .attr("class", "prediction-area") .attr("d", predictionArea()); - this.navigation + navigation .append("path") .datum(this.waterlevels) .attr("class", "prediction-area") .attr("d", predictionArea(true)); return () => { - this.diagram.select(".prediction-area").attr("d", predictionArea()); + diagram.select(".prediction-area").attr("d", predictionArea()); }; }, - drawRefLines(refWaterLevels) { + drawRefLines({ refWaterLevels, diagram, scale, dimensions, extent }) { // filling area between HDC and LDC - this.diagram + diagram .append("rect") .attr("class", "hdc-ldc-area") .attr("x", 0) - .attr("y", this.scale.y(refWaterLevels.HDC)) - .attr("width", this.dimensions.width) + .attr("y", scale.y(refWaterLevels.HDC)) + .attr("width", dimensions.width) .attr( "height", - this.scale.y(refWaterLevels.LDC) - this.scale.y(refWaterLevels.HDC) + scale.y(refWaterLevels.LDC) - scale.y(refWaterLevels.HDC) ); const refWaterlevelLine = d3 .line() - .x(d => this.scale.x(d.x)) - .y(d => this.scale.y(d.y)); + .x(d => scale.x(d.x)) + .y(d => scale.y(d.y)); for (let ref in refWaterLevels) { if (refWaterLevels[ref]) { - this.diagram + diagram .append("path") .datum([ { x: 0, y: refWaterLevels[ref] }, - { x: this.extent.date[1], y: refWaterLevels[ref] } + { x: extent.date[1], y: refWaterLevels[ref] } ]) .attr("class", ref.toLowerCase() + "-line") .attr("d", refWaterlevelLine); - this.diagram // label + diagram // label .append("rect") .attr("class", "ref-waterlevel-label-background") .attr("x", 1) - .attr("y", this.scale.y(refWaterLevels[ref]) - 13) + .attr("y", scale.y(refWaterLevels[ref]) - 13) .attr("width", 55) .attr("height", 12); - this.diagram + diagram .append("text") .text( `${ref} (${this.$options.filters.waterlevel( @@ -867,47 +879,45 @@ ) .attr("class", "ref-waterlevel-label") .attr("x", 5) - .attr("y", this.scale.y(refWaterLevels[ref]) - 3); + .attr("y", scale.y(refWaterLevels[ref]) - 3); } } }, - drawNashSutcliffe(hours) { + drawNashSutcliffe({ hours, diagram, scale, dimensions }) { const coeff = this.nashSutcliffe.coeffs.find(c => c.hours === hours); const dateNow = new Date(this.nashSutcliffe.when); const dateStart = new Date(dateNow.getTime() - hours * 60 * 60 * 1000); const nashSutcliffeBox = hours => { // show/hide boxes depending on scale of chart (hide if > 90 days) - this.diagram + diagram .selectAll("path.nash-sutcliffe") .attr( "stroke-opacity", - this.scale.x.domain()[1] - this.scale.x.domain()[0] > 90 * 86400000 - ? 0 - : 1 + scale.x.domain()[1] - scale.x.domain()[0] > 90 * 86400000 ? 0 : 1 ); return d3 .area() - .x(d => this.scale.x(d)) - .y0(() => this.dimensions.mainHeight + 0.5) - .y1(() => this.dimensions.mainHeight - 15 * (hours / 24)); + .x(d => scale.x(d)) + .y0(() => dimensions.mainHeight + 0.5) + .y1(() => dimensions.mainHeight - 15 * (hours / 24)); }; const nashSutcliffeLabel = (label, date, hours) => { let days = hours / 24; label - .attr("x", Math.min(this.scale.x(date), this.dimensions.width) - 4) - .attr("y", this.dimensions.mainHeight - (15 * days + 0.5) + 12); + .attr("x", Math.min(scale.x(date), dimensions.width) - 4) + .attr("y", dimensions.mainHeight - (15 * days + 0.5) + 12); }; if (coeff.samples) { - this.diagram + diagram .append("path") .datum([dateStart, dateNow]) .attr("class", "nash-sutcliffe ns" + hours) .attr("d", nashSutcliffeBox(hours)); - this.diagram + diagram .append("text") .attr("class", "nash-sutcliffe ns" + hours) .attr("text-anchor", "end") @@ -927,42 +937,39 @@ } return () => { - this.diagram + diagram .select("path.nash-sutcliffe.ns" + hours) .attr("d", nashSutcliffeBox(hours)); - this.diagram + diagram .select("text.nash-sutcliffe.ns" + hours) .call(nashSutcliffeLabel, dateNow, hours); }; }, - createZoom(updaters, eventRect) { + createZoom({ updaters, eventRect, dimensions, scale, navigation, svg }) { const brush = d3 .brushX() .handleSize(4) - .extent([[0, 0], [this.dimensions.width, this.dimensions.navHeight]]); + .extent([[0, 0], [dimensions.width, dimensions.navHeight]]); const zoom = d3 .zoom() .scaleExtent([1, Infinity]) - .translateExtent([ - [0, 0], - [this.dimensions.width, this.dimensions.mainHeight] - ]) - .extent([[0, 0], [this.dimensions.width, this.dimensions.mainHeight]]); + .translateExtent([[0, 0], [dimensions.width, dimensions.mainHeight]]) + .extent([[0, 0], [dimensions.width, dimensions.mainHeight]]); brush.on("brush end", () => { if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom - let s = d3.event.selection || this.scale.x2.range(); - this.scale.x.domain(s.map(this.scale.x2.invert, this.scale.x2)); + let s = d3.event.selection || scale.x2.range(); + scale.x.domain(s.map(scale.x2.invert, scale.x2)); updaters.forEach(u => u && u()); - this.setInlineStyles(); - this.svg + this.setInlineStyles(svg); + svg .select(".zoom") .call( zoom.transform, d3.zoomIdentity - .scale(this.dimensions.width / (s[1] - s[0])) + .scale(dimensions.width / (s[1] - s[0])) .translate(-s[0], 0) ); }); @@ -971,29 +978,29 @@ if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush let t = d3.event.transform; - this.scale.x.domain(t.rescaleX(this.scale.x2).domain()); + scale.x.domain(t.rescaleX(scale.x2).domain()); updaters.forEach(u => u && u()); - this.setInlineStyles(); - this.navigation + this.setInlineStyles(svg); + navigation .select(".brush") - .call(brush.move, this.scale.x.range().map(t.invertX, t)); + .call(brush.move, scale.x.range().map(t.invertX, t)); }); zoom.on("start", () => { - this.svg.select(".chart-dot").style("opacity", 0); - this.svg.select(".chart-tooltip").style("opacity", 0); + svg.select(".chart-dot").style("opacity", 0); + svg.select(".chart-tooltip").style("opacity", 0); }); - this.navigation + navigation .append("g") .attr("class", "brush") .call(brush) - .call(brush.move, this.scale.x.range()); + .call(brush.move, scale.x.range()); eventRect.call(zoom); }, - createTooltips(eventRect) { + createTooltips({ eventRect, diagram, scale, dimensions }) { // create clippable container for the dot - this.diagram + diagram .append("g") .attr("class", "chart-dots") .append("circle") @@ -1001,7 +1008,7 @@ .attr("r", 4); // create container for the tooltip - const tooltip = this.diagram.append("g").attr("class", "chart-tooltip"); + const tooltip = diagram.append("g").attr("class", "chart-tooltip"); tooltip .append("rect") .attr("rx", "0.25em") @@ -1019,16 +1026,16 @@ eventRect .on("mouseover", () => { - this.diagram.select(".chart-dot").style("opacity", 1); - this.diagram.select(".chart-tooltip").style("opacity", 1); + diagram.select(".chart-dot").style("opacity", 1); + diagram.select(".chart-tooltip").style("opacity", 1); }) .on("mouseout", () => { - this.diagram.select(".chart-dot").style("opacity", 0); - this.diagram.select(".chart-tooltip").style("opacity", 0); + diagram.select(".chart-dot").style("opacity", 0); + diagram.select(".chart-tooltip").style("opacity", 0); }) .on("mousemove", () => { // find data point closest to mouse - const x0 = this.scale.x.invert( + const x0 = scale.x.invert( d3.mouse(document.getElementById("zoom-waterlevels"))[0] ), i = d3.bisector(d => d.date).left(this.waterlevels, x0, 1), @@ -1037,12 +1044,12 @@ d = x0 - d0.date > d1.date - x0 ? d1 : d0; const coords = { - x: this.scale.x(d.date), - y: this.scale.y(d.waterlevel) + x: scale.x(d.date), + y: scale.y(d.waterlevel) }; // position the dot - this.diagram + diagram .select(".chart-dot") .style("opacity", 1) .attr("transform", `translate(${coords.x}, ${coords.y})`); @@ -1106,14 +1113,14 @@ // get text dimensions const textBBox = tooltipText.node().getBBox(); - this.diagram + diagram .selectAll(".chart-tooltip text tspan") .attr("x", textBBox.width / 2 + tooltipPadding) .attr("y", tooltipPadding); // position and scale tooltip box const xMax = - this.dimensions.width - + dimensions.width - (textBBox.width + diagramPadding + tooltipPadding * 2); const tooltipX = Math.max( diagramPadding, @@ -1124,7 +1131,7 @@ tooltipY = coords.y + 10; } - this.diagram + diagram .select(".chart-tooltip") .style("opacity", 1) .attr("transform", `translate(${tooltipX}, ${tooltipY})`) @@ -1133,17 +1140,14 @@ .attr("height", textBBox.height + tooltipPadding * 2); }); }, - isNext() { + isNext(scale) { // Check whether points in the chart can be considered "next to each other". // For that they need to be exactly 15 minutes apart (for automatically // imported gauge measurements). If the chart shows more than 15 days then // 1 hour is also valid (for approved gauge measurements). return (prev, current) => { let difference = (current.date - prev.date) / 1000; - if ( - (this.scale.x.domain()[1] - this.scale.x.domain()[0]) / 86400000 > - 15 - ) + if ((scale.x.domain()[1] - scale.x.domain()[0]) / 86400000 > 15) return [900, 3600].includes(difference); return difference === 900; };