Titi
Titi

Reputation: 172

D3 multiple Axis interpolation

I am working on a project to plot a stream of csv/Json data (bar chart) where the order of arrival of the data is important. The Y axis is unique, but there are multiple X axes that correspond to different measures of the data. I am having trouble producing a nice graph that looks like this, given the following data:

x0,x1,x2,y,idx
-1,z,w2,10,0
0,z,w2,9,1
1,z,w2,8,2
-1,k,w2,11,3
0,k,5q,5,4
1,k,5q,8,5

enter image description here idx represent the order the data arrives in.

this is what I get

X=["idx","x0","x1","x2"];
Y=["y"];


   var margin = {
       top: 80,
       right: 180,
       bottom: 180,
       left: 180
     },
     width = 960 - margin.left - margin.right,
     height = 500 - margin.top - margin.bottom;

   var y = d3.scale.linear()
     .range([height, 0]);

   var xAxis = [],
     x = [];
   var x_uid = d3.scale.ordinal()
     .rangeRoundPoints([0, width]);
   for (var idx = 0; idx < X.length; idx++) {
     x[idx] = d3.scale.ordinal()
       .rangeRoundPoints([0, width]);

     xAxis[idx] = d3.svg.axis()
       .scale(x[idx])
       .orient("bottom");
   }
   var yAxis = d3.svg.axis()
     .scale(y)
     .orient("left");
    //            .ticks(8, "%");

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

var data = [{
x0:-1,
x1:z,
x2:w2,
y:10,
idx:0
},
{
x0:0,
x1:z,
x2:w2,
y:10,
idx:1
},
{
x0:1,
x1:z,
x2:w2,
y:10,
idx:2
},
{
x0:-1,
x1:j,
x2:w2,
y:10,
idx:3
},
{
x0:0,
x1:j,
x2:5q,
y:10,
idx:4
},
{
x0:1,
x1:j,
x2:5q,
y:10,
idx:5
}]

   if(data) {

     for (var idx = 0; idx < X.length; idx++) {
       x[idx].domain(data.map(function(d) {
         return d[X[idx]];
       }));
     }
     x_uid.domain(data.map(function(d) {
       return d.idx;
     }));
     y.domain([0, d3.max(data, function(d) {
       d.value = d[Y[0]];
       return d.value;
     })]);


     for (var idx = 0; idx < X.length; idx++)
       svg.append("g")
       .attr("class", "x axis")
       .attr("transform", "translate(0," + (height + idx * 25) + ")")
       .call(xAxis[idx]);

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

     svg.selectAll(".bar")
       .data(data)
       .enter().append("rect")
       .attr("class", "bar")
       .attr("x", function(d) {
         return x_uid(d.idx);
       })
       .attr("width", 1)
       .attr("y", function(d) {
         return y(d.value);
       })
       .attr("height", function(d) {
         return height - y(d.value);
       });
   });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.6/d3.min.js"></script>


<div id="chart"></div>

enter image description here

Offsetting the ticks' text is not an issue, but I am having problems with the interpolation due to the multiplicities of the values: e.g. width of w2 > width of 5q e.g. x0 axis should be -1 0 1 -1 0 1 but d3 interpolates as -1 0 1 I tried using rangeRoundBand instead of rangeRoundPoint but the issue is similar. I also tried playing around with tickValues but to no avail. I tried doing my own interpolation using linear scales instead of ordinal, but it becomes very messy very quickly because is forces me to manually calculate and adjust all the ticks' positions and texts while taking into account the d3.behavior zoom level etc...

function adjustTickPosition(selection, count, scale, translate, rotate) {
  
  //selection = axis
  //count = multiplicity of each tick
  //scale = d3.behavior.zoom scale
  //translate = d3.behavior.zoom translation
  //rotate = irrelevent here (additional styling)

        console.info( selection.selectAll("g.tick"))
        
        // cancel previous position
        //
        // /!\ For some reason there is always 100 ticks instead of the appropriate number
        //
        selection.selectAll("g.tick")
            .attr("transform", "translate(0,0)");

        // align tick marks
        selection.selectAll("g.tick line")
            .attr('transform', function (d, k) {
                if (k <= count.length - 1) {

                    var newPosition = scaleTranslate(count[k]);

                    if (newPosition > width || newPosition < 0) {
                        d3.select(this.parentNode).style("visibility", "hidden");
                    } else
                        d3.select(this.parentNode).style("visibility", "visible");


                    return 'translate(' + newPosition + ',0)';
                } else
                    return 'translate(0,0)';
            });


        // offset tick label compared to tick marks
        selection.selectAll("g.tick text")
            .attr('transform', function (d, k) {
                if (k <= count.length - 1) {
                    var pos, transform;
                    if (k > 0) pos = (count[k - 1] + count[k]) / 2;
                    else pos = count[k] / 2;

                    var newPosition = scaleTranslate(pos);

                    if (newPosition > width || newPosition < 0) {
                        d3.select(this.parentNode).style("visibility", "hidden");
                    } else
                        d3.select(this.parentNode).style("visibility", "visible");


                    var transform = 'translate(' + newPosition + ',0)';
                    if (rotate) transform += ' rotate(-65)';
                    return transform;
                } else
                    return 'translate(0,0)';
            });

        if (rotate) selection.selectAll("g.tick text").style("text-anchor", "end")
            .attr("dx", "-.8em")
            .attr("dy", ".15em");

        return selection;

        function scaleTranslate(v) {
            return v / count[count.length - 1] * width * scale + translate[0];
        }

    }

Could someone please show me how to properly use axes ticks for this kind of purpose?

Thank you in advance

Upvotes: 1

Views: 422

Answers (1)

Titi
Titi

Reputation: 172

I made my own class/object because d3 was apparently not meant for this kind of graph

function chartAxis(key, args) {

    //***************************
    //      PRIVATE
    //***************************


    var _direction = args ? (args.direction || "x") : "x";
    var _width = args ? (args.width || 500) : 500;
    var _alignTicks = args ? (args.alignTicks || false) : false;
    var _tickSize = args ? (args.tickSize || 0) : 0;
    var _numTicks = args ? (args.numTicks || 10) : 10;
    var _offset = args ? (args.offset || 25) : 25;
    var _zoom = args ? (args.zoom || {
        s: 1,
        t: 0
    }) : {
        s: 1,
        t: 0
    };
    var _totalLength;

    function consecutiveReduction(list, key) {

        var Bin = function (val, cnt) {
            return {
                value: val,
                count: cnt,
                cumulativeCount: 0,
                center: 0,
                position: 0
            };
        };

        var result = list.map(function (d) {

            return key ? d[key] : d;

        }).reduce(function (acc, d) {

            var currentBin = acc[acc.length - 1];

            if ((acc.length > 0) && d === currentBin.value) {
                //add to current bin
                currentBin.count++;
            } else {
                //create new bin
                acc.push(new Bin(d, 1));
            }

            return acc;
        }, []);

        result.forEach(accumulate);
        result.forEach(positionTick);

        return result;
    }

    function positionTick(d) {

        d.position = ApplyZoom(d.cumulativeCount);
        d.center = _alignTicks ? d.position : ApplyZoom(d.cumulativeCount - d.count / 2);

        function ApplyZoom(val) {
            var translate;
            if (_zoom.t.length > 1)
                translate = (_direction == "x") ? _zoom.t[0] : _zoom.t[1];
            else
                translate = _zoom.t;

            return val / _totalLength * _width * _zoom.s + translate;
        }
    }

    function accumulate(d, i, arr) {
        d.cumulativeCount = d.count;
        if (i > 0) d.cumulativeCount += arr[i - 1].cumulativeCount;
    }


    //***************************
    //      PUBLIC
    //***************************

    var xAxis = function (selection) {

        selection.each(function (data) {

            // calculate 
            _totalLength = data.length;
            var tickData = consecutiveReduction(data, key);

                        console.log(tickData.map(function (d) {
                            return d.count
                        }))
                        
                        console.table(data,key)


            //create parent axis with clip-path
            var axis = d3.select(this)
                .attr("id", key);
            axis.selectAll("#clipAxis-" + key).data([1]).enter()
                .append("clipPath")
                .attr("id", "clipAxis-" + key)
                .append("svg:rect")
                .attr("x", 0)
                .attr("y", _offset - _tickSize)
                .attr("width", _width)
                .attr("height", 25 + _tickSize);


            // Axis line and label
            var axisLine = axis.selectAll(".axisLine").data([1]).enter();

            axisLine.append("line").attr({
                x1: 0,
                y1: _offset,
                x2: _width,
                y2: _offset,
                class: "axisLine"
            });
            axisLine.append("text")
                .text(key)
                .attr({
                    x: _width + 10,
                    y: _offset
                }).style("text-anchor", "start");


            // tick on the axis
            var ticks = axis.selectAll("g.tick")
                .data(tickData);

            // ENTER
            var newticks = ticks.enter().append("g").attr("class", "tick");
            newticks.append("line");
            newticks.append("text");


            // UPDATE
            ticks.attr("clip-path", "url(#clipAxis-" + key + ")");

            ticks.select(".tick line")
                .attr("x1", function (d) {
                    return d.position
                })
                .attr("x2", function (d) {
                    return d.position
                })
                .attr("y1", function (d) {
                    return _offset - _tickSize
                })
                .attr("y2", function (d) {
                    return _offset + 5
                });

            ticks.select(".tick text")
                .text(function (d) {
                    return d.value;
                })
                .attr("x", function (d) {
                    return d.center;
                })
                .attr("y", function (d) {
                    return _offset + 10;
                })
                .style("text-anchor", "middle")
                .style("text-length", function (d) {
                    return (0.6 * 2 * (d.position - d.center)) + "px";
                });

            // EXIT
            ticks.exit().remove();

        })
    };


    var yAxis = function (selection) {

        selection.each(function (data) {

            // calculate 
            _totalLength = data.length;
            var tickData = d3.extent(data, function (d) {
                return d[key];
            });
            var tickRange = (tickData[1] - tickData[0]) / (_numTicks - 4 + 1); // -4 -> [0.85*min  min  ... max  1.15*max]





            console.log(tickData.map(function (d) {
                return d.count
            }))
            console.log(_tickSize)


            //create parent axis with clip-path
            var axis = axisLine = d3.select(this)
                .attr("id", key);
            axis.selectAll("#clipAxis-" + key).data([1]).enter()
                .append("clipPath")
                .attr("id", "clipAxis-" + key)
                .append("svg:rect")
                .attr("x", _offset)
                .attr("y", 0)
                .attr("width", _width)
                .attr("height", 25 + _tickSize);


            // Axis line and label
            axisLine = axis.selectAll(".axisLine").data([1]).enter();

            axisLine.append("line").attr({
                x1: _offset,
                y1: 0,
                x2: _offset,
                y2: _width,
                class: "axisLine"
            });
            axisLine.append("text")
                .text(key)
                .attr({
                    x: _offset,
                    y: -10
                }).style("text-anchor", "start");


            // tick on the axis
            var ticks = axis.selectAll("g.tick")
                .data(tickData);

            // ENTER
            var newticks = ticks.enter().append("g").attr("class", "tick");
            newticks.append("line");
            newticks.append("text");


            // UPDATE
            ticks.attr("clip-path", "url(#clipAxis-" + key + ")");

            ticks.select(".tick line")
                .attr("x1", function (d) {
                    return _offset - 5
                })
                .attr("x2", function (d) {
                    return _offset + _tickSize
                })
                .attr("y1", function (d) {
                    return d.position
                })
                .attr("y2", function (d) {
                    return d.position
                });

            ticks.select(".tick text")
                .text(function (d) {
                    return d.value;
                })
                .attr("x", function (d) {
                    return _offset + 10;
                })
                .attr("y", function (d) {
                    return d.center;
                })
                .style("text-anchor", "middle")
                .style("text-length", function (d) {
                    return (0.6 * 2 * (d.position - d.center)) + "px";
                });

            // EXIT
            ticks.exit().remove();

        }); // end select.foreach

    }; // end yAxis


    xAxis.BindToZoom = function (zoomObject) {
        _zoom = zoomObject;
        return xAxis;
    }
    yAxis.BindToZoom = function (zoomObject) {
        _zoom = zoomObject;
        return yAxis;
    }



    return (_direction == "x") ? xAxis : yAxis;

}

Usage:

   function chartAxis(key, args) {

     //***************************
     //      PRIVATE
     //***************************


     var _direction = args ? (args.direction || "x") : "x";
     var _width = args ? (args.width || 500) : 500;
     var _alignTicks = args ? (args.alignTicks || false) : false;
     var _tickSize = args ? (args.tickSize || 0) : 0;
     var _numTicks = args ? (args.numTicks || 10) : 10;
     var _offset = args ? (args.offset || 25) : 25;
     var _zoom = args ? (args.zoom || {
       s: 1,
       t: 0
     }) : {
       s: 1,
       t: 0
     };
     var _totalLength;

     function consecutiveReduction(list, key) {

       var Bin = function(val, cnt) {
         return {
           value: val,
           count: cnt,
           cumulativeCount: 0,
           center: 0,
           position: 0
         };
       };

       var result = list.map(function(d) {

         return key ? d[key] : d;

       }).reduce(function(acc, d) {

         var currentBin = acc[acc.length - 1];

         if ((acc.length > 0) && d === currentBin.value) {
           //add to current bin
           currentBin.count++;
         } else {
           //create new bin
           acc.push(new Bin(d, 1));
         }

         return acc;
       }, []);

       result.forEach(accumulate);
       result.forEach(positionTick);

       return result;
     }

     function positionTick(d) {

       d.position = ApplyZoom(d.cumulativeCount);
       d.center = _alignTicks ? d.position : ApplyZoom(d.cumulativeCount - d.count / 2);

       function ApplyZoom(val) {
         var translate;
         if (_zoom.t.length > 1)
           translate = (_direction == "x") ? _zoom.t[0] : _zoom.t[1];
         else
           translate = _zoom.t;

         return val / _totalLength * _width * _zoom.s + translate;
       }
     }

     function accumulate(d, i, arr) {
       d.cumulativeCount = d.count;
       if (i > 0) d.cumulativeCount += arr[i - 1].cumulativeCount;
     }


     //***************************
     //      PUBLIC
     //***************************

     var xAxis = function(selection) {

       selection.each(function(data) {

         // calculate 
         _totalLength = data.length;
         var tickData = consecutiveReduction(data, key);

         //create parent axis with clip-path
         var axis = d3.select(this)
           .attr("id", key);
         axis.selectAll("#clipAxis-" + key).data([1]).enter()
           .append("clipPath")
           .attr("id", "clipAxis-" + key)
           .append("svg:rect")
           .attr("x", 0)
           .attr("y", _offset - _tickSize)
           .attr("width", _width)
           .attr("height", 25 + _tickSize);


         // Axis line and label
         var axisLine = axis.selectAll(".axisLine").data([1]).enter();

         axisLine.append("line").attr({
           x1: 0,
           y1: _offset,
           x2: _width,
           y2: _offset,
           class: "axisLine"
         });
         axisLine.append("text")
           .text(key)
           .attr({
             x: _width + 10,
             y: _offset
           }).style("text-anchor", "start");


         // tick on the axis
         var ticks = axis.selectAll("g.tick")
           .data(tickData);

         // ENTER
         var newticks = ticks.enter().append("g").attr("class", "tick");
         newticks.append("line");
         newticks.append("text");


         // UPDATE
         ticks.attr("clip-path", "url(#clipAxis-" + key + ")");

         ticks.select(".tick line")
           .attr("x1", function(d) {
             return d.position
           })
           .attr("x2", function(d) {
             return d.position
           })
           .attr("y1", function(d) {
             return _offset - _tickSize
           })
           .attr("y2", function(d) {
             return _offset + 5
           });

         ticks.select(".tick text")
           .text(function(d) {
             return d.value;
           })
           .attr("x", function(d) {
             return d.center;
           })
           .attr("y", function(d) {
             return _offset + 10;
           })
           .style("text-anchor", "middle")
           .style("text-length", function(d) {
             return (0.6 * 2 * (d.position - d.center)) + "px";
           });

         // EXIT
         ticks.exit().remove();

       })
     };


     var yAxis = function(selection) {

       selection.each(function(data) {

         // calculate 
         _totalLength = data.length;
         var tickData = consecutiveReduction(data, key);


         //create parent axis with clip-path
         var axis = axisLine = d3.select(this)
           .attr("id", key);
         axis.selectAll("#clipAxis-" + key).data([1]).enter()
           .append("clipPath")
           .attr("id", "clipAxis-" + key)
           .append("svg:rect")
           .attr("x", _offset)
           .attr("y", 0)
           .attr("width", _width)
           .attr("height", 25 + _tickSize);


         // Axis line and label
         axisLine = axis.selectAll(".axisLine").data([1]).enter();

         axisLine.append("line").attr({
           x1: _offset,
           y1: 0,
           x2: _offset,
           y2: _width,
           class: "axisLine"
         });
         axisLine.append("text")
           .text(key)
           .attr({
             x: _offset,
             y: -10
           }).style("text-anchor", "start");


         // tick on the axis
         var ticks = axis.selectAll("g.tick")
           .data(tickData);

         // ENTER
         var newticks = ticks.enter().append("g").attr("class", "tick");
         newticks.append("line");
         newticks.append("text");


         // UPDATE
         ticks.attr("clip-path", "url(#clipAxis-" + key + ")");

         ticks.select(".tick line")
           .attr("x1", function(d) {
             return _offset - 5
           })
           .attr("x2", function(d) {
             return _offset + _tickSize
           })
           .attr("y1", function(d) {
             return d.position
           })
           .attr("y2", function(d) {
             return d.position
           });

         ticks.select(".tick text")
           .text(function(d) {
             return d.value;
           })
           .attr("x", function(d) {
             return _offset + 10;
           })
           .attr("y", function(d) {
             return d.center;
           })
           .style("text-anchor", "middle")
           .style("text-length", function(d) {
             return (0.6 * 2 * (d.position - d.center)) + "px";
           });

         // EXIT
         ticks.exit().remove();

       }); // end select.foreach

     }; // end yAxis


     xAxis.BindToZoom = function(zoomObject) {
       _zoom = zoomObject;
       return xAxis;
     }
     yAxis.BindToZoom = function(zoomObject) {
       _zoom = zoomObject;
       return yAxis;
     }



     return (_direction == "x") ? xAxis : yAxis;

   }


   var data = [{
     "a": 1,
     "b": 3,
     c: 1
   }, {
     "a": 1,
     "b": 3,
     c: 2
   }, {
     "a": 1,
     "b": 2,
     c: 3
   }, {
     "a": 1,
     "b": 3,
     c: 4
   }, {
     "a": 2,
     "b": 3,
     c: 5
   }, {
     "a": 3,
     "b": "a",
     c: 6
   }, {
     "a": 1,
     "b": "a",
     c: 7
   }];


   X = ["b", "a", "c"];


   var axesDOM = d3.select("svg")
     .selectAll(".axis")
     .data(X).enter()
     .append("g").attr("class", "axis");


   axesDOM.each(function(x, i) {
     d3.select(this).datum(data)
       .call(new chartAxis(x, {
         width: 200,
         offset: 25 + i * 25,
         direction: "x"
       }));
   });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg width="200px" height="200px"></svg>

Upvotes: 1

Related Questions