lundi 1 octobre 2018

How to add updating labels in D3 Vertical Bar Chart in an Ember Application

I have a vertical bar chart in my Ember application and I am struggling to attach text labels to the top of the bars.

The chart is broken up into the following functions:

Drawing the static elements of the chart:

didInsertElement() {
    let svg = select(this.$('svg')[0]);
    this.set('svg', svg);
    let height = 325
    let width = 800


    let padding = {
      top: 10,
      bottom: 30,
      left: 40,
      right: 0
    };
    this.set('barsContainer', svg.append('g')
      .attr('class', 'bars')
      .attr('transform', `translate(${padding.left}, ${padding.top})`)
    );
    let barsHeight = height - padding.top - padding.bottom;

    this.set('barsHeight', barsHeight);
    let barsWidth = width - padding.left - padding.right;

    // Y scale & axes
    let yScale = scaleLinear().range([barsHeight, 0]);
    this.set('yScale', yScale);
    this.set('yAxis', axisLeft(yScale));
    this.set('yAxisContainer', svg.append('g')
      .attr('class', 'axis axis--y axisWhite')
      .attr('transform', `translate(${padding.left}, ${padding.top})`)
    );

    // X scale & axes
    let xScale = scaleBand().range([0, barsWidth]).paddingInner(0.15);
    this.set('xScale', xScale);
    this.set('xAxis', axisBottom(xScale));
    this.set('xAxisContainer', svg.append('g')
      .attr('class', 'axis axis--x axisWhite')
      .attr('transform', `translate(${padding.left}, ${padding.top + barsHeight})`)
    );


    // Color scale
    this.set('colorScale', scaleLinear().range(COLORS[this.get('color')]));

    this.renderChart();
    this.set('didRenderChart', true);
  },

This re-draws the chart when the model changes:

 didUpdateAttrs() {
    this.renderChart();
  },

This handles the drawing of the chart:

  renderChart() {
    let data = this.get('data');
    let counts = data.map(data => data.count);

    // Update the scales
    this.get('yScale').domain([0, Math.max(...counts)]);
    this.get('colorScale').domain([0, Math.max(...counts)]);
    this.get('xScale').domain(data.map(data => data.label));

    // Update the axes
    this.get('xAxis').scale(this.get('xScale'));
    this.get('xAxisContainer').call(this.get('xAxis')).selectAll('text').attr("y", 0)
    .attr("x", 9)
    .attr("dy", ".35em")
    .attr("transform", "rotate(40)")
    .style("text-anchor", "start");
    this.get('yAxis').scale(this.get('yScale'));
    this.get('yAxisContainer').call(this.get('yAxis'));


    let barsUpdate = this.get('barsContainer').selectAll('rect').data(data, data => data.label);
    // Enter
    let barsEnter = barsUpdate.enter()
    .append('rect')
    .attr('opacity', 0);
    let barsExit = barsUpdate.exit();
    let div = select('body')
    .append("div")
    .attr("class", "vert-tooltip");

    // Update
    let rafId;
    barsEnter
    .merge(barsUpdate)
    .transition()
    .attr('width', `${this.get('xScale').bandwidth()}px`)
    .attr('height', data => `${this.get('barsHeight') - this.get('yScale')(data.count)}px`)
    .attr('x', data => `${this.get('xScale')(data.label)}px`)
    .attr('y', data => `${this.get('yScale')(data.count)}px`)
    .attr('fill', data => this.get('colorScale')(data.count))
    .attr('opacity', data => {
      let selected = this.get('selectedLabel');
      return (selected && data.label !== selected) ? '0.5' : '1.0';
    })
    .on('start', (data, index) => {
      if (index === 0) {
        (function updateTether() {
          Tether.position()
          rafId = requestAnimationFrame(updateTether);
        })();
      }
    })
    .on('end interrupt', (data, index) => {
      if (index === 0) {
        cancelAnimationFrame(rafId);
      }
    });


    // Exit
    barsExit
      .transition()
      .attr('opacity', 0)
      .remove();

}

I have stripped some tooltip and click events to maintain clarity.

To add the labels I have tried to add the following in the renderChart() function:

barsEnter.selectAll("text")
        .data(data)
        .enter()
        .append("text")
        .text(function (d) { return d.count; })
        .attr("x", function (d) { return xScale(d.label) + xScale.bandwidth() / 2; })
        .attr("y", function (d) { return yScale(d.count) + 12; })
        .style("fill", "white");

with the above code I receive an error to say that xScale and yScale are not found because they are not within this functions scope. If I use:

.attr("x", function (d) { return this.get('xScale')(d.label) + this.get('xScale').bandwidth() / 2; })
.attr("y", function (d) { return this.get('yScale')(d.count) + 12; })

I generate 'this.get' is not a function errors and the context of 'this' becomes the an object with the value of (d).

If I add the X and Y scales as variables to this function like:

let xScale = this.get('xScale')
let yScale = this.get('ySCale')

...

        .attr("x", function (d) { return xScale(d.label) + xScale.bandwidth() / 2; })
        .attr("y", function (d) { return yScale(d.count) + 12; })

Then the x and y attrs are returned as undefined. Please let me know if I have missed anything out.




Aucun commentaire:

Enregistrer un commentaire