node.jssocket.iosocket.io-redis

Making realtime datatable updates


I built an app which consumes data from a redis channel(sellers) with socketio and push the data in realtime to the frontend. The dataset could contain up to a thousand rows so I'm thinking about using a datatable to represent the data in a clean way. The table elements will be updated regularly, but there will be no rows to add/remove, only updates.

The problem I'm facing is that I don't know which would be the proper way to implement it due to my inexperience in the visualization ecosystem. I've been toying with d3js but I think It'll be too difficult to have something ready quickly and also tried using the datatables js library but I failed to see how to make the datatable realtime.

This is the code excerpt from the front end:

socket.on('sellers', function(msg){
  var seller = $.parseJSON(msg);
  var sales = [];
  var visits = [];
  var conversion = [];
  var items = seller['items'];

  var data = [];
  for(item in items) {
    var item_data = items[item];
    //data.push(item_data)
    data.push([item_data['title'], item_data['today_visits'], item_data['sold_today'], item_data['conversion-rate']]);
  }

  //oTable.dataTable(data);

  $(".chart").html("");
  drawBar(data);
});

Solution

  • Using d3 to solve your problem is simple and elegant. I took a little time this morning to create a fiddle that you can adapt to your own needs:

    http://jsfiddle.net/CelloG/47nxxhfu/

    To use d3, you need to understand how it works with joining the data to the html elements. Check out http://bost.ocks.org/mike/join/ for a brief description by the author.

    The code in the fiddle is:

    var table = d3.select('#data')
    
    // set up the table header
    table.append('thead')
        .append('tr')
        .selectAll('th')
            .data(['Title', 'Visits', 'Sold', 'Conversion Rate'])
        .enter()
            .append('th')
            .text(function (d) { return d })
    
    table.append('tbody')
    
    // set up the data
    // note that both the creation of the table AND the update is
    // handled by the same code.  The code must be run on each time
    // the data is changed.
    
    function setupData(data) {
        // first, select the table and join the data to its rows
        // just in case we have unsorted data, use the item's title
        // as a key for mapping data on update
        var rows = d3.select('tbody')
            .selectAll('tr')
            .data(data, function(d) { return d.title })
    
        // if you do end up having variable-length data,
        // uncomment this line to remove the old ones.
        // rows.exit().remove()
    
        // For new data, we create rows of <tr> containing
        // a <td> for each item.
        // d3.map().values() converts an object into an array of
        // its values
        var entertd = rows.enter()
            .append('tr')
                .selectAll('td')
                    .data(function(d) { return d3.map(d).values() })
                .enter()
                    .append('td')
    
        entertd.append('div')
        entertd.append('span')
    
        // now that all the placeholder tr/td have been created
        // and mapped to their data, we populate the <td> with the data.
    
        // First, we split off the individual data for each td.
        // d3.map().entries() returns each key: value as an object
        // { key: "key", value: value}
        // to get a different color for each column, we set a
        // class using the attr() function.
    
        // then, we add a div with a fixed height and width
        // proportional to the relative size of the value compared
        // to all values in the input set.
        // This is accomplished with a linear scale (d3.scale.linear)
        // that maps the extremes of values to the width of the td,
        // which is 100px
    
        // finally, we display the value.  For the title entry, the div
        // is 0px wide
        var td = rows.selectAll('td')
            .data(function(d) { return d3.map(d).entries() })
            .attr('class', function (d) { return d.key })
    
        // the simple addition of the transition() makes the
        // bars update smoothly when the data changes
        td.select('div')
            .transition()
            .duration(800)
            .style('width', function(d) {
                switch (d.key) {
                    case 'conversion_rate' :
                        // percentage scale is static
                        scale = d3.scale.linear()
                            .domain([0, 1])
                            .range([0, 100])
                        break;
                    case 'today_visits': 
                    case 'sold_today' :
                        scale = d3.scale.linear()
                        .domain(d3.extent(data, function(d1) { return d1[d.key] }))
                        .range([0, 100])
                        break;
                    default:
                        return '0px'
                }
                return scale(d.value) + 'px'
            })
        td.select('span')
            .text(function(d) {
                if (d.key == 'conversion_rate') {
                    return Math.round(100*d.value) + '%'
                }
                return d.value
            })
    }
    
    setupData(randomizeData())
    
    d3.select('#update')
        .on('click', function() {
            setupData(randomizeData())
        })
    
    // dummy randomized data: use this function for the socketio data
    // instead
    //
    // socket.on('sellers', function(msg){
    //  setupData(JSON.parse(msg).items)
    // })
    function randomizeData() {
        var ret = []
        for (var i = 0; i < 1000; i++) {
            ret.push({
                title: "Item " + i,
                today_visits: Math.round(Math.random() * 300),
                sold_today: Math.round(Math.random() * 200),
                conversion_rate: Math.random()
            })
        }
        return ret
    }