Mercurial > gemma
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", |