comparison client/src/components/fairway/AvailableFairwayDepth.vue @ 3963:feb53713bc2f diagram-cleanup

client: moved duplicated code to mixins and unified code patterns in diagram components [WIP]
author Markus Kottlaender <markus@intevation.de>
date Fri, 12 Jul 2019 15:14:16 +0200
parents 7b3935a8d9ee
children afc7bca44df4
comparison
equal deleted inserted replaced
3944:0ed8af02d5a9 3963:feb53713bc2f
1 <template> 1 <template>
2 <div class="d-flex flex-column flex-fill"> 2 <div class="d-flex flex-column flex-fill">
3 <UIBoxHeader icon="chart-area" :title="title" :closeCallback="close" /> 3 <UIBoxHeader icon="chart-area" :title="title" :closeCallback="close" />
4 <UISpinnerOverlay v-if="loading" />
5 <div class="d-flex flex-fill"> 4 <div class="d-flex flex-fill">
6 <DiagramLegend> 5 <DiagramLegend>
7 <div v-for="(entry, index) in legend" :key="index" class="legend"> 6 <div v-for="(entry, index) in legend" :key="index" class="legend">
8 <span 7 <span
9 :style=" 8 :style="
42 >Download CSV</a 41 >Download CSV</a
43 > 42 >
44 </div> 43 </div>
45 </DiagramLegend> 44 </DiagramLegend>
46 <div 45 <div
47 ref="diagramContainer" 46 class="d-flex flex-fill justify-content-center align-items-center"
48 :id="containerId" 47 :id="containerId"
49 class="diagram-container flex-fill" 48 >
50 ></div> 49 <div v-if="!fwData.length">
50 <translate>No data available.</translate>
51 </div>
52 </div>
51 </div> 53 </div>
52 </div> 54 </div>
53 </template> 55 </template>
54
55 <style></style>
56 56
57 <script> 57 <script>
58 /* This is Free Software under GNU Affero General Public License v >= 3.0 58 /* This is Free Software under GNU Affero General Public License v >= 3.0
59 * without warranty, see README.md and license for details. 59 * without warranty, see README.md and license for details.
60 * 60 *
69 * * Thomas Junk <thomas.junk@intevation.de> 69 * * Thomas Junk <thomas.junk@intevation.de>
70 * * Markus Kottländer <markus.kottlaender@intevation.de> 70 * * Markus Kottländer <markus.kottlaender@intevation.de>
71 * * Fadi Abbud <fadi.abbud@intevation.de> 71 * * Fadi Abbud <fadi.abbud@intevation.de>
72 */ 72 */
73 import * as d3 from "d3"; 73 import * as d3 from "d3";
74 import app from "@/main";
75 import debounce from "debounce";
76 import { mapState } from "vuex"; 74 import { mapState } from "vuex";
75 import { diagram, pdfgen, templateLoader } from "@/lib/mixins";
76 import { FREQUENCIES } from "@/store/fairwayavailability";
77 import filters from "@/lib/filters.js"; 77 import filters from "@/lib/filters.js";
78 import { diagram, pdfgen, templateLoader } from "@/lib/mixins"; 78
79 import { HTTP } from "@/lib/http"; 79 // hoursInDays is a const instead of component method because it's used where
80 import { displayError } from "@/lib/errors"; 80 // the component context is not available
81 import { FREQUENCIES } from "@/store/fairwayavailability";
82 import { defaultDiagramTemplate } from "@/lib/DefaultDiagramTemplate";
83
84 const hoursInDays = x => x / 24; 81 const hoursInDays = x => x / 24;
82 const COLORS = {
83 LDC: "#cdcdcd",
84 HIGHEST: "#3675ff",
85 REST: ["#782121", "#ff6c6c", "#ffaaaa"]
86 };
85 87
86 export default { 88 export default {
87 mixins: [diagram, pdfgen, templateLoader], 89 mixins: [diagram, pdfgen, templateLoader],
88 components: { 90 components: {
89 DiagramLegend: () => import("@/components/DiagramLegend") 91 DiagramLegend: () => import("@/components/DiagramLegend")
90 }, 92 },
91 data() { 93 data() {
92 return { 94 return {
93 containerId: "availablefairwaydepth-diagram-container", 95 containerId: "availablefairwaydepth-diagram-container",
94 resizeListenerFunction: null,
95 loading: false,
96 scalePaddingLeft: 60, 96 scalePaddingLeft: 60,
97 scalePaddingRight: 0, 97 scalePaddingRight: 0,
98 paddingTop: 25, 98 paddingTop: 25
99 pdf: {
100 doc: null,
101 width: null,
102 height: null
103 },
104 form: {
105 template: null
106 },
107 templateData: null,
108 templates: [],
109 defaultTemplate: defaultDiagramTemplate
110 }; 99 };
111 },
112 created() {
113 this.resizeListenerFunction = debounce(this.drawDiagram, 100);
114 window.addEventListener("resize", this.resizeListenerFunction);
115 },
116 destroyed() {
117 window.removeEventListener("resize", this.resizeListenerFunction);
118 },
119 mounted() {
120 // Nasty but necessary if we don't want to use the updated hook to re-draw
121 // the diagram because this would re-draw it also for irrelevant reasons.
122 // In this case we need to wait for the child component (DiagramLegend) to
123 // render. According to the docs (https://vuejs.org/v2/api/#mounted) this
124 // should be possible with $nextTick() but it doesn't work because it does
125 // not guarantee that the DOM is not only updated but also re-painted on the
126 // screen.
127 setTimeout(this.drawDiagram, 150);
128
129 this.templates[0] = this.defaultTemplate;
130 this.form.template = this.templates[0];
131 this.templateData = this.form.template;
132 HTTP.get("/templates/diagram", {
133 headers: {
134 "X-Gemma-Auth": localStorage.getItem("token"),
135 "Content-type": "text/xml; charset=UTF-8"
136 }
137 })
138 .then(response => {
139 if (response.data.length) {
140 this.templates = response.data;
141 this.form.template = this.templates[0];
142 this.templates[this.templates.length] = this.defaultTemplate;
143 this.applyChange();
144 }
145 })
146 .catch(e => {
147 const { status, data } = e.response;
148 displayError({
149 title: this.$gettext("Backend Error"),
150 message: `${status}: ${data.message || data}`
151 });
152 });
153 }, 100 },
154 computed: { 101 computed: {
155 ...mapState("fairwayavailability", [ 102 ...mapState("fairwayavailability", [
156 "selectedFairwayAvailabilityFeature", 103 "selectedFairwayAvailabilityFeature",
157 "fwData", 104 "fwData",
214 if (this.selectedFairwayAvailabilityFeature == null) return ""; 161 if (this.selectedFairwayAvailabilityFeature == null) return "";
215 return this.selectedFairwayAvailabilityFeature.properties.name; 162 return this.selectedFairwayAvailabilityFeature.properties.name;
216 } 163 }
217 }, 164 },
218 methods: { 165 methods: {
219 applyChange() {
220 if (this.form.template.hasOwnProperty("properties")) {
221 this.templateData = this.defaultTemplate;
222 return;
223 }
224 if (this.form.template) {
225 this.loadTemplates("/templates/diagram/" + this.form.template.name)
226 .then(response => {
227 this.prepareImages(response.data.template_data.elements).then(
228 values => {
229 values.forEach(v => {
230 response.data.template_data.elements[v.index].url = v.url;
231 });
232 this.templateData = response.data.template_data;
233 }
234 );
235 })
236 .catch(e => {
237 const { status, data } = e.response;
238 displayError({
239 title: this.$gettext("Backend Error"),
240 message: `${status}: ${data.message || data}`
241 });
242 });
243 }
244 },
245 downloadPDF() { 166 downloadPDF() {
246 let title = `Available Fairway Depth: ${this.featureName}`; 167 let title = `Available Fairway Depth: ${this.featureName}`;
247 this.generatePDF({ 168 this.generatePDF({
248 templateData: this.templateData, 169 templateData: this.templateData,
249 diagramTitle: title 170 diagramTitle: title
264 if (["bottomright", "bottomleft"].indexOf(position) !== -1) { 185 if (["bottomright", "bottomleft"].indexOf(position) !== -1) {
265 y = this.pdf.height - offset.y - this.getTextHeight(6); 186 y = this.pdf.height - offset.y - this.getTextHeight(6);
266 } 187 }
267 188
268 this.pdf.doc.setTextColor(color); 189 this.pdf.doc.setTextColor(color);
269 this.pdf.doc.setDrawColor(this.$options.COLORS.LDC); 190 this.pdf.doc.setDrawColor(COLORS.LDC);
270 this.pdf.doc.setFillColor(this.$options.COLORS.LDC); 191 this.pdf.doc.setFillColor(COLORS.LDC);
271 this.pdf.doc.roundedRect(x, y, 10, 4, 1.5, 1.5, "FD"); 192 this.pdf.doc.roundedRect(x, y, 10, 4, 1.5, 1.5, "FD");
272 this.pdf.doc.text(this.legend[0], x + 12, y + 3); 193 this.pdf.doc.text(this.legend[0], x + 12, y + 3);
273 194
274 this.pdf.doc.setDrawColor(this.$options.COLORS.REST[0]); 195 this.pdf.doc.setDrawColor(COLORS.REST[0]);
275 this.pdf.doc.setFillColor(this.$options.COLORS.REST[0]); 196 this.pdf.doc.setFillColor(COLORS.REST[0]);
276 this.pdf.doc.roundedRect(x, y + 5, 10, 4, 1.5, 1.5, "FD"); 197 this.pdf.doc.roundedRect(x, y + 5, 10, 4, 1.5, 1.5, "FD");
277 this.pdf.doc.text(this.legend[1], x + 12, y + 8); 198 this.pdf.doc.text(this.legend[1], x + 12, y + 8);
278 199
279 this.pdf.doc.setDrawColor(this.$options.COLORS.REST[1]); 200 this.pdf.doc.setDrawColor(COLORS.REST[1]);
280 this.pdf.doc.setFillColor(this.$options.COLORS.REST[1]); 201 this.pdf.doc.setFillColor(COLORS.REST[1]);
281 this.pdf.doc.roundedRect(x, y + 10, 10, 4, 1.5, 1.5, "FD"); 202 this.pdf.doc.roundedRect(x, y + 10, 10, 4, 1.5, 1.5, "FD");
282 this.pdf.doc.text(this.legend[2], x + 12, y + 13); 203 this.pdf.doc.text(this.legend[2], x + 12, y + 13);
283 204
284 this.pdf.doc.setDrawColor(this.$options.COLORS.HIGHEST); 205 this.pdf.doc.setDrawColor(COLORS.HIGHEST);
285 this.pdf.doc.setFillColor(this.$options.COLORS.HIGHEST); 206 this.pdf.doc.setFillColor(COLORS.HIGHEST);
286 this.pdf.doc.roundedRect(x, y + 15, 10, 4, 1.5, 1.5, "FD"); 207 this.pdf.doc.roundedRect(x, y + 15, 10, 4, 1.5, 1.5, "FD");
287 this.pdf.doc.text(this.legend[3], x + 12, y + 18); 208 this.pdf.doc.text(this.legend[3], x + 12, y + 18);
288 }, 209 },
289 legendStyle(index) { 210 legendStyle(index) {
290 return [ 211 return [
291 `background-color: ${this.$options.COLORS.LDC};`, 212 `background-color: ${COLORS.LDC};`,
292 `background-color: ${this.$options.COLORS.HIGHEST};`, 213 `background-color: ${COLORS.HIGHEST};`,
293 `background-color: ${this.$options.COLORS.REST[1]};`, 214 `background-color: ${COLORS.REST[1]};`,
294 `background-color: ${this.$options.COLORS.REST[0]};` 215 `background-color: ${COLORS.REST[0]};`
295 ][index]; 216 ][index];
296 }, 217 },
297 close() { 218 close() {
298 this.$store.commit("application/paneSetup", "DEFAULT"); 219 this.$store.commit("application/paneSetup", "DEFAULT");
299 }, 220 },
300 getPrintLayout(svgHeight) { 221 getPrintLayout() {
301 return { 222 return {
302 main: { top: 0, right: 20, bottom: 50, left: 20 }, 223 main: { top: 0, right: 20, bottom: 50, left: 20 }
303 nav: {
304 top: svgHeight - 65,
305 right: 20,
306 bottom: 30,
307 left: 80
308 }
309 }; 224 };
310 }, 225 },
311 drawDiagram() { 226 drawDiagram() {
312 const elem = document.querySelector("#" + this.containerId); 227 const elem = document.querySelector("#" + this.containerId);
313 const svgWidth = elem != null ? elem.clientWidth : 0; 228 const svgWidth = elem != null ? elem.clientWidth : 0;
314 const svgHeight = elem != null ? elem.clientHeight : 0; 229 const svgHeight = elem != null ? elem.clientHeight : 0;
315 const layout = this.getPrintLayout(svgHeight); 230 const layout = this.getPrintLayout();
316 const dimensions = this.getDimensions({ 231 const dimensions = this.getDimensions({
317 svgHeight, 232 svgHeight,
318 svgWidth, 233 svgWidth,
319 ...layout 234 ...layout
320 }); 235 });
321 d3.select(".diagram-container svg").remove(); 236 d3.select("#" + this.containerId + " svg").remove();
322 this.renderTo({ element: ".diagram-container", dimensions }); 237 this.renderTo({ element: "#" + this.containerId, dimensions });
323 }, 238 },
324 renderTo({ element, dimensions }) { 239 renderTo({ element, dimensions }) {
325 const diagram = d3 240 const diagram = d3
326 .select(element) 241 .select(element)
327 .append("svg") 242 .append("svg")
438 return yScale(0) - yScale(hoursInDays(d.height)); 353 return yScale(0) - yScale(hoursInDays(d.height));
439 }) 354 })
440 .attr("x", ldcOffset + spaceBetween / 2) 355 .attr("x", ldcOffset + spaceBetween / 2)
441 .attr("width", widthPerItem - ldcOffset - spaceBetween) 356 .attr("width", widthPerItem - ldcOffset - spaceBetween)
442 .attr("fill", (d, i) => { 357 .attr("fill", (d, i) => {
443 return this.$options.COLORS.REST[i]; 358 return COLORS.REST[i];
444 }); 359 });
445 }, 360 },
446 fnheight({ name, yScale }) { 361 fnheight({ name, yScale }) {
447 return d => yScale(0) - yScale(hoursInDays(d[name])); 362 return d => yScale(0) - yScale(hoursInDays(d[name]));
448 }, 363 },
477 .attr("width", widthPerItem - ldcOffset - spaceBetween) 392 .attr("width", widthPerItem - ldcOffset - spaceBetween)
478 .attr( 393 .attr(
479 "transform", 394 "transform",
480 d => `translate(0 ${this.paddingTop + -1 * height(d)})` 395 d => `translate(0 ${this.paddingTop + -1 * height(d)})`
481 ) 396 )
482 .attr("fill", this.$options.COLORS.LDC) 397 .attr("fill", COLORS.LDC)
483 .attr("id", "ldc"); 398 .attr("id", "ldc");
484 }, 399 },
485 drawHighestLevel({ 400 drawHighestLevel({
486 everyBar, 401 everyBar,
487 yScale, 402 yScale,
521 .attr("width", widthPerItem - ldcOffset - spaceBetween) 436 .attr("width", widthPerItem - ldcOffset - spaceBetween)
522 .attr( 437 .attr(
523 "transform", 438 "transform",
524 d => `translate(0 ${this.paddingTop + -1 * height(d)})` 439 d => `translate(0 ${this.paddingTop + -1 * height(d)})`
525 ) 440 )
526 .attr("fill", this.$options.COLORS.HIGHEST); 441 .attr("fill", COLORS.HIGHEST);
527 }, 442 },
528 drawLabelPerBar({ everyBar, dimensions, widthPerItem }) { 443 drawLabelPerBar({ everyBar, dimensions, widthPerItem }) {
529 everyBar 444 everyBar
530 .append("text") 445 .append("text")
531 .text(d => d.label) 446 .text(d => d.label)
535 .attr("font-size", 10); 450 .attr("font-size", 10);
536 }, 451 },
537 drawScaleLabel({ diagram, dimensions }) { 452 drawScaleLabel({ diagram, dimensions }) {
538 diagram 453 diagram
539 .append("text") 454 .append("text")
540 .text(this.$options.LEGEND) 455 .text(this.$gettext("Sum of days"))
541 .attr("text-anchor", "middle") 456 .attr("text-anchor", "middle")
542 .attr("x", 0) 457 .attr("x", 0)
543 .attr("y", 0) 458 .attr("y", 0)
544 .attr("dy", "20") 459 .attr("dy", "20")
545 .attr( 460 .attr(
613 }, 528 },
614 watch: { 529 watch: {
615 fwData() { 530 fwData() {
616 this.drawDiagram(); 531 this.drawDiagram();
617 } 532 }
618 },
619 LEGEND: app.$gettext("Sum of days"),
620 COLORS: {
621 LDC: "#cdcdcd",
622 HIGHEST: "#3675ff",
623 REST: ["#782121", "#ff6c6c", "#ffaaaa"]
624 } 533 }
625 }; 534 };
626 </script> 535 </script>