Mercurial > gemma
comparison client/src/components/gauge/HydrologicalConditions.vue @ 3869:7d86beedfb00
hydrological conditions: factor out side-effects from diagram rendering
author | Thomas Junk <thomas.junk@intevation.de> |
---|---|
date | Tue, 09 Jul 2019 16:50:36 +0200 |
parents | 36e85a74fca0 |
children | a7a75e003b78 |
comparison
equal
deleted
inserted
replaced
3868:91b4ca03174e | 3869:7d86beedfb00 |
---|---|
116 }, | 116 }, |
117 data() { | 117 data() { |
118 return { | 118 return { |
119 containerId: "hydrologicalconditions-diagram-container", | 119 containerId: "hydrologicalconditions-diagram-container", |
120 resizeListenerFunction: null, | 120 resizeListenerFunction: null, |
121 svg: null, | |
122 diagram: null, | |
123 navigation: null, | |
124 dimensions: null, | |
125 extent: null, | |
126 scale: null, | |
127 axes: null, | |
128 templateData: null, | 121 templateData: null, |
129 form: { | 122 form: { |
130 template: null, | 123 template: null, |
131 form: null | 124 form: null |
132 }, | 125 }, |
352 const refWaterLevels = JSON.parse( | 345 const refWaterLevels = JSON.parse( |
353 this.selectedGauge.properties.reference_water_levels | 346 this.selectedGauge.properties.reference_water_levels |
354 ); | 347 ); |
355 | 348 |
356 // dimensions (widths, heights, margins) | 349 // dimensions (widths, heights, margins) |
357 this.dimensions = this.getDimensions(); | 350 const dimensions = this.getDimensions(); |
358 | 351 |
359 // get min/max values for date and waterlevel axis | 352 // get min/max values for date and waterlevel axis |
360 this.extent = this.getExtent(refWaterLevels); | 353 const extent = this.getExtent(refWaterLevels); |
361 | 354 |
362 // scaling helpers | 355 // scaling helpers |
363 this.scale = this.getScale(); | 356 const scale = this.getScale({ dimensions, extent }); |
364 | 357 |
365 // creating the axes based on the scales | 358 // creating the axes based on the scales |
366 this.axes = this.getAxes(); | 359 const axes = this.getAxes({ scale, dimensions }); |
367 | 360 |
368 // DRAW DIAGRAM/NAVIGATION AREAS | 361 // DRAW DIAGRAM/NAVIGATION AREAS |
369 | 362 |
370 // create svg | 363 // create svg |
371 this.svg = d3 | 364 const svg = d3 |
372 .select(element) | 365 .select(element) |
373 .append("svg") | 366 .append("svg") |
374 .attr("width", "100%") | 367 .attr("width", "100%") |
375 .attr("height", "100%"); | 368 .attr("height", "100%"); |
376 | 369 |
377 // create container for main diagram | 370 // create container for main diagram |
378 this.diagram = this.svg | 371 const diagram = svg |
379 .append("g") | 372 .append("g") |
380 .attr("class", "main") | 373 .attr("class", "main") |
381 .attr( | 374 .attr( |
382 "transform", | 375 "transform", |
383 `translate(${this.dimensions.mainMargin.left}, ${ | 376 `translate(${dimensions.mainMargin.left}, ${ |
384 this.dimensions.mainMargin.top | 377 dimensions.mainMargin.top |
385 })` | 378 })` |
386 ); | 379 ); |
387 | 380 |
388 // create container for navigation diagram | 381 // create container for navigation diagram |
389 this.navigation = this.svg | 382 const navigation = svg |
390 .append("g") | 383 .append("g") |
391 .attr("class", "nav") | 384 .attr("class", "nav") |
392 .attr( | 385 .attr( |
393 "transform", | 386 "transform", |
394 `translate(${this.dimensions.navMargin.left}, ${ | 387 `translate(${dimensions.navMargin.left}, ${dimensions.navMargin.top})` |
395 this.dimensions.navMargin.top | |
396 })` | |
397 ); | 388 ); |
398 | 389 |
399 // define visible area, everything outside this area will be hidden | 390 // define visible area, everything outside this area will be hidden |
400 this.svg | 391 svg |
401 .append("defs") | 392 .append("defs") |
402 .append("clipPath") | 393 .append("clipPath") |
403 .attr("id", "hydrocond-clip") | 394 .attr("id", "hydrocond-clip") |
404 .append("rect") | 395 .append("rect") |
405 .attr("width", this.dimensions.width) | 396 .attr("width", dimensions.width) |
406 .attr("height", this.dimensions.mainHeight); | 397 .attr("height", dimensions.mainHeight); |
407 | 398 |
408 // DRAW DIAGRAM PARTS | 399 // DRAW DIAGRAM PARTS |
409 | 400 |
410 // Each drawSomething function (with the exception of drawRefLines) | 401 // Each drawSomething function (with the exception of drawRefLines) |
411 // returns a fuction to update the respective chart/area/etc. These | 402 // returns a fuction to update the respective chart/area/etc. These |
412 // updater functions are used by the zoom feature to rescale all elements. | 403 // updater functions are used by the zoom feature to rescale all elements. |
413 const updaters = []; | 404 const updaters = []; |
414 | 405 |
415 // draw | 406 // draw |
416 updaters.push(this.drawAxes()); | 407 updaters.push(this.drawAxes({ diagram, dimensions, axes, navigation })); |
417 updaters.push(this.drawWaterlevelMinMaxAreaChart()); | 408 updaters.push( |
418 updaters.push(this.drawWaterlevelLineChart("median")); | 409 this.drawWaterlevelMinMaxAreaChart({ scale, diagram, navigation }) |
419 updaters.push(this.drawWaterlevelLineChart("q25")); | 410 ); |
420 updaters.push(this.drawWaterlevelLineChart("q75")); | 411 updaters.push( |
421 updaters.push(this.drawWaterlevelLineChart("mean", this.yearWaterlevels)); | 412 this.drawWaterlevelLineChart({ type: "median", scale, diagram }) |
422 updaters.push(this.drawNowLines()); | 413 ); |
414 updaters.push( | |
415 this.drawWaterlevelLineChart({ type: "q25", scale, diagram }) | |
416 ); | |
417 updaters.push( | |
418 this.drawWaterlevelLineChart({ type: "q75", scale, diagram }) | |
419 ); | |
420 updaters.push( | |
421 this.drawWaterlevelLineChart({ | |
422 type: "mean", | |
423 data: this.yearWaterlevels, | |
424 scale, | |
425 diagram | |
426 }) | |
427 ); | |
428 updaters.push(this.drawNowLines({ extent, diagram, scale, navigation })); | |
423 | 429 |
424 if (refWaterLevels) { | 430 if (refWaterLevels) { |
425 this.drawRefLines(refWaterLevels); // static, doesn't need an updater | 431 this.drawRefLines({ refWaterLevels, scale, diagram, extent }); // static, doesn't need an updater |
426 } | 432 } |
427 | 433 |
428 // INTERACTIONS | 434 // INTERACTIONS |
429 | 435 |
430 // create rectanlge on the main chart area to capture mouse events | 436 // create rectanlge on the main chart area to capture mouse events |
431 const eventRect = this.svg | 437 const eventRect = svg |
432 .append("rect") | 438 .append("rect") |
433 .attr("id", "zoom-hydrocond") | 439 .attr("id", "zoom-hydrocond") |
434 .attr("class", "zoom") | 440 .attr("class", "zoom") |
435 .attr("width", this.dimensions.width) | 441 .attr("width", dimensions.width) |
436 .attr("height", this.dimensions.mainHeight) | 442 .attr("height", dimensions.mainHeight) |
437 .attr( | 443 .attr( |
438 "transform", | 444 "transform", |
439 `translate(${this.dimensions.mainMargin.left}, ${ | 445 `translate(${dimensions.mainMargin.left}, ${ |
440 this.dimensions.mainMargin.top | 446 dimensions.mainMargin.top |
441 })` | 447 })` |
442 ); | 448 ); |
443 | 449 |
444 this.createZoom(updaters, eventRect); | 450 this.createZoom({ |
445 this.createTooltips(eventRect); | 451 updaters, |
446 this.setInlineStyles(); | 452 eventRect, |
447 }, | 453 dimensions, |
448 setInlineStyles() { | 454 scale, |
449 this.svg.selectAll(".hide").attr("fill-opacity", 0); | 455 svg, |
450 this.svg | 456 navigation |
457 }); | |
458 this.createTooltips({ eventRect, diagram, scale, dimensions }); | |
459 this.setInlineStyles(svg); | |
460 }, | |
461 setInlineStyles(svg) { | |
462 svg.selectAll(".hide").attr("fill-opacity", 0); | |
463 svg | |
451 .selectAll(".line") | 464 .selectAll(".line") |
452 .attr("clip-path", "url(#hydrocond-clip)") | 465 .attr("clip-path", "url(#hydrocond-clip)") |
453 .attr("stroke-width", 2) | 466 .attr("stroke-width", 2) |
454 .attr("fill", "none"); | 467 .attr("fill", "none"); |
455 this.svg.selectAll(".line.mean").attr("stroke", "red"); | 468 svg.selectAll(".line.mean").attr("stroke", "red"); |
456 this.svg.selectAll(".line.median").attr("stroke", "black"); | 469 svg.selectAll(".line.median").attr("stroke", "black"); |
457 this.svg.selectAll(".line.q25").attr("stroke", "orange"); | 470 svg.selectAll(".line.q25").attr("stroke", "orange"); |
458 this.svg.selectAll(".line.q75").attr("stroke", "purple"); | 471 svg.selectAll(".line.q75").attr("stroke", "purple"); |
459 this.svg | 472 svg |
460 .selectAll(".area") | 473 .selectAll(".area") |
461 .attr("clip-path", "url(#hydrocond-clip)") | 474 .attr("clip-path", "url(#hydrocond-clip)") |
462 .attr("stroke", "none") | 475 .attr("stroke", "none") |
463 .attr("fill", "lightsteelblue"); | 476 .attr("fill", "lightsteelblue"); |
464 this.svg | 477 svg |
465 .selectAll(".hdc-line, .ldc-line, .mw-line, .rn-line") | 478 .selectAll(".hdc-line, .ldc-line, .mw-line, .rn-line") |
466 .attr("stroke-width", 1) | 479 .attr("stroke-width", 1) |
467 .attr("fill", "none") | 480 .attr("fill", "none") |
468 .attr("clip-path", "url(#hydrocond-clip)"); | 481 .attr("clip-path", "url(#hydrocond-clip)"); |
469 this.svg.selectAll(".hdc-line").attr("stroke", "red"); | 482 svg.selectAll(".hdc-line").attr("stroke", "red"); |
470 this.svg.selectAll(".ldc-line").attr("stroke", "green"); | 483 svg.selectAll(".ldc-line").attr("stroke", "green"); |
471 this.svg.selectAll(".mw-line").attr("stroke", "grey"); | 484 svg.selectAll(".mw-line").attr("stroke", "grey"); |
472 this.svg.selectAll(".rn-line").attr("stroke", "grey"); | 485 svg.selectAll(".rn-line").attr("stroke", "grey"); |
473 this.svg | 486 svg |
474 .selectAll(".ref-waterlevel-label") | 487 .selectAll(".ref-waterlevel-label") |
475 .style("font-size", "10px") | 488 .style("font-size", "10px") |
476 .attr("fill", "black"); | 489 .attr("fill", "black"); |
477 this.svg | 490 svg |
478 .selectAll(".ref-waterlevel-label-background") | 491 .selectAll(".ref-waterlevel-label-background") |
479 .attr("fill", "rgba(255, 255, 255, 0.6)"); | 492 .attr("fill", "rgba(255, 255, 255, 0.6)"); |
480 this.svg | 493 svg |
481 .selectAll(".now-line") | 494 .selectAll(".now-line") |
482 .attr("stroke", "#999") | 495 .attr("stroke", "#999") |
483 .attr("stroke-width", 1) | 496 .attr("stroke-width", 1) |
484 .attr("stroke-dasharray", "5, 5") | 497 .attr("stroke-dasharray", "5, 5") |
485 .attr("clip-path", "url(#hydrocond-clip)"); | 498 .attr("clip-path", "url(#hydrocond-clip)"); |
486 this.svg | 499 svg |
487 .selectAll(".now-line-label") | 500 .selectAll(".now-line-label") |
488 .attr("fill", "#999") | 501 .attr("fill", "#999") |
489 .style("font-size", "11px"); | 502 .style("font-size", "11px"); |
490 this.svg | 503 svg |
491 .selectAll(".tick line") | 504 .selectAll(".tick line") |
492 .attr("stroke-dasharray", 5) | 505 .attr("stroke-dasharray", 5) |
493 .attr("stroke", "#ccc"); | 506 .attr("stroke", "#ccc"); |
494 this.svg.selectAll(".tick text").attr("fill", "black"); | 507 svg.selectAll(".tick text").attr("fill", "black"); |
495 this.svg.selectAll(".domain").attr("stroke", "black"); | 508 svg.selectAll(".domain").attr("stroke", "black"); |
496 | 509 |
497 this.svg | 510 svg |
498 .selectAll(".zoom") | 511 .selectAll(".zoom") |
499 .attr("cursor", "move") | 512 .attr("cursor", "move") |
500 .attr("fill", "none") | 513 .attr("fill", "none") |
501 .attr("pointer-events", "all"); | 514 .attr("pointer-events", "all"); |
502 this.svg | 515 svg |
503 .selectAll(".brush .selection") | 516 .selectAll(".brush .selection") |
504 .attr("stroke", "none") | 517 .attr("stroke", "none") |
505 .attr("fill-opacity", 0.2); | 518 .attr("fill-opacity", 0.2); |
506 this.svg | 519 svg |
507 .selectAll(".brush .handle") | 520 .selectAll(".brush .handle") |
508 .attr("stroke", "rgba(23, 162, 184, 0.5)") | 521 .attr("stroke", "rgba(23, 162, 184, 0.5)") |
509 .attr("fill", "rgba(23, 162, 184, 0.5)"); | 522 .attr("fill", "rgba(23, 162, 184, 0.5)"); |
510 this.svg | 523 svg.selectAll(".chart-dots").attr("clip-path", "url(#hydrocond-clip)"); |
511 .selectAll(".chart-dots") | 524 svg |
512 .attr("clip-path", "url(#hydrocond-clip)"); | |
513 this.svg | |
514 .selectAll(".chart-dots .chart-dot") | 525 .selectAll(".chart-dots .chart-dot") |
515 .attr("fill", "black") | 526 .attr("fill", "black") |
516 .attr("stroke", "black") | 527 .attr("stroke", "black") |
517 .attr("stroke-opacity", 0) | 528 .attr("stroke-opacity", 0) |
518 .style("pointer-events", "none") | 529 .style("pointer-events", "none") |
519 .attr("fill-opacity", 0) | 530 .attr("fill-opacity", 0) |
520 .transition() | 531 .transition() |
521 .attr("fill-opacity", "0.1s"); | 532 .attr("fill-opacity", "0.1s"); |
522 this.svg | 533 svg |
523 .selectAll(".chart-tooltip") | 534 .selectAll(".chart-tooltip") |
524 .attr("fill-opacity", 0) | 535 .attr("fill-opacity", 0) |
525 .transition() | 536 .transition() |
526 .attr("fill-opacity", "0.3s"); | 537 .attr("fill-opacity", "0.3s"); |
527 this.svg | 538 svg |
528 .selectAll(".chart-tooltip rect") | 539 .selectAll(".chart-tooltip rect") |
529 .attr("fill", "#fff") | 540 .attr("fill", "#fff") |
530 .attr("stroke", "#ccc"); | 541 .attr("stroke", "#ccc"); |
531 this.svg | 542 svg |
532 .selectAll(".chart-tooltip text") | 543 .selectAll(".chart-tooltip text") |
533 .attr("fill", "666") | 544 .attr("fill", "666") |
534 .style("font-size", "0.8em"); | 545 .style("font-size", "0.8em"); |
535 }, | 546 }, |
536 getDimensions() { | 547 getDimensions() { |
578 // set min/max values for the waterlevel axis | 589 // set min/max values for the waterlevel axis |
579 // including HDC (+ 1/8 HDC-LDC) and LDC (- 1/4 HDC-LDC) | 590 // including HDC (+ 1/8 HDC-LDC) and LDC (- 1/4 HDC-LDC) |
580 waterlevel: d3.extent(waterlevelValues) | 591 waterlevel: d3.extent(waterlevelValues) |
581 }; | 592 }; |
582 }, | 593 }, |
583 getScale() { | 594 getScale({ dimensions, extent }) { |
584 // scaling helpers to convert real world values into pixels | 595 // scaling helpers to convert real world values into pixels |
585 const x = d3.scaleTime().range([0, this.dimensions.width]); | 596 const x = d3.scaleTime().range([0, dimensions.width]); |
586 const y = d3.scaleLinear().range([this.dimensions.mainHeight, 0]); | 597 const y = d3.scaleLinear().range([dimensions.mainHeight, 0]); |
587 const x2 = d3.scaleTime().range([0, this.dimensions.width]); | 598 const x2 = d3.scaleTime().range([0, dimensions.width]); |
588 const y2 = d3.scaleLinear().range([this.dimensions.navHeight, 0]); | 599 const y2 = d3.scaleLinear().range([dimensions.navHeight, 0]); |
589 | 600 |
590 // setting the min and max values for the diagram axes | 601 // setting the min and max values for the diagram axes |
591 x.domain(d3.extent(this.extent.date)); | 602 x.domain(d3.extent(extent.date)); |
592 y.domain(this.extent.waterlevel); | 603 y.domain(extent.waterlevel); |
593 x2.domain(x.domain()); | 604 x2.domain(x.domain()); |
594 y2.domain(y.domain()); | 605 y2.domain(y.domain()); |
595 | 606 |
596 return { x, y, x2, y2 }; | 607 return { x, y, x2, y2 }; |
597 }, | 608 }, |
598 getAxes() { | 609 getAxes({ scale, dimensions }) { |
599 return { | 610 return { |
600 x: d3 | 611 x: d3 |
601 .axisTop(this.scale.x) | 612 .axisTop(scale.x) |
602 .tickSizeInner(this.dimensions.mainHeight) | 613 .tickSizeInner(dimensions.mainHeight) |
603 .tickSizeOuter(0) | 614 .tickSizeOuter(0) |
604 .tickFormat(date => { | 615 .tickFormat(date => { |
605 // make the x-axis label formats dynamic, based on zoom | 616 // make the x-axis label formats dynamic, based on zoom |
606 // but never display year numbers since they don't make any sense in | 617 // but never display year numbers since they don't make any sense in |
607 // this diagram | 618 // this diagram |
618 ? d3.timeFormat("%a %d") | 629 ? d3.timeFormat("%a %d") |
619 : d3.timeFormat("%b %d") | 630 : d3.timeFormat("%b %d") |
620 : d3.timeFormat("%B"))(date); | 631 : d3.timeFormat("%B"))(date); |
621 }), | 632 }), |
622 y: d3 | 633 y: d3 |
623 .axisRight(this.scale.y) | 634 .axisRight(scale.y) |
624 .tickSizeInner(this.dimensions.width) | 635 .tickSizeInner(dimensions.width) |
625 .tickSizeOuter(0) | 636 .tickSizeOuter(0) |
626 .tickFormat(d => this.$options.filters.waterlevel(d)), | 637 .tickFormat(d => this.$options.filters.waterlevel(d)), |
627 x2: d3.axisBottom(this.scale.x2) | 638 x2: d3.axisBottom(scale.x2) |
628 }; | 639 }; |
629 }, | 640 }, |
630 drawNowLines() { | 641 drawNowLines({ extent, diagram, scale, navigation }) { |
631 const now = new Date(); | 642 const now = new Date(); |
632 const nowCoords = [ | 643 const nowCoords = [ |
633 { x: now, y: this.extent.waterlevel[0] }, | 644 { x: now, y: extent.waterlevel[0] }, |
634 { x: now, y: this.extent.waterlevel[1] } | 645 { x: now, y: extent.waterlevel[1] } |
635 ]; | 646 ]; |
636 const nowLine = d3 | 647 const nowLine = d3 |
637 .line() | 648 .line() |
638 .x(d => this.scale.x(d.x)) | 649 .x(d => scale.x(d.x)) |
639 .y(d => this.scale.y(d.y)); | 650 .y(d => scale.y(d.y)); |
640 const nowLabel = selection => { | 651 const nowLabel = selection => { |
641 selection.attr( | 652 selection.attr( |
642 "transform", | 653 "transform", |
643 `translate(${this.scale.x(now)}, ${this.scale.y( | 654 `translate(${scale.x(now)}, ${scale.y(extent.waterlevel[1])})` |
644 this.extent.waterlevel[1] | |
645 )})` | |
646 ); | 655 ); |
647 }; | 656 }; |
648 | 657 |
649 // draw in main | 658 // draw in main |
650 this.diagram | 659 diagram |
651 .append("path") | 660 .append("path") |
652 .datum(nowCoords) | 661 .datum(nowCoords) |
653 .attr("class", "now-line") | 662 .attr("class", "now-line") |
654 .attr("d", nowLine); | 663 .attr("d", nowLine); |
655 this.diagram // label | 664 diagram // label |
656 .append("text") | 665 .append("text") |
657 .text(this.$gettext("Now")) | 666 .text(this.$gettext("Now")) |
658 .attr("class", "now-line-label") | 667 .attr("class", "now-line-label") |
659 .attr("text-anchor", "middle") | 668 .attr("text-anchor", "middle") |
660 .call(nowLabel); | 669 .call(nowLabel); |
661 | 670 |
662 // draw in nav | 671 // draw in nav |
663 this.navigation | 672 navigation |
664 .append("path") | 673 .append("path") |
665 .datum(nowCoords) | 674 .datum(nowCoords) |
666 .attr("class", "now-line") | 675 .attr("class", "now-line") |
667 .attr( | 676 .attr( |
668 "d", | 677 "d", |
669 d3 | 678 d3 |
670 .line() | 679 .line() |
671 .x(d => this.scale.x2(d.x)) | 680 .x(d => scale.x2(d.x)) |
672 .y(d => this.scale.y2(d.y)) | 681 .y(d => scale.y2(d.y)) |
673 ); | 682 ); |
674 | 683 |
675 return () => { | 684 return () => { |
676 this.diagram.select(".now-line").attr("d", nowLine); | 685 diagram.select(".now-line").attr("d", nowLine); |
677 this.diagram.select(".now-line-label").call(nowLabel); | 686 diagram.select(".now-line-label").call(nowLabel); |
678 }; | 687 }; |
679 }, | 688 }, |
680 drawAxes() { | 689 drawAxes({ diagram, dimensions, axes, navigation }) { |
681 this.diagram | 690 diagram |
682 .append("g") | 691 .append("g") |
683 .attr("class", "axis--x") | 692 .attr("class", "axis--x") |
684 .attr("transform", `translate(0, ${this.dimensions.mainHeight})`) | 693 .attr("transform", `translate(0, ${dimensions.mainHeight})`) |
685 .call(this.axes.x) | 694 .call(axes.x) |
686 .selectAll(".tick text") | 695 .selectAll(".tick text") |
687 .attr("y", 15); | 696 .attr("y", 15); |
688 this.diagram // label | 697 diagram // label |
689 .append("text") | 698 .append("text") |
690 .text(this.$gettext("Waterlevel [m]")) | 699 .text(this.$gettext("Waterlevel [m]")) |
691 .attr("text-anchor", "middle") | 700 .attr("text-anchor", "middle") |
692 .attr( | 701 .attr( |
693 "transform", | 702 "transform", |
694 `translate(-45, ${this.dimensions.mainHeight / 2}) rotate(-90)` | 703 `translate(-45, ${dimensions.mainHeight / 2}) rotate(-90)` |
695 ); | 704 ); |
696 this.diagram | 705 diagram |
697 .append("g") | 706 .append("g") |
698 .call(this.axes.y) | 707 .call(axes.y) |
699 .selectAll(".tick text") | 708 .selectAll(".tick text") |
700 .attr("x", -25); | 709 .attr("x", -25); |
701 | 710 |
702 this.navigation | 711 navigation |
703 .append("g") | 712 .append("g") |
704 .attr("class", "axis axis--x") | 713 .attr("class", "axis axis--x") |
705 .attr("transform", `translate(0, ${this.dimensions.navHeight})`) | 714 .attr("transform", `translate(0, ${dimensions.navHeight})`) |
706 .call(this.axes.x2); | 715 .call(axes.x2); |
707 | 716 |
708 return () => { | 717 return () => { |
709 this.diagram | 718 diagram |
710 .select(".axis--x") | 719 .select(".axis--x") |
711 .call(this.axes.x) | 720 .call(axes.x) |
712 .selectAll(".tick text") | 721 .selectAll(".tick text") |
713 .attr("y", 15); | 722 .attr("y", 15); |
714 }; | 723 }; |
715 }, | 724 }, |
716 drawWaterlevelMinMaxAreaChart() { | 725 drawWaterlevelMinMaxAreaChart({ scale, diagram, navigation }) { |
717 const areaChart = isNav => | 726 const areaChart = isNav => |
718 d3 | 727 d3 |
719 .area() | 728 .area() |
720 .x(d => this.scale[isNav ? "x2" : "x"](d.date)) | 729 .x(d => scale[isNav ? "x2" : "x"](d.date)) |
721 .y0(d => this.scale[isNav ? "y2" : "y"](d.min)) | 730 .y0(d => scale[isNav ? "y2" : "y"](d.min)) |
722 .y1(d => this.scale[isNav ? "y2" : "y"](d.max)); | 731 .y1(d => scale[isNav ? "y2" : "y"](d.max)); |
723 | 732 |
724 this.diagram | 733 diagram |
725 .append("path") | 734 .append("path") |
726 .datum(this.longtermWaterlevels) | 735 .datum(this.longtermWaterlevels) |
727 .attr("class", "area") | 736 .attr("class", "area") |
728 .attr("d", areaChart()); | 737 .attr("d", areaChart()); |
729 | 738 |
730 this.navigation | 739 navigation |
731 .append("path") | 740 .append("path") |
732 .datum(this.longtermWaterlevels) | 741 .datum(this.longtermWaterlevels) |
733 .attr("class", "area") | 742 .attr("class", "area") |
734 .attr("d", areaChart(true)); | 743 .attr("d", areaChart(true)); |
735 | 744 |
736 return () => { | 745 return () => { |
737 this.diagram.select(".area").attr("d", areaChart()); | 746 diagram.select(".area").attr("d", areaChart()); |
738 }; | 747 }; |
739 }, | 748 }, |
740 drawWaterlevelLineChart(type, data) { | 749 drawWaterlevelLineChart({ type, data, scale, diagram }) { |
741 const lineChart = type => | 750 const lineChart = type => |
742 d3 | 751 d3 |
743 .line() | 752 .line() |
744 .x(d => this.scale.x(d.date)) | 753 .x(d => scale.x(d.date)) |
745 .y(d => this.scale.y(d[type])) | 754 .y(d => scale.y(d[type])) |
746 .curve(d3.curveLinear); | 755 .curve(d3.curveLinear); |
747 this.diagram | 756 diagram |
748 .append("path") | 757 .append("path") |
749 .attr("class", "line " + type) | 758 .attr("class", "line " + type) |
750 .datum(data || this.longtermWaterlevels) | 759 .datum(data || this.longtermWaterlevels) |
751 .attr("d", lineChart(type)); | 760 .attr("d", lineChart(type)); |
752 | 761 |
753 return () => { | 762 return () => { |
754 this.diagram.select(".line." + type).attr("d", lineChart(type)); | 763 diagram.select(".line." + type).attr("d", lineChart(type)); |
755 }; | 764 }; |
756 }, | 765 }, |
757 drawRefLines(refWaterLevels) { | 766 drawRefLines({ refWaterLevels, scale, diagram, extent }) { |
758 const refWaterlevelLine = d3 | 767 const refWaterlevelLine = d3 |
759 .line() | 768 .line() |
760 .x(d => this.scale.x(d.x)) | 769 .x(d => scale.x(d.x)) |
761 .y(d => this.scale.y(d.y)); | 770 .y(d => scale.y(d.y)); |
762 | 771 |
763 for (let ref in refWaterLevels) { | 772 for (let ref in refWaterLevels) { |
764 if (refWaterLevels[ref]) { | 773 if (refWaterLevels[ref]) { |
765 this.diagram | 774 diagram |
766 .append("path") | 775 .append("path") |
767 .datum([ | 776 .datum([ |
768 { x: this.extent.date[0], y: refWaterLevels[ref] }, | 777 { x: extent.date[0], y: refWaterLevels[ref] }, |
769 { x: this.extent.date[1], y: refWaterLevels[ref] } | 778 { x: extent.date[1], y: refWaterLevels[ref] } |
770 ]) | 779 ]) |
771 .attr("class", ref.toLowerCase() + "-line") | 780 .attr("class", ref.toLowerCase() + "-line") |
772 .attr("d", refWaterlevelLine); | 781 .attr("d", refWaterlevelLine); |
773 this.diagram // label | 782 diagram // label |
774 .append("rect") | 783 .append("rect") |
775 .attr("class", "ref-waterlevel-label-background") | 784 .attr("class", "ref-waterlevel-label-background") |
776 .attr("x", 1) | 785 .attr("x", 1) |
777 .attr("y", this.scale.y(refWaterLevels[ref]) - 13) | 786 .attr("y", scale.y(refWaterLevels[ref]) - 13) |
778 .attr("width", 55) | 787 .attr("width", 55) |
779 .attr("height", 12); | 788 .attr("height", 12); |
780 this.diagram | 789 diagram |
781 .append("text") | 790 .append("text") |
782 .text( | 791 .text( |
783 `${ref} (${this.$options.filters.waterlevel( | 792 `${ref} (${this.$options.filters.waterlevel( |
784 refWaterLevels[ref] | 793 refWaterLevels[ref] |
785 )})` | 794 )})` |
786 ) | 795 ) |
787 .attr("class", "ref-waterlevel-label") | 796 .attr("class", "ref-waterlevel-label") |
788 .attr("x", 5) | 797 .attr("x", 5) |
789 .attr("y", this.scale.y(refWaterLevels[ref]) - 3); | 798 .attr("y", scale.y(refWaterLevels[ref]) - 3); |
790 } | 799 } |
791 } | 800 } |
792 }, | 801 }, |
793 createZoom(updaters, eventRect) { | 802 createZoom({ updaters, eventRect, dimensions, scale, svg, navigation }) { |
794 const brush = d3 | 803 const brush = d3 |
795 .brushX() | 804 .brushX() |
796 .handleSize(4) | 805 .handleSize(4) |
797 .extent([[0, 0], [this.dimensions.width, this.dimensions.navHeight]]); | 806 .extent([[0, 0], [dimensions.width, dimensions.navHeight]]); |
798 | 807 |
799 const zoom = d3 | 808 const zoom = d3 |
800 .zoom() | 809 .zoom() |
801 .scaleExtent([1, Infinity]) | 810 .scaleExtent([1, Infinity]) |
802 .translateExtent([ | 811 .translateExtent([[0, 0], [dimensions.width, dimensions.mainHeight]]) |
803 [0, 0], | 812 .extent([[0, 0], [dimensions.width, dimensions.mainHeight]]); |
804 [this.dimensions.width, this.dimensions.mainHeight] | |
805 ]) | |
806 .extent([[0, 0], [this.dimensions.width, this.dimensions.mainHeight]]); | |
807 | 813 |
808 brush.on("brush end", () => { | 814 brush.on("brush end", () => { |
809 if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") | 815 if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") |
810 return; // ignore brush-by-zoom | 816 return; // ignore brush-by-zoom |
811 let s = d3.event.selection || this.scale.x2.range(); | 817 let s = d3.event.selection || scale.x2.range(); |
812 this.scale.x.domain(s.map(this.scale.x2.invert, this.scale.x2)); | 818 scale.x.domain(s.map(scale.x2.invert, scale.x2)); |
813 updaters.forEach(u => u && u()); | 819 updaters.forEach(u => u && u()); |
814 this.setInlineStyles(); | 820 this.setInlineStyles(svg); |
815 this.svg | 821 svg |
816 .select(".zoom") | 822 .select(".zoom") |
817 .call( | 823 .call( |
818 zoom.transform, | 824 zoom.transform, |
819 d3.zoomIdentity | 825 d3.zoomIdentity |
820 .scale(this.dimensions.width / (s[1] - s[0])) | 826 .scale(dimensions.width / (s[1] - s[0])) |
821 .translate(-s[0], 0) | 827 .translate(-s[0], 0) |
822 ); | 828 ); |
823 }); | 829 }); |
824 | 830 |
825 zoom.on("zoom", () => { | 831 zoom.on("zoom", () => { |
826 if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") | 832 if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") |
827 return; // ignore zoom-by-brush | 833 return; // ignore zoom-by-brush |
828 let t = d3.event.transform; | 834 let t = d3.event.transform; |
829 this.scale.x.domain(t.rescaleX(this.scale.x2).domain()); | 835 scale.x.domain(t.rescaleX(scale.x2).domain()); |
830 updaters.forEach(u => u && u()); | 836 updaters.forEach(u => u && u()); |
831 this.setInlineStyles(); | 837 this.setInlineStyles(svg); |
832 this.navigation | 838 navigation |
833 .select(".brush") | 839 .select(".brush") |
834 .call(brush.move, this.scale.x.range().map(t.invertX, t)); | 840 .call(brush.move, scale.x.range().map(t.invertX, t)); |
835 }); | 841 }); |
836 zoom.on("start", () => { | 842 zoom.on("start", () => { |
837 this.svg.select(".chart-dot").style("opacity", 0); | 843 svg.select(".chart-dot").style("opacity", 0); |
838 this.svg.select(".chart-tooltip").style("opacity", 0); | 844 svg.select(".chart-tooltip").style("opacity", 0); |
839 }); | 845 }); |
840 | 846 |
841 this.navigation | 847 navigation |
842 .append("g") | 848 .append("g") |
843 .attr("class", "brush") | 849 .attr("class", "brush") |
844 .call(brush) | 850 .call(brush) |
845 .call(brush.move, this.scale.x.range()); | 851 .call(brush.move, scale.x.range()); |
846 | 852 |
847 eventRect.call(zoom); | 853 eventRect.call(zoom); |
848 }, | 854 }, |
849 createTooltips(eventRect) { | 855 createTooltips({ eventRect, diagram, scale, dimensions }) { |
850 // create clippable container for the dot | 856 // create clippable container for the dot |
851 this.diagram | 857 diagram |
852 .append("g") | 858 .append("g") |
853 .attr("class", "chart-dots") | 859 .attr("class", "chart-dots") |
854 .append("circle") | 860 .append("circle") |
855 .attr("class", "chart-dot") | 861 .attr("class", "chart-dot") |
856 .attr("r", 4); | 862 .attr("r", 4); |
857 | 863 |
858 // create container for the tooltip | 864 // create container for the tooltip |
859 const tooltip = this.diagram.append("g").attr("class", "chart-tooltip"); | 865 const tooltip = diagram.append("g").attr("class", "chart-tooltip"); |
860 tooltip | 866 tooltip |
861 .append("rect") | 867 .append("rect") |
862 .attr("rx", "0.25em") | 868 .attr("rx", "0.25em") |
863 .attr("ry", "0.25em"); | 869 .attr("ry", "0.25em"); |
864 | 870 |
870 const tooltipPadding = 10; | 876 const tooltipPadding = 10; |
871 const diagramPadding = 5; | 877 const diagramPadding = 5; |
872 | 878 |
873 eventRect | 879 eventRect |
874 .on("mouseover", () => { | 880 .on("mouseover", () => { |
875 this.diagram.select(".chart-dot").style("opacity", 1); | 881 diagram.select(".chart-dot").style("opacity", 1); |
876 this.diagram.select(".chart-tooltip").style("opacity", 1); | 882 diagram.select(".chart-tooltip").style("opacity", 1); |
877 }) | 883 }) |
878 .on("mouseout", () => { | 884 .on("mouseout", () => { |
879 this.diagram.select(".chart-dot").style("opacity", 0); | 885 diagram.select(".chart-dot").style("opacity", 0); |
880 this.diagram.select(".chart-tooltip").style("opacity", 0); | 886 diagram.select(".chart-tooltip").style("opacity", 0); |
881 }) | 887 }) |
882 .on("mousemove", () => { | 888 .on("mousemove", () => { |
883 // find data point closest to mouse | 889 // find data point closest to mouse |
884 const x0 = this.scale.x.invert( | 890 const x0 = scale.x.invert( |
885 d3.mouse(document.getElementById("zoom-hydrocond"))[0] | 891 d3.mouse(document.getElementById("zoom-hydrocond"))[0] |
886 ), | 892 ), |
887 i = d3.bisector(d => d.date).left(this.longtermWaterlevels, x0, 1), | 893 i = d3.bisector(d => d.date).left(this.longtermWaterlevels, x0, 1), |
888 d0 = this.longtermWaterlevels[i - 1], | 894 d0 = this.longtermWaterlevels[i - 1], |
889 d1 = this.longtermWaterlevels[i] || d0, | 895 d1 = this.longtermWaterlevels[i] || d0, |
890 d = x0 - d0.date > d1.date - x0 ? d1 : d0; | 896 d = x0 - d0.date > d1.date - x0 ? d1 : d0; |
891 | 897 |
892 const coords = { | 898 const coords = { |
893 x: this.scale.x(d.date), | 899 x: scale.x(d.date), |
894 y: this.scale.y(d.median) | 900 y: scale.y(d.median) |
895 }; | 901 }; |
896 | 902 |
897 // position the dot | 903 // position the dot |
898 this.diagram | 904 diagram |
899 .select(".chart-dot") | 905 .select(".chart-dot") |
900 .style("opacity", 1) | 906 .style("opacity", 1) |
901 .attr("transform", `translate(${coords.x}, ${coords.y})`); | 907 .attr("transform", `translate(${coords.x}, ${coords.y})`); |
902 | 908 |
903 // remove current texts | 909 // remove current texts |
972 } | 978 } |
973 | 979 |
974 // get text dimensions | 980 // get text dimensions |
975 const textBBox = tooltipText.node().getBBox(); | 981 const textBBox = tooltipText.node().getBBox(); |
976 | 982 |
977 this.diagram | 983 diagram |
978 .selectAll(".chart-tooltip text tspan") | 984 .selectAll(".chart-tooltip text tspan") |
979 .attr("x", textBBox.width / 2 + tooltipPadding) | 985 .attr("x", textBBox.width / 2 + tooltipPadding) |
980 .attr("y", tooltipPadding); | 986 .attr("y", tooltipPadding); |
981 | 987 |
982 // position and scale tooltip box | 988 // position and scale tooltip box |
983 const xMax = | 989 const xMax = |
984 this.dimensions.width - | 990 dimensions.width - |
985 (textBBox.width + diagramPadding + tooltipPadding * 2); | 991 (textBBox.width + diagramPadding + tooltipPadding * 2); |
986 const tooltipX = Math.max( | 992 const tooltipX = Math.max( |
987 diagramPadding, | 993 diagramPadding, |
988 Math.min(coords.x - (textBBox.width + tooltipPadding * 2) / 2, xMax) | 994 Math.min(coords.x - (textBBox.width + tooltipPadding * 2) / 2, xMax) |
989 ); | 995 ); |
990 let tooltipY = coords.y - (textBBox.height + tooltipPadding * 2) - 10; | 996 let tooltipY = coords.y - (textBBox.height + tooltipPadding * 2) - 10; |
991 if (coords.y < textBBox.height + tooltipPadding * 2) { | 997 if (coords.y < textBBox.height + tooltipPadding * 2) { |
992 tooltipY = coords.y + 10; | 998 tooltipY = coords.y + 10; |
993 } | 999 } |
994 | 1000 |
995 this.diagram | 1001 diagram |
996 .select(".chart-tooltip") | 1002 .select(".chart-tooltip") |
997 .style("opacity", 1) | 1003 .style("opacity", 1) |
998 .attr("transform", `translate(${tooltipX}, ${tooltipY})`) | 1004 .attr("transform", `translate(${tooltipX}, ${tooltipY})`) |
999 .select("rect") | 1005 .select("rect") |
1000 .attr("width", textBBox.width + tooltipPadding * 2) | 1006 .attr("width", textBBox.width + tooltipPadding * 2) |