comparison client/src/components/fairway/AvailableFairwayDepth.vue @ 4334:8ac59c8183e8

client: add showNumbers to AvailableFairwayDepth diagram * Add bootstrap button to the diagram legend. Below the the other options because of its lesser importance. * Change calculation of days from hours to use one precision after the decimal point to show that we are coming from summed hours. This shall lead to clarification what users expect, see code comment. * Draw the numbers close to the bars: Above for LDC and highestLevel, below for the "lowerLevels". * Enchance test data in docs/developers.md to cover more interesting cases.
author Bernhard Reiter <bernhard@intevation.de>
date Thu, 05 Sep 2019 14:50:05 +0200
parents 0d516bac1aae
children 2f212f520a04
comparison
equal deleted inserted replaced
4333:3f0422751cb4 4334:8ac59c8183e8
40 :download="csvFileName" 40 :download="csvFileName"
41 class="mt-2 btn btn-sm btn-info w-100" 41 class="mt-2 btn btn-sm btn-info w-100"
42 >Download CSV</a 42 >Download CSV</a
43 > 43 >
44 </div> 44 </div>
45 <div class="btn-group-toggle w-100 mt-2">
46 <label
47 class="btn btn-outline-secondary btn-sm"
48 :class="{ active: showNumbers }"
49 ><input
50 type="checkbox"
51 v-model="showNumbers"
52 autocomplete="off"
53 />Numbers
54 </label>
55 </div>
45 </DiagramLegend> 56 </DiagramLegend>
46 <div 57 <div
47 ref="diagramContainer" 58 ref="diagramContainer"
48 :id="containerId" 59 :id="containerId"
49 class="diagram-container flex-fill" 60 class="diagram-container flex-fill"
67 * 78 *
68 * Author(s): 79 * Author(s):
69 * * Thomas Junk <thomas.junk@intevation.de> 80 * * Thomas Junk <thomas.junk@intevation.de>
70 * * Markus Kottländer <markus.kottlaender@intevation.de> 81 * * Markus Kottländer <markus.kottlaender@intevation.de>
71 * * Fadi Abbud <fadi.abbud@intevation.de> 82 * * Fadi Abbud <fadi.abbud@intevation.de>
83 * * Bernhard Reiter <bernhard.reiter@intevation.de>
72 */ 84 */
73 import * as d3 from "d3"; 85 import * as d3 from "d3";
74 import app from "@/main"; 86 import app from "@/main";
75 import debounce from "debounce"; 87 import debounce from "debounce";
76 import { mapState } from "vuex"; 88 import { mapState } from "vuex";
79 import { HTTP } from "@/lib/http"; 91 import { HTTP } from "@/lib/http";
80 import { displayError } from "@/lib/errors"; 92 import { displayError } from "@/lib/errors";
81 import { FREQUENCIES } from "@/store/fairwayavailability"; 93 import { FREQUENCIES } from "@/store/fairwayavailability";
82 import { defaultDiagramTemplate } from "@/lib/DefaultDiagramTemplate"; 94 import { defaultDiagramTemplate } from "@/lib/DefaultDiagramTemplate";
83 95
84 const hoursInDays = x => Math.round(x / 24); 96 // FIXME This is a rounding methods that shows that we have fractions,
97 // because we are coming from hours. Users will understand the underlying
98 // math better and we can see if this is wanted.
99 // With the backend just giving us the summarized hours, we cannot do
100 // a classification of each day into a category.
101 // (The name of the function is kept to keep the diff more readable and
102 // should changed if this is more clarified.)
103 const hoursInDays = x => Math.round((x * 10) / 24) / 10;
85 104
86 export default { 105 export default {
87 mixins: [diagram, pdfgen, templateLoader], 106 mixins: [diagram, pdfgen, templateLoader],
88 components: { 107 components: {
89 DiagramLegend: () => import("@/components/DiagramLegend") 108 DiagramLegend: () => import("@/components/DiagramLegend")
104 form: { 123 form: {
105 template: null 124 template: null
106 }, 125 },
107 templateData: null, 126 templateData: null,
108 templates: [], 127 templates: [],
109 defaultTemplate: defaultDiagramTemplate 128 defaultTemplate: defaultDiagramTemplate,
129 showNumbers: false
110 }; 130 };
111 }, 131 },
112 created() { 132 created() {
113 this.resizeListenerFunction = debounce(this.drawDiagram, 100); 133 this.resizeListenerFunction = debounce(this.drawDiagram, 100);
114 window.addEventListener("resize", this.resizeListenerFunction); 134 window.addEventListener("resize", this.resizeListenerFunction);
452 .on("mousemove", function(d) { 472 .on("mousemove", function(d) {
453 let y = d3.mouse(this)[1]; 473 let y = d3.mouse(this)[1];
454 const dy = document 474 const dy = document
455 .querySelector(".diagram-container") 475 .querySelector(".diagram-container")
456 .getBoundingClientRect().left; 476 .getBoundingClientRect().left;
457 const value = Number.parseFloat(hoursInDays(d.height)).toFixed(2);
458 d3.select("#tooltip") 477 d3.select("#tooltip")
459 .text(Math.round(value)) 478 .text(hoursInDays(d.height))
460 .attr("y", y - 10) 479 .attr("y", y - 10)
461 .attr("x", d3.event.pageX - dy); 480 .attr("x", d3.event.pageX - dy);
462 //d3.event.pageX gives coordinates relative to SVG 481 //d3.event.pageX gives coordinates relative to SVG
463 //dy gives offset of svg on page 482 //dy gives offset of svg on page
464 }) 483 })
470 .attr("height", d => { 489 .attr("height", d => {
471 return yScale(0) - yScale(hoursInDays(d.height)); 490 return yScale(0) - yScale(hoursInDays(d.height));
472 }) 491 })
473 .attr("x", ldcOffset + spaceBetween / 2) 492 .attr("x", ldcOffset + spaceBetween / 2)
474 .attr("width", widthPerItem - ldcOffset - spaceBetween) 493 .attr("width", widthPerItem - ldcOffset - spaceBetween)
494 .attr("id", "lower")
475 .attr("fill", (d, i) => { 495 .attr("fill", (d, i) => {
476 return this.$options.COLORS.REST[i]; 496 return this.$options.COLORS.REST[i];
477 }); 497 });
498 if (this.showNumbers) {
499 everyBar
500 .selectAll("g.bars")
501 .data(d => d.lowerLevels)
502 .enter()
503 .filter(d => hoursInDays(d.height) > 0)
504 .insert("text")
505 .attr("y", d => {
506 return ( 2 * yScale(0)
507 - yScale(hoursInDays(d.translateY))
508 + this.paddingTop
509 + (yScale(0) - yScale(hoursInDays(d.height)))
510 + (yScale(0) - yScale(1.9)) //instead o alignment-baseline hanging
511 );
512 })
513 .attr("x", widthPerItem / 2)
514 .text(d => hoursInDays(d.height))
515 // does not work with svg2pdf .attr("alignment-baseline", "hanging")
516 .attr("text-anchor", "middle")
517 .attr("font-size", "8")
518 .attr("fill", "black");
519 }
478 }, 520 },
479 fnheight({ name, yScale }) { 521 fnheight({ name, yScale }) {
480 return d => yScale(0) - yScale(hoursInDays(d[name])); 522 return d => yScale(0) - yScale(hoursInDays(d[name]));
481 }, 523 },
482 drawLDC({ everyBar, yScale, widthPerItem, spaceBetween, ldcOffset }) { 524 drawLDC({ everyBar, yScale, widthPerItem, spaceBetween, ldcOffset }) {
483 const height = this.fnheight({ name: "ldc", yScale }); 525 const height = this.fnheight({ name: "ldc", yScale });
484 everyBar 526 everyBar
485 .append("rect") 527 .append("rect")
486 .on("mouseover", function() { 528 .on("mouseover", function() {
487 d3.select(this).attr("opacity", "0.8"); 529 d3.select(this).attr("opacity", "0.7");
488 d3.select("#tooltip").attr("opacity", 1); 530 d3.select("#tooltip").attr("opacity", 1);
489 }) 531 })
490 .on("mouseout", function() { 532 .on("mouseout", function() {
491 d3.select(this).attr("opacity", 1); 533 d3.select(this).attr("opacity", 1);
492 d3.select("#tooltip").attr("opacity", 0); 534 d3.select("#tooltip").attr("opacity", 0);
494 .on("mousemove", function(d) { 536 .on("mousemove", function(d) {
495 let y = d3.mouse(this)[1]; 537 let y = d3.mouse(this)[1];
496 const dy = document 538 const dy = document
497 .querySelector(".diagram-container") 539 .querySelector(".diagram-container")
498 .getBoundingClientRect().left; 540 .getBoundingClientRect().left;
499 const value = Number.parseFloat(hoursInDays(d.ldc)).toFixed(2);
500 d3.select("#tooltip") 541 d3.select("#tooltip")
501 .text(Math.round(value)) 542 .text(hoursInDays(d.ldc))
502 .attr("y", y - 50) 543 .attr("y", y - 50)
503 .attr("x", d3.event.pageX - dy); 544 .attr("x", d3.event.pageX - dy);
504 //d3.event.pageX gives coordinates relative to SVG 545 //d3.event.pageX gives coordinates relative to SVG
505 //dy gives offset of svg on page 546 //dy gives offset of svg on page
506 }) 547 })
512 "transform", 553 "transform",
513 d => `translate(0 ${this.paddingTop + -1 * height(d)})` 554 d => `translate(0 ${this.paddingTop + -1 * height(d)})`
514 ) 555 )
515 .attr("fill", this.$options.COLORS.LDC) 556 .attr("fill", this.$options.COLORS.LDC)
516 .attr("id", "ldc"); 557 .attr("id", "ldc");
558 if (this.showNumbers) {
559 everyBar
560 .filter(d => hoursInDays(d.ldc) > 0)
561 .append("text")
562 .attr("y", yScale(0.5)) // some distance from the bar
563 .attr("x", spaceBetween / 2)
564 .text(d => hoursInDays(d.ldc))
565 .attr("text-anchor", "left")
566 .attr("font-size", "8")
567 .attr(
568 "transform",
569 d => `translate(0 ${this.paddingTop + -1 * height(d)})`
570 )
571 .attr("fill", "black");
572 }
517 }, 573 },
518 drawHighestLevel({ 574 drawHighestLevel({
519 everyBar, 575 everyBar,
520 yScale, 576 yScale,
521 widthPerItem, 577 widthPerItem,
536 .on("mousemove", function(d) { 592 .on("mousemove", function(d) {
537 let y = d3.mouse(this)[1]; 593 let y = d3.mouse(this)[1];
538 const dy = document 594 const dy = document
539 .querySelector(".diagram-container") 595 .querySelector(".diagram-container")
540 .getBoundingClientRect().left; 596 .getBoundingClientRect().left;
541 const value = Number.parseFloat(hoursInDays(d.highestLevel)).toFixed(
542 2
543 );
544 d3.select("#tooltip") 597 d3.select("#tooltip")
545 .text(Math.round(value)) 598 .text(hoursInDays(d.highestLevel))
546 .attr("y", y - 50) 599 .attr("y", y - 50)
547 .attr("x", d3.event.pageX - dy); 600 .attr("x", d3.event.pageX - dy);
548 //d3.event.pageX gives coordinates relative to SVG 601 //d3.event.pageX gives coordinates relative to SVG
549 //dy gives offset of svg on page 602 //dy gives offset of svg on page
550 }) 603 })
555 .attr( 608 .attr(
556 "transform", 609 "transform",
557 d => `translate(0 ${this.paddingTop + -1 * height(d)})` 610 d => `translate(0 ${this.paddingTop + -1 * height(d)})`
558 ) 611 )
559 .attr("fill", this.$options.COLORS.HIGHEST); 612 .attr("fill", this.$options.COLORS.HIGHEST);
613 if (this.showNumbers) {
614 everyBar
615 .filter(d => hoursInDays(d.highestLevel) > 0)
616 .append("text")
617 .attr("y", yScale(0.5)) // some distance from the bar
618 .attr("x", widthPerItem / 2)
619 .text(d => hoursInDays(d.highestLevel))
620 .attr("text-anchor", "middle")
621 .attr("font-size", "8")
622 .attr(
623 "transform",
624 d => `translate(0 ${this.paddingTop + -1 * height(d)})`
625 )
626 .attr("fill", "black");
627 }
560 }, 628 },
561 drawLabelPerBar({ everyBar, dimensions, widthPerItem }) { 629 drawLabelPerBar({ everyBar, dimensions, widthPerItem }) {
562 everyBar 630 everyBar
563 .append("text") 631 .append("text")
564 .text(d => d.label) 632 .text(d => d.label)
645 } 713 }
646 }, 714 },
647 watch: { 715 watch: {
648 fwData() { 716 fwData() {
649 this.drawDiagram(); 717 this.drawDiagram();
718 },
719 showNumbers() {
720 this.drawDiagram();
650 } 721 }
651 }, 722 },
652 LEGEND: app.$gettext("Sum of days"), 723 LEGEND: app.$gettext("Sum of days"),
653 COLORS: { 724 COLORS: {
654 LDC: "aqua", 725 LDC: "aqua",