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)