简体   繁体   中英

Interactivity with the d3 reusable pattern

In D3:
I'm trying to implement a chart using the reusable charts pattern which allows me to add events which are handled outside the actual chart. For instance I have a simple barchart where I can click on each individual bar. Now instead of just handling the click event inside the chart component I want to be able to handle it from where I create a chart instance. For example I want to edit the data which corresponds to the clicked bar element or I want to update the position of some other element depending on the position of the bar I clicked on. To keep the chart component as modular and independent as possible I don't want certain tasks to be done inside it.

I came up with a pretty straight forward solution but I feel like there must be a better solution to this. This way I have to account for every possible event that the outside world might be interested in. JSFiddle

function clickHandler(d, i) {
  // do something with the element/event/data
}

var svg = d3.select('#container')
  .append('svg');

var myChart = chart()
    .onClick(clickHandler);

svg.append('g')
  .datum(data)
  .call(chart);

function chart() {
  var onClick = function() {};

  function _chart(selection) {
    selection.each(function(d, i) {

      selection.selectAll('rect')
        .data(d)
        .enter()
        .append('rect')
        .attr('x', 0)
        .attr('y', function(d, i) {return i * 11;})
        .attr('width', function(d, i) {return d;})
        .attr('height', 10);
        .on('click', _onClick);

    });
  }

  function _onClick(d, i) {
    d3.select(this)
      .attr('fill', 'red');

    onClick(d, i);
  }

  _chart.onClick = function(_) {
    if(!arguments.length) return onClick;
    onClick = _;
    return _chart;
  }

  return _chart;
}

Is there any kind of standard way/best practice to handle this type of situation? Or is this just not really a common situation?

Any help appreciated!

What you're looking for is to create a separation between your core application code and code that can be reused. This is a good idea because it keeps your application's code small, making it easier for you to change it and move it forward. In a reusable chart you want to send a very generic events, eg clicked , while in your application you need the events to be very specific to your domain, eg addSugar .

There are two ingredients you need: d3.dispatch and d3.rebind . The former helps you create a clear internal API, while the latter helps you expose the dispatched events to the outside world.

Go here to find an example of this. What you want to look for is three things:

  1. Inside the reusable chart you create a dispatcher with events you want to publish to the outside world: var myDispatch = d3.dispatch('myclick', 'mydrag')
  2. Then in your reusable chart's event handlers you publish these events like so: myDispatch.myclick(argumentsyouwanttopass)
  3. As a last step you make these events available to the outside: return d3.rebind(_chart, myDispatch, "on");
  4. Then you can bind to these events on the outside: myChart.on('myclick', function(){})

So your example, rewritten could look like this:

function chart() {
  var dispatch = d3.dispatch('click');

  function _chart(selection) {
    selection.each(function(d, i) {

      selection.selectAll('rect')
        .data(d)
        .enter()
        .append('rect')
        .attr('x', 0)
        .attr('y', function(d, i) {return i * 11;})
        .attr('width', function(d, i) {return d;})
        .attr('height', 10);
        .on('click', dispatch.click);

    });
  }

  return d3.rebind(_chart, dispatch, 'on');
}

Additional note: to register multiple event handlers, you need to namespace them (like you have to for regular d3 events), eg chart.on('click.foo', handlerFoo) , chart.on('click.bar', handlerBar) etc.

One idea to make it more reusable and applicable to any possible event is to define a handler array. This is similar to d3 itself. Here is an updated fiddle

function chart() {
  var event_handlers = {}; //key value pairs

  function _chart(selection) {
    selection.each(function(d, i) {

      var rects = selection.selectAll('rect')
        .data(d)
        .enter()
        .append('rect')
        .attr('x', 0)
        .attr('y', function(d, i) {return i * 11;})
        .attr('width', function(d, i) {return d;})
        .attr('height', 10);

        for(var event in event_handlers){
          rects.on(event, event_handlers[event])
        }

    });
  }

  _chart.on = function(event, fun) {
    if(arguments.length==1) return event_handlers[event];
    event_handlers[event] = fun;
    return _chart;
  }

  return _chart;
}

You can use your chart the same way you use HTML elements.

chart.on("click", clickHandler);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM