Placing a circle over a sparkline when the user changes the ordering of the data

喜欢而已 提交于 2019-12-12 08:19:51

问题


I created this question a few days ago regarding the ordering of sparklines when the user chooses to change the order of the data. The answer has solved the problem but remains the correct positioning of the red circles that highlight where the user place the mouse.

This is the code: PLUNKER.

I thought about how to change the code to reposition the sparkline circles when the data sort changes. I didn't understand where and how to change the code. Below I try to explain my reasoning based on the points of the code concerning the sparklines.

(1) These two lines of code define the domain and the range for sparklines. It seems to me that they should not be changed when the ordering of the data changes.

// domain and range for sparkline lines
var xSpark = d3.scaleLinear().domain([0, numYears-1]).range([0, sparkLength]); 
var ySpark = d3.scaleLinear().domain([minYvalue, maxYvalue]).range([itemSize-2, 2]);

(2) This piece of code selects the element #data-svg-i (where i is the sparkline line), it append a circle that positions it in cx, cy which depend on xSpark and ySpark. If the one mentioned in point 1 is true (ie that xSpark and ySpark are "fixed" values), then even this piece of code does not have to be changed when the data order changes.

var cells = svg.selectAll('.cell')
    .data(data)
    .enter()
    .append('g')
    .append('rect')
    .on('mouseover', function(d, i) { // on mouseover rect
        // get row, column and value of this rect
        var idr = d3.select(this).attr('data-r'); // row
        var idc = d3.select(this).attr('data-c'); // column
        var value = d3.select(this).attr('data-value');
        // highlight this rect
        d3.select(this).style('stroke', 'red');
        // add red dot to sparkline
        d3.select('#data-svg-' + idr)
            .append('circle')
            .attr('r', 3) // radius
            .style('stroke', 'red')
            .style('fill', 'red')
            .attr('cx', xSpark(idc))
            .attr('cy', ySpark(value));
    })

(3) This piece of code also does not need to be changed when the data order changes.

line = d3.line()
    .x(function(d, i) { 
        return xSpark(i); 
    })
    .y(function(d) { 
        return ySpark(d); 
    })
    .defined(function(d) { // for missing (0) data
        return d !== 0;
    });

(4) data should be updated but yet it is not. Before sorting, data contains the data in the correct order in which they are displayed, after the ordering, data is not changed yet it should, no?

pos I don't think it should be changed nor cx and cy because they depend on xSpark/ySpark and pos.

var sparkSvg = d3.select('#sparkline')
    .append('svg')
    .on('mousemove', function() { // on mousemove svg sparkline canvas
        var mouse = d3.mouse(this); // mouse position [x, y]
        var r = d3.select(this).attr('data-r'); // number of line
        var data = d3.select(this).select('path').data(); // array containing all the data values of that line

        var element = document.getElementById('data-path-' + r); // get the right path
        var pos = get_data_on_line(data, mouse);
        d3.selectAll('.data-svg').selectAll('circle').remove(); // remove old circles

        // add new circle
        d3.select('#data-svg-' + r)
            .append('circle')
            .attr('r', 3)
            .style('stroke', 'red')
            .attr('fill', 'red')
            .attr('cx', xSpark(pos[1]))
            .attr('cy', ySpark(pos[0]));
    })

Conclusion

In short, I don't understand what point of the code should be modified and how. Would anyone know how to help me?

EDIT 1

Mark's answer solves the problem of when the user hovers over the filemap rectangle.

But when the user hovers over the sparklines, the red circles are not positioned in the correct position. I hope this image can clarify what the problem is.

I hovered over the sparkline related to Italy, and the circle is displayed not on the line but above. Furthermore, the data seems to be mixed up.

EDIT 2

I test the code here (Mark's updated code). I modify the code adding some console.log(d) when the user click on a row and column label:

  var rowLabels = svg.append('g')
    .attr('class', 'rowLabels')
    .selectAll('.rowLabels')
    .data(regionsName)
    .enter().append('text')
    .text(function(d) {
      return d;
    })
    .attr('x', 0)
    .attr('y', function(d, i) {
      return i * cellSize;
    })
    .attr('transform', function(d, i) {
      return 'translate(-3, 11)';
    })
    .attr('class', 'rowLabel mono')
    .attr('id', function(d) {
      return 'rowLabel_' + regionsName.indexOf(d);
    })
    .attr('label-r', function(d) {
      return regionsName.indexOf(d);
    })
    .attr('font-weight', 'normal')
    .style('text-anchor', 'end')
    .on('click', function(d, i) {
      console.log(d); // <-- ADDDED
      rowSortOrder = !rowSortOrder;
      sortByValues('r', i, rowSortOrder);
    });

  // year labels
  var colLabels = svg.append('g')
    .attr('class', 'colLabels')
    .selectAll('.colLabels')
    .data(yearsName)
    .enter().append('text')
    .text(function(d) {
      return d;
    })
    .attr('transform', function(d, i) {
      return 'translate(' + (i * cellSize) + ', 2) rotate(-65)';
    })
    .attr('class', 'colLabel mono')
    .attr('id', function(d) {
      return 'colLabel_' + yearsName.indexOf(d);
    })
    .attr('label-c', function(d) {
      return yearsName.indexOf(d);
    })
    .attr('font-weight', 'normal')
    .style('text-anchor', 'left')
    .attr('dx', '.8em')
    .attr('dy', '.5em')
    .on('click', function(d, i) {
      console.log(d); // <-- ADDDED
      colSortOrder = !colSortOrder;
      sortByValues('c', i, colSortOrder);
    });

Example of error: when the user click on Germany, then on 2000, then on 2002, then on 2005, then on 2003 and finally on Italy, this is the result:

As you can see sparklines and heatmap are not correct because sparkline associated with Italy has missing data that, in reality it doens't have.

EDIT 3

I created this gif showing what the problem is:

Initially, Germany has two unknown values (relating to 2000 and 2001). The corresponding sparkline is correct.

When you click on Germany, the data is sorted in descending order and the sparkline is still correct.

Then click on the year 2002 and the data are sorted in descending order, the rows are then repositioned in the correct order. And the sparklines are correct.

Then click on 2005, the data is sorted and the sparklines are correct.

Then click on 2003 and everything is correct again.

Finally click on Italy and the graph is no longer correct. Germany has two missing data, but from the corresponding sparkline, this is not highlighted. Instead the two missing data are on the sparkline of Italy.


回答1:


This is a novel and neat concept for a visualization. If I understand correctly, we need to get complete alignment between rectangles and sparklines, mouseovers and tooltips, regardless of sorting. For my answer I'll be using the plunkr you link to first (not Mark's version). If I understand the question correctly, then are five issues that need to be addressed:

  1. Sorting vertically erases the datum used in the mouseover

First, in sorting your svg sparklines vertically you are assinging new datums - these are overwriting the child datums of the path, which is why once sorted vertically the mouseover doesn't work when hovering over the sparklines (the circle hugs the top of each svg). See this simplified example for example:

var div = d3.select("body").append("div");
div.append("p").datum("hello").text(function(d) { return d; });
div.datum("new datum").select("p").text(function(d) { return d; });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

This breaks the portion of the code where you derive information from the path's datum with get_data_on_line(), instead of an array of all the points you now have a index of the path.

This is a bit of a challenge, this could be alleviated by not using a forEach loop to append the svg's but instead using a proper enter/update/exit cylce for this and specifying more data than just an array for the line itself (specifically, if the index was included in the datum this would be much easier).

Having said that, the easiest way to do this without modifying the code much will be to replace this:

var sortedSVG = d3.selectAll(".data-svg")
   .datum(function(d){
      return sorted.indexOf(+this.dataset.r)})
   .sort(function(a,b){
     return d3.ascending(a,b)
});

With:

  sortedVertical.forEach(function(d) {
    d3.select("#data-svg-"+d).raise();
  })

This will order the SVG's in the desired order and does not alter the datum of the SVG's or the paths.

I've split up the vertical and horizontal orders into two separate variables, more on that in number two

  1. The mouseover for each rect uses a fixed column/x value

When setting the coordinates for the circle on on the sparkline in the rectangle mouseover function you use a fixed value:

 .attr('cx', xSpark(idc))

idc doesn't change, nor does the scale. So each rectangle will continue to highlight the same portion of the sparkgraph, regardless of reordering of the rectangles. To fix this we need to preserve the horizontal ordering of the indexes. I used the sorted variable, but gave it a scope so that both mouseover functions and sorting functions could access it:

var sorted = [0,1,2,3,4,5]; // base ordering

And then I could set the rectangle mouseover to get the correct column on the sparklines:

 .attr('cx', xSpark(sorted.indexOf(+idc))) 

Make sure you remove the definition of sorted in the sorting function. Consequently, this also requires using two variables, one for horizontal and another for vertical sorting orders, as we need the horizontal sorting order on demand for mouse overs, not just in the sorting function

  1. When sorting vertically you are moving nodes, but don't account for it

By sorting nodes vertically you break the ordering needed for the paths when sorting horizontally:

d3.selectAll('.sparkline-path')

This will select the element in the order they appear in the DOM - but as a user may have modified this order through sorting, the indexes no longer refer to the proper data when selecting rectangles when using the index of the above selection with:

d3.selectAll('.sparkline-path')
 .attr('d', function(d, k) {
    var arr = [];     
    var sel = d3.selectAll('rect[data-r=\'' + (index) + '\']') 
 ....

Instead, let's modify that to:

.attr('d', function(d, k) {
    var index = d3.select(this).attr("id").split("-")[2]; // get the original index    
    var arr = [];       
    var sel = d3.selectAll('rect[data-r=\'' + index + '\']')  

The index comes from the id property of the SVG, which lets us use the right data - if the index comes from the order of the SVG's after sorting we'll run into problems.

  1. The datum of the sparklines' paths remains the same even though the path changes.

The datum remains unchanged on updating the paths, but the datum is used in calculating what squares to highlight and how to place the circle. We need to update the datum of the spark line each time we modify it:

d3.select(this).datum(result);
  1. Need to update tooltip position based on sorted data

For the tooltip to align properly we need to consider the reordering we have done:

Instead of:

if(rect_r == r && rect_c == pos[1] ) {

Make sure you compare the relative position of each after considering ordering:

if(rect_r == r && sorted.indexOf(+rect_c) == pos[1] ) {  

Altogether, that changes code in about 11 places by my count (including several changes that just propagate the changes outlined above). I've created an updated plunkr: http://plnkr.co/edit/Bgbp7swTs98CMDFkSss3?p=preview

Each change is clearly marked with the old code commented out and the new code placed below.


I'll just re-emphasize though, the use of an enter cycle for entering the paths and SVGs (or alternatively, gs) with more descriptive data bound would allow for easier sorting, updating, and most likely easier de-bugging. The enter cycle seems to work well for the rectangles, but I'm unsure why the approaches for the paths differs. This answer comes to mind



来源:https://stackoverflow.com/questions/49982686/placing-a-circle-over-a-sparkline-when-the-user-changes-the-ordering-of-the-data

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!