Combining Parent and Nested Data with d3.js

前端 未结 3 1874
臣服心动
臣服心动 2020-12-28 18:44

I have a data structure like this (assume that the data structure is non-negotiable):

data = {
    segments : [
        {x : 20, size : 10,          


        
相关标签:
3条回答
  • 2020-12-28 19:16

    I would try to flatten the colors before you actually start creating the elements. If changes to the data occur I would then update this flattened data structure and redraw. The flattened data needs to be stored somewhere to make real d3 transitions possible.

    Here is a longer example that worked for me. Yon can see it in action here.

    Here is the code:

    var data = {
        segments : [
            {x : 20, size : 10, colors : ['#ff0000','#00ff00']},
            {x : 40, size : 20, colors : ['#0000ff','#000000']}
        ]
    };
    
    function pos(d,i) { return d.x + (i * d.size); } // rect position
    function size(d,i) { return d.size; }            // rect size
    function f(d,i) { return d.color; }              // rect color
    
    function flatten(data) {
        // converts the .colors to a ._colors list
        data.segments.forEach( function(s,i) {
            var list = s._colors = s._colors || [];
            s.colors.forEach( function(c,j) {
                var obj = list[j] = list[j] || {}
                obj.color = c
                obj.x = s.x
                obj.size = s.size
            });
        });
    }
    
    function changeRect(chain) {
        return chain
        .transition()
        .attr('x',pos)
        .attr('y',pos)
        .attr('width',size)
        .attr('height',size)
        .attr('fill',f)
        .style('fill-opacity', 0.5)
    }
    
    vis = d3
    .select('#container')
    .append('svg')
    .attr('width',200)
    .attr('height',200);
    
    // add the top-level svg element and size it
    function update(){
    
        flatten(data);
    
        // add the nested svg elements
        var all = vis.selectAll('g')
        .data(data.segments)
    
        all.enter().append('g');
        all.exit().remove();
    
        // Add a rectangle for each color
        var rect = all.selectAll('rect')
        .data(function (d) { return d._colors; }, function(d){return d.color;})
    
        changeRect( rect.enter().append('rect') )
        changeRect( rect )
    
        rect.exit().remove()
    }
    
    function changeLater(time) {
        setTimeout(function(){
            var ds = data.segments
            ds[0].x    = 10 + Math.random() * 100;
            ds[0].size = 10 + Math.random() * 100;
            ds[1].x    = 10 + Math.random() * 100;
            ds[1].size = 10 + Math.random() * 100;
            if(time == 500)  ds[0].colors.push("orange")
            if(time == 1000) ds[1].colors.push("purple")
            if(time == 1500) ds[1].colors.push("yellow")
            update()
        }, time)
    }
    
    update()
    changeLater(500)
    changeLater(1000)
    changeLater(1500)
    

    Important here is the flatten function which does the data conversion and stores/reuses the result as _colors property in the parent data element. Another important line is;

    .data(function (d) { return d._colors; }, function(d){return d.color;})
    

    which specifies where to get the data (first parameter) AND what the unique id for each data element is (second parameter). This helps identifying existing colors for transitions, etc.

    0 讨论(0)
  • 2020-12-28 19:17

    You could do something like the following to restructure your data:

    newdata = data.segments.map(function(s) {
      return s.colors.map(function(d) {
        var o = this; // clone 'this' in some manner, for example:
        o = ["x", "size"].reduce(function(obj, k) { return(obj[k] = o[k], obj); }, {});
        return (o.color = d, o); 
      }, s);
    });
    

    This will transform your input data into:

    // newdata:
        [
          [
            {"size":10,"x":20,"color":"#ff0000"},
            {"size":10,"x":20,"color":"#00ff00"}],
          [
            {"size":20,"x":40,"color":"#0000ff"},
            {"size":20,"x":40,"color":"#000000"}
          ]
        ]
    

    which then can be used in the standard nested data selection pattern:

    var nested = vis.selectAll('g')
        .data(newdata)
      .enter().append('g');
    
    nested.selectAll('rect')
        .data(function(d) { return d; })
      .enter().append('rect')
        .attr('x',pos)
        .attr('y',pos)
        .attr('width',size)
        .attr('height',size)
        .attr('fill',f);
    

    BTW, if you'd like to be more d3-idiomatic, I would change the indentation style a bit for the chained methods. Mike proposed to use half indentation every time the selection changes. This helps to make it very clear what selection you are working on. For example in the last code; the variable nested refers to the enter() selection. See the 'selections' chapter in: http://bost.ocks.org/mike/d3/workshop/

    0 讨论(0)
  • 2020-12-28 19:27

    You can use closures

    var nested = vis
      .selectAll('g')
      .data(data.segments);
    
    
    nested.enter()
      .append('g')
      .each(function(segment, i) {
        var colors = d3.select(this)
          .selectAll('rect')
          .data(segment.colors);
    
        colors.enter()
          .append('rect')
          .attr('x', function(color, j) { return pos(segment, j); })
          // OR: .attr('x', function(color, j) { return segment.x + (j * segment.size); })
          .attr('width', function(color, j) { return size(segment); })
          .attr('fill', String);
      });
    
    0 讨论(0)
提交回复
热议问题