0

I am trying to have two (or more) similiar graph on one page inside an Ionic2 app. I use d3-ng2-service for wrapping the d3 types for Angular2. My problem is the following: When I try to place the two graphs in two different div elements each inside their respective custom element the drawing fails for both. When I did select the first div in the page the second graph overrides the first one, but it does get drawn.

Is there a clever way to place graphs more the the one graph? The examples always give the outer container a unique id, which is, what I try to do too:

import { Component, Input, OnInit, ElementRef } from '@angular/core';

import { D3Service, D3, Selection, ScaleLinear, ScaleTime, Axis, Line } from 'd3-ng2-service'; // <-- import the D3 Service, the type alias for the d3 variable and the Selection interface


@Component({
  selector: 'd3-test-app',
  templateUrl: 'd3-test-app.html',
  providers: [D3Service],
})

export class D3TestAppComponent {
  //Time is on x-axis, value is on y-axis
  @Input('timeSeries') timeSeries: Array<{isoDate: string | Date | number | {valueOf(): number}, value: number}>;
  @Input('ref') ref: string;
  /* the size input defines, how the component is drawn  */
  @Input('size') size: string;


  private d3: D3;

  private margin: {top: number, right: number, bottom: number, left: number};
  private width: number;
  private height: number;
  private d3ParentElement: Selection<any, any, any, any>; // <-- Use the Selection interface (very basic here for illustration only)



  constructor(element: ElementRef,
              d3Service: D3Service) { // <-- pass the D3 Service into the constructor
              this.d3 = d3Service.getD3(); // <-- obtain the d3 object from the D3 Service
              this.d3ParentElement = element.nativeElement;
  }

  ngOnInit() {
    let x: ScaleTime<number, number>;
    let y: ScaleLinear<number, number>;

    let minDate: number;
    let maxDate: number;

    let minValue: number = 0;
    let maxValue: number;

    // set the dimensions and margins of the graph
    switch (this.size) {
      case "large":
        this.margin = {top: 20, right: 20, bottom: 30, left: 50};
        this.width = 640 - this.margin.left - this.margin.right;
        this.height = 480 - this.margin.top - this.margin.bottom;
        break;
      case "medium":
          this.margin = {top: 20, right: 0, bottom: 20, left: 20};
          //golden ratio
          this.width = 420 - this.margin.left - this.margin.right;
          this.height = 260 - this.margin.top - this.margin.bottom;
          break;
      case "small":
        this.margin = {top: 2, right: 2, bottom: 3, left: 5};
        this.width = 120 - this.margin.left - this.margin.right;
        this.height = 80 - this.margin.top - this.margin.bottom;
        break;
      default:
        this.margin = {top: 20, right: 20, bottom: 30, left: 50};
        this.width = 640 - this.margin.left - this.margin.right;
        this.height = 480 - this.margin.top - this.margin.bottom;
    }


    // ...
    if (this.d3ParentElement !== null) {
      let d3 = this.d3; // <-- for convenience use a block scope variable

      //THIS FAILS...   
      let selector: string = '#' + this.ref + ' .graphContainer';
      console.log(selector);

      let svg = d3.select( selector).append("svg")
        .attr("width", this.width + this.margin.left + this.margin.right)
        .attr("height", this.height + this.margin.top + this.margin.bottom)
        .append("g")
        .attr("transform",
          "translate(" + this.margin.left + "," + this.margin.top + ")");


          this.timeSeries.forEach((d) => {

            d.isoDate = +d3.isoParse(d.isoDate as string);
            d.value = +d.value;
            if (minDate == null || minDate >= d.isoDate) {
              minDate = d.isoDate as number;
            }
            if (maxDate == null || maxDate <= d.isoDate) {
              maxDate = d.isoDate as number;
            }
            // if (minValue == null || minValue >= d.value) {
            //   minValue = d.value as number;
            // }
            if (maxValue == null || maxValue <= d.value) {
              maxValue = d.value as number;
            }
          });

      // TODO magic numbers to real min max
      x = d3.scaleTime().domain([minDate, maxDate]).range([0,this.width]);
      y = d3.scaleLinear().domain([0, maxValue]).range([this.height, 0]);

      let xAxis: Axis<number | Date | {valueOf() : number;}> = d3.axisBottom(x);
      let yAxis: Axis<number | {valueOf(): number;}> = d3.axisLeft(y);

      let valueLine: Line<{isoDate: number; value: number}> = d3.line<{ isoDate: number; value: number }>()
       .x(function (d) { return x(d.isoDate)})
       .y(function (d) { return y(d.value)});


      // Add the valueline path.
      svg.append("path")
        .data([this.timeSeries as {isoDate: number, value: number}[]])
        .attr("class", "line")
        .attr("d", valueLine);

      svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + this.height + ")")
        .call(xAxis)

      svg.append("g")
        .attr("class", "y axis")
        .call(yAxis)

    }
  }

  myParser() : (string) => Date {
    return this.d3.utcParse("%Y-%m-%dT%H:%M:%S.%LZ");
  }
}

The HTML:

<div class='graphContainer'>
</div>

The HTML file where the custom component is used:

<ion-header>
  <ion-navbar #dashboardNav>
    <ion-title>Dashboard</ion-title>
    <button ion-button menuToggle="favMenu" right>
        <ion-icon name="menu"></ion-icon>
    </button>
  </ion-navbar>
</ion-header>

<ion-content>
  <ion-item *ngFor="let entry of dashboard">
    {{ entry.name }}
    <d3-test-app [id]='entry.name' [timeSeries]='entry.timeSeries' [ref]='entry.name' size='medium'></d3-test-app>
  </ion-item>
</ion-content>
GGaluba
  • 23
  • 5

1 Answers1

0

Hard to debug without seeing a stack trace but it looks like this is failing because of how you select the element. I will base my answer on that assumption.

Querying by ID is handy when you have to look inside the DOM for a specific element and when you are sure there is only one element with that ID. Since you are inside an Angular element you already have the reference you need, it's the element itself, no need to dynamically create ID references.

I am not an expert in ng2 at all, but take a look a at how to select the raw element in Angular and choose the best approach for the framework. Say you go for something like the example shown on this answer:

  constructor(public element: ElementRef) {
    this.element.nativeElement // <- your direct element reference 
  }

NB - looks like there are various way of achieving this... not sure this is the best/correct one, but the goal is to get the ref anyway

Then simply select it via the D3 dsl like you are already doing by passing that raw reference

 // At this point this.element.nativeElement
 // should contain the raw element reference
    if (this.d3ParentElement !== null) {
      const { d3, element } = this; // <-- for convenience use block scope variables

      const svg = d3.select(element.nativeElement).append("svg")...
Community
  • 1
  • 1
Aurelio
  • 24,702
  • 9
  • 60
  • 63