Making a D3 chart an Ember Component using InsertElement and didUpdateAttrs


#1

Hello. I have a D3 chart that re-renders data when a model is refreshed an although I have the chart working I am having a lot of trouble adding it as an Ember component. Here is what I have so far:

(note that the appropriate d3 modules are imported before the component object is extended)

export default Component.extend({
data: [],

  didInsertElement() {
    this.buildChart()
  },

  didUpdateAttrs() {
    this.buildChart()
  },

  buildChart() {
    var data = this.get('data')

    var margin = {top: 35, right: 145, bottom: 35, left: 45},
        width = 650 - margin.left - margin.right,
        height = 450 - margin.top - margin.bottom;

    var svg = select("#chart")
    	.attr("width", width + margin.left + margin.right)
    	.attr("height", height + margin.top + margin.bottom)
    .append("g")
    	.attr("transform","translate(" + margin.left + "," + margin.top + ")");

    var x = scaleBand()
        .rangeRound([0, width])
        .padding(0.1);

    var y = scaleLinear()
        .rangeRound([height, 0]);

    var z = scaleOrdinal()
      .range(["steelblue","darkorange", "red"]);

    svg.append("g")
      .attr("class","x-axis");

    svg.append("g")
      .attr("class", "y-axis");

    var input = selectAll(".opt").property("value");

    selectAll(".opt").on("change", function() {
    	update(data, this.value)
    })

    update(data);

    function update(data) {

    	var keys = ["count1", "count2", "count3"];

    	var series = stack()
    		.keys(keys)
    		.offset(stackOffsetDiverging)
    		(data);

    	x.domain(data.map(d => d.label));

    	y.domain([
    		min(series, stackMin),
    		max(series, stackMax)
    	]).nice();

        var barGroups = svg.selectAll("g.layer")
        	.data(series);

        barGroups.exit().remove();

        barGroups.enter().insert("g", ".x-axis")
          .classed('layer', true);

        svg.selectAll("g.layer")
        	.transition().duration(750)
        	.attr("fill", d => z(d.key));

        var bars = svg.selectAll("g.layer").selectAll("rect")
          .data(function(d) { return d; });

    	bars.exit().remove()
        bars = bars
        	.enter()
        .append("rect")
        	.attr("width", x.bandwidth())
        	.attr("x", d => x(d.data.label))
          .merge(bars)

        bars.transition().duration(750)
        	.attr("y", d => y(d[1]))
        	.attr("height", d => Math.abs(y(d[0])) - y(d[1]));

    	svg.selectAll(".x-axis").transition().duration(750)
    		.attr("transform", "translate(0," + y(0) + ")")
    		.call(axisBottom(x));

    	svg.selectAll(".y-axis").transition().duration(750)
    		.call(axisLeft(y));

    	function stackMin(serie) {
    		return min(serie, function(d) { return d[0]; });
    	}

    	function stackMax(serie) {
    	  return max(serie, function(d) { return d[1]; });
    	}

    }
  }
});

In the first instance the chart renders correctly. The problem occurs when the model refreshes.

Rather than simply transition the bars, the whole chart is re-rendered and is placed ‘on top’ of the existing chart.

I know that this is because when the attrs update I am redrawing the whole chart but I am struggling to divide my code between the insertElement hook and the buildChart function. I tried to move all of the standard elements (such as svg, x, y etc) to insertElement and use setters/getters to reference them in buildChart but I constantly run into ‘is undefined’ errors.

Does anyone have any experience in similar problems?


#2

I would recommend ember-d3, as they have done a lot of the wrapping for you. Also, I use d3 for the scaling and formatting and whatnot and then do a lot of the actual SVG drawing in component .hbs. That way there’s only Ember drawing things. Also, you get control over the scope of redraws, rather than always redrawing the whole thing. Very fast.

I’ll return in an hour or two to making what you’re trying to do work the way you’re trying to do it, because in the past I’ve done that, too.


#3

Hi, thanks for your comments. I am actually using ember-d3 but only as the import mechanism for the d3 functions for example:

import {
  scaleBand,
  scaleLinear,
  scaleOrdinal,
} from 'd3-scale'

In my current component.hbs I have a simple SVG open/close tag with the id. I do have a requirement to have this scale to the div that the component is placed in and was planning to tackle that after clarification on the above.

I’m looking forward to your comments later, thank you in advance for any effort you put into this on my behalf.


#4

We use d3 in a couple places, and at least one of them is a very complex bar chart with line overlays and icons that can have hundreds of data points. What I did is to split the update functions for the various chart elements out (so I have a setupBars function, drawLine function, etc.) and as the data changes, an Ember observer calls the appropriate functions that use d3 to update the chart. Our application only graphs the bar elements that are in the visible viewport, and has a scrubber that updates what parts of the chart is visible, so updates are made quite often (those are d3 events calling the same functions called for the data updates from Ember).

Hope that helps, feel free to reach out for further clarification and I’ll share as much as I’m able.


#5

For one thing, you are doing select("#chart") during didUpdateAttrs(), a context where you aren’t guaranteed to have the chart or its surrounding tags in your DOM at the moment, along the lines of the following simpler case:

I’m guessing this is at the root of your problem. What happens if you do everything during the didInsertElement()? If you make your source data an input to the component, replacing one set of source data with another will trigger a redraw and re-execution of didInsertElement().

I had hoped to have an example to show you what I mean, but whatever I had wasn’t in one of my local experiments but buried in the distant past of source control for the project in question, so it would be difficult to pull up. I believe my approach for this ultimately was to do all the meaningful graph manipulations in the didInsertElement() method, and to make the source data an input to the component, so the component would be redrawn (at the right point in the cycle) when it changed.

@localpcguy used observers to trigger methods rather than computed values. As a general rule, I have found very few situations where I needed to use observers in this way. They are devilishly tricky to get right with the Ember run loop. However, when you are “impedance-matching” Ember with some other drawing system (like jQuery), observers that call the right run loop methods can make sure the right things get responded to at the right time. If you use them, I would recommend using them only within the single component that encapsulates the “foreign” component in question, and use them in a way where every interaction with anything outside the component - the DOM, services, and other components, is invoked in the right lifecycle context. (I have an example of that somewhere, too, from when we were using jqPlot for the same tasks. jqPlot using canvases was much slower than SVG and D3 so we abandoned it after several years.)


#6

I do agree regarding observers, and we try to eliminate them from our code wherever possible. In the case I mentioned, it was a case of there not really being a computed return value to consume in the UI since all the d3 updates were triggered in code (and I didn’t want to just have a computed that called other functions that I had to “kickstart” in the UI or init).


#7

Yeah, there sure are situations - that’s why there are guidelines for what approaches hurt the least rather than rules about what not to do. :slight_smile:

I’m trying to imagine something that would cause a redraw of a chart that didn’t originate in either the input data or the settings of controls of some sort that modify the parameters of the visualization. We supply both kinds of values as input parameters, keeping the chart itself as general as we can and rolling in overlays in other components inside the {{#mychart as |geometry| }}...in here...{{/mychart}} brackets. (Underlays can be a trickier matter.)

However, like I say, there are situations…

Oh, and while we mostly do the SVG for the chart structure ourselves these days based on data derived from using d3 scaling, format, etc., we do let d3 draw the plot lines and such. We do so much of the work ourselves because, if we’re going to be doing a bunch of SVG on our own anyway, I’ve found it less frustrating to stick to one “language” for drawing than figuring out how we have to tweak an API to generate precisely what I’m seeing in my head anyway. The actual plot lines are generally the least fussy part of a graph. It’s getting the axes, labels, divisions, etc. exactly the way you want them to appear that take all the cut-and-try time.


#8

So in the current implementation the chart component is being fed an array of objects as part of the component call. The array of objects is derived from iterating on an Ember Model in a computed property.

The model is updated by the user manipulating a date selector and on selection of a new date, the model (and subsequently, the computed property) is reloaded with the data relevant to the selected dates.

The data is refreshing and passing through correctly because the ‘new’ bars are drawn on top of the existing bars (which are not being removed correctly). The bars don’t transition nicely though, they are simply drawn in in one lump rather than each bar moving to its new height (or new bar being added to the graph for an expanded date range).

I’ve added the full code above to the insertElement hook but this stops the chart from drawing any new data at all. The data is still being refreshed however because it is also used elsewhere on the application page.

There seems to be very few resources on working with d3 in ember which is a bit of a surprise! I wish I could put up a fiddle or tweddle but I’ve never been able to get them working with d3.


#9

@Lupestro - we had performance problems using D3 APIs and letting Ember draw the chart, so had to go all in on D3 for the DOM manipulations which is why we ended up where we did.

@sorvah - I hear what you’re saying about the documentation, maybe I can try to put together a blog post or two describing my experiences anyways (and I encourage others to do the same). What it sounds like needs to happen for your situation is to use D3 to select the bars, and pass in a new data object via the .enter function (likely .exit also). Assuming you want to use D3 to do all the DOM stuff, and aren’t using Ember to update the SVG itself.


#10

@localpcguy - interesting, so maybe I’ll be where you are in a few months to a year as we do more complex charts. Good to know what problems and solutions will lie ahead of me. And please do write up what you’ve learned - for all of us doing this stuff. :slight_smile:


#11

For the record - it wasn’t that Ember was slow - it was the sheer number of DOM nodes it was dealing with. It’s possible we could have applied the final solution (limiting what was painting on the screen to the visible chart area only) to using Ember to do the DOM, but we’d already switched everything to d3 at that point and time was too short to switch back. That said, D3 is seemed quite efficient at what it does and provided some nice to haves in terms of animations for adding/removing/scrubbing. And for more complex items it can be nice to just treat it as kind of a black box as far as Ember is concerned.