Skip to content

Instantly share code, notes, and snippets.

@jamesarosen
Created March 15, 2015 22:01
Show Gist options
  • Select an option

  • Save jamesarosen/8ea6285eca9609268d1b to your computer and use it in GitHub Desktop.

Select an option

Save jamesarosen/8ea6285eca9609268d1b to your computer and use it in GitHub Desktop.
An open letter to Chris Henn on the topic of SeriesComponents in Ember

Dear Chris Henn,

I loved your talk at EmberConf this year. Thanks so much for sharing that with us!

I had recently spiked on replacing our graphing library with some components and happened upon a very similar solution to what you described. What I didn't know is that there was an existing grammar for these concepts. I knew a few terms like axes and series, so those became nouns (or components) in my world. My template looks like this:

{{#viz-chart as |d|}}
  {{viz-xaxis dimensions=d}}
  {{viz-yaxis dimensions=d}}
  
  {{viz-line data=droppedRequests color='orange' dimensions=d}}
  
  {{!-- a viz-area plus a viz-line give a nice "edged area" effect --}}
  {{viz-area data=cacheHits color='grey' dimensions=d}}
  {{viz-line data=cacheHits color='grey' dimensions=d}}
{{/viz-chart}}

viz-chart yields a ChartDimensions object and the axes and series register themselves with it. This is as opposed to you giving the whole dataset to your equivalent of viz-chart and having the various series slice it up.

One reason I chose this approach is that when the user toggles the visibility of a series, that series will deregister itself from the ChartDimensions, which can then adjust the scaling functions.

What do you think of this approach? As I watched your talk, I felt my version didn't align to the existing visualizations grammar, which worries me a little.

@chnn
Copy link
Copy Markdown

chnn commented Mar 15, 2015

Thanks! I'm glad you enjoyed the talk.

I certainly don't want to make what I presented seem like the one way of graphic composition—there are many different good solutions. I think what you've got here is an interesting approach. I definitely like the idea of limiting the scope that the parent component yields to it's children.

Without knowing more about the ChartDimensions object though, I'm not sure I entirely understand it's role. Could you talk a little more about what that looks like? It sounds like you may be using as a way of passing around mutable state to child components. In general, allowing child components to mutate data supplied to them can lead to some confusing code. Instead, I usually prefer to have child components send actions to the parent component when they need to mutate state belonging to the parent. If you have heard the phrase “data down, actions up” recently, this is exactly what it refers to.

That may not be the right way of looking at this, depending on how you are using the ChartDimensions object. If that's where axes and series are registered, what state left belongs to the viz-chart component? Right now it looks like you have a line layer each for droppedRequests and for cacheHits. Does a viz-chart always have these two particular layers, or is the number of layers more variable (could there be additional datasets plotted on top)?

@jamesarosen
Copy link
Copy Markdown
Author

Here is dimensions.js:

// Keeps track of the dimensions of a chart, including
// width, height, margins, and how to scale data.
//
// An Array of Series objects, each of which should have
// minX, maxX, minY, maxY.
export default Ember.ArrayProxy.extend({
  outerHeight: null,
  outerWidth: null,

  marginTop: 30,
  marginRight: 20,
  marginBottom: 0,
  marginLeft: 50,

  innerHeight: function() {
    return this.get('outerHeight') - this.get('marginTop') - this.get('marginBottom');
  }.property('outerHeight', 'marginTop', 'marginBottom'),

  innerWidth: function() {
    return this.get('outerWidth') - this.get('marginLeft') - this.get('marginRight');
  }.property('outerWidth', 'marginLeft', 'marginRight'),

  scaleX: function() {
    return d3.time
    .scale()
    .domain([ this.get('xMin'), this.get('xMax') ])
    .range([ this.get('marginLeft'), this.get('outerWidth') - this.get('marginRight') ]);
  }.property('outerWidth', 'marginLeft', 'marginRight', 'xMin', 'xMax'),

  scaleY: function() {
    return d3.scale
      .linear()
      .domain([ this.get('yMin'), this.get('yMax') ])
      .range([ this.get('innerHeight'), 0 ]);
  }.property('innerHeight', 'yMin', 'yMax'),

  _initializeContent: function() {
    this.set('content', []);
  }.on('init'),

  _visibleSeries: Ember.computed.filterBy('content', 'isVisible'),

  _xMins:  Ember.computed.mapBy('_visibleSeries', 'xMin'),
  _xMaxes: Ember.computed.mapBy('_visibleSeries', 'xMax'),
  _yMaxes: Ember.computed.mapBy('_visibleSeries', 'yMax'),

  xMin: Ember.computed.min('_xMins'),
  xMax: Ember.computed.max('_xMaxes'),
  yMin: 0, // graphs whose Y-axis don't start at 0 are lying!
  yMax: Ember.computed.max('_yMaxes')
});

You can see that the only mutations that series are supposed to make are registering and deregistering themselves. They don't know anything about the range calculations.

I absolutely agree with the principle of encapsulation you suggest. If there's a way to make Dimensions more cohesive, I'm for it!

@chnn
Copy link
Copy Markdown

chnn commented Mar 16, 2015

Cool, looks like a nice way to share the scales and dimensions among the child components.

A slight tweak to the approach could be passing the dimensions object down to child components the same as now, but leave registering and deregistering as a responsibility of the parent component. Child components would notify the parent component of their intent to register through actions, maybe something like a 'yExtentChanged'. This would be more verbose (you would have to subscribe to the actions of each child component), but it would give you more power over the resulting scales, as the parent component would be entirely in control.

This may or may not be valuable for how you plan to use the viz-chart component. I've run into situations where there are user facing controls to change the min/max values used to compute scales, and in that case having a little more control is nice.

But looks good! The existing approach is easy to understand and think about and seems like it works well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment