1

I'm new to D3, but loving it so far. But I know my solutions lack... elegance.

I am trying to have 2 controls, a table and a graph displaying the data represented by the cells of the table. If you click on a cell on the table, the associated line should be highlighted. If you hover over a line the associate table cell will change color. Eventually there will be a third control showing detail data specific to that cel. Unfortunately I've only managed to make this work if I use static calls to the update function. If I try to be clever and dynamic the whole thing breaks.

I've tried to minimize my example as much as I can below. The click table->update line works because the calls to SelectData() that updates everything uses constant data. However the mouseover on the lines doesn't work. Eventually I need the table to be more dynamic too, but for now, how do I fix this?

<!DOCTYPE html>
<meta charset="utf-8">
<style>
    .lineDefault {
        fill: none;
        stroke: red;
        stroke-width: 1.5px;
        stroke-dasharray: 4,4;
    }
    .axis path,
    .axis line {
        fill: none;
        stroke: #000;
        shape-rendering: crispEdges;
    }
</style>
<body>
    <div id="wrap">
        <table>
            <tr>
                <td id="dataBlock" onclick="SelectData(0)">1</td>
                <td id="dataBlock" onclick="SelectData(1)">2</td>
            </tr>
            <tr>
                <td id="dataBlock" onclick="SelectData(2)">3</td>
                <td id="dataBlock" onclick="SelectData(3)">4</td>
            </tr>
        </table>
        <div>
            <svg class="chart"></svg>
        </div>
    </div>
    <script src="http://d3js.org/d3.v3.min.js"></script>
<script>
    var width = 600, height = 600;
    var maxx = 100,
        maxy = 100;

    var linedata = {};
    linedata[0] = [[0, 50 ],[ 50, 60 ],[100, 100]];
    linedata[1] = [[0, 40 ],[ 40, 40 ],[100, 90 ]];
    linedata[2] = [[0, 20 ],[ 50, 30 ],[100, 90 ]];
    linedata[3] = [[0, 0  ],[ 60, 30 ],[100, 30 ]];
    var activeElement = 0;
    var graphlines = {};
    var numlines = 0;

    chart = d3.select(".chart").attr("viewBox", "0 0 600 600").append("g");

    var x = d3.scale.linear().domain([0, maxx]).range([0, width]);

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

    var xAxis = d3.svg.axis().scale(x).orient("bottom");

    var yAxis = d3.svg.axis().scale(y).orient("left");

    var line = d3.svg.line()
        .x(function(d) { return x(d[0]); })
        .y(function(d) { return y(d[1]); });

    for (var i = 0; i < 4; i++) {
        graphlines[i] = chart
            .append("path")
            .datum(linedata[i])
            .attr("class", "lineDefault")
            .attr("id", "linedata")
            .attr("d", line)
            .on("mouseover", SelectData(i));
        numlines++;
    }

    function SelectData(n) {
        d3.selectAll("td").transition()
            .style("background-color", function(d, i) {
                return i == n ? "#c99" : "#fff";
            });
        activeElement = n;
        for (var i = 0; i<numlines; i++) {
            if (i == n) {
                graphlines[i]
                    .style("stroke-dasharray", "1,0")
                    .transition()
                    .style("stroke-width", "3")
                    .style("stroke", "steelblue");
            } else {
                graphlines[i]
                    .style("stroke-dasharray", "4,4")
                    .transition()
                    .style("stroke-width", "1.5")
                    .style("stroke", "red");
            }
        }
    }

</script>

The mouseclick on the table effects the lines, the mouseover on the lines does not effect the table. Also I will accept any berating on my elegance and pointers to fixing them.

AmeliaBR
  • 27,344
  • 6
  • 86
  • 119
Cymon
  • 107
  • 1
  • 11

2 Answers2

3

First, the reason why your mouseover method wasn't working is because in this line:

        .on("mouseover", SelectData(i));

you are calling the SelectData method at the time you call the .on() method. So when you're done initializing, your last item is selected, but there is no function listening to the mouseover event. What you want is to pass the function name to the .on() method. If you make that function take two parameters (usually named d and i), then d3 will automatically pass the index value as the second parameter. I wrote up a rather long discussion about passing functions as parameters for someone else recently, you may find it useful.

Furthermore, you're really not taking advantage of the d3 selection structure, which can do in one call all the things you're using for-loops to do. I'd suggest taking the time to read through some tutorials on how the d3 selections work.

Now, to your main question:

The usual solution for selecting an element whose data matches the data of a clicked element is to give all elements a class based on a unique ID from the data. Then you can easily select all the elements associated with a given data object. If the user selects an object with d.name=Sue, and you initialized all the elements with that name as a class name, then you can do d3.select("path.Sue") and also d3.select("td.Sue") to find the correct ones.

Your example data doesn't seem to have any unique data id value, just the index number. So you don't even need a unique class, you can just use the nth-of-type(i+1) CSS selector. (It's i+1 because the CSS counting starts at 1, while the data count starts at 0.)

However, I would suggest that you use a special CSS class to apply all your highlighting styles. That way, it will be easy to select the currently highlighted values to remove that class, instead of having to loop through everything to test whether or not it matches. You can use CSS transitions to transition the style after a class changes.

Here's a fiddle version of your code, tidied up to use d3 properly and with the above method for highlighting data.

http://fiddle.jshell.net/g8z5h/

I haven't implemented the live version of your table, but I recommend you read the tutorial on nested selections to figure out how that will work.

I did give each data block it's own row so that the nth-of-type() selector would work properly (the CSS numbering of elements only works if they are all siblings, they can't be table data elements split across multiple rows). If you need the original layout to work, you'll have to give elements class names based on their index value, and use those to select. I also moved the click event binding into the code, because JSFiddle wraps all its code in a window load event, so the SelectData function wasn't visible outside it.

Community
  • 1
  • 1
AmeliaBR
  • 27,344
  • 6
  • 86
  • 119
  • 1
    P.S. If you don't like the sliding effect when the lines change from dashed to solid, define the `stroke-dasharray` property for the solid lines to have the same total length as the dashed line pattern. E.g. with the dash pattern being `4 4`, the solid line pattern of the same length is `8 0`. – AmeliaBR Jan 31 '14 at 01:21
  • And one final comment: **`id` values have to be unique for the entire webpage!** You can't use `id` to group similar elements, that's what classes are for. That's why I changed `id` to `class` in multiple parts of your code. – AmeliaBR Jan 31 '14 at 01:28
  • I am a big fan of yours @AmeliaBR! Thank you so much for taking the time to write such clear, detailed and comprehensive explanations...not to mention the extra mile you go in cooking up some really solid examples. People like you, Lars and many others (I wish I could list them all) that frequent the D3 stackoverflow community are an absolute trove of great information and good will. Thanks again! – FernOfTheAndes Jan 31 '14 at 02:07
  • 1
    @FernOfTheAndes Aw shucks, I'm blushing! But seriously, I figure the more detailed I make the explanations the first time around, the less likely someone will come back with a near-identical question tomorrow. – AmeliaBR Jan 31 '14 at 03:04
  • This is great. Thank you so much. I've been going through tutorials for days but when I venture off the beaten path... well, you've helped me a ton. Now the problem I have is that the line is very small. I'd be nice if the mouseover area were a little wider. – Cymon Feb 03 '14 at 17:31
  • @Cymon you could use the trick that is used by many d3 "Voronoi" examples (google them) -- add a second, larger element over top, that isn't painted but that has the style `pointer-events:all;` and make that the element that responds to mouse events. In your case, that second element would be a path with the same `"d"` attribute, but a much thicker stroke width. Depending on how much else you are doing with the graph (updating, etc.), you'll have to decide whether it is worth the hassle of keeping track of twice as many path elements. – AmeliaBR Feb 03 '14 at 17:51
  • Correction: you'd actually want the overlay line to have `pointer-events: stroke;`; "all" would create a strange response area based on the fill of the line as well. Info on pointer events and SVG: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/pointer-events – AmeliaBR Feb 03 '14 at 17:54
  • @AmeliaBR Can you help me with that overlay. I can't figure out what I'm doing wrong. I've tried doing what I've seen in other examples, but it doesn't work. I've fiddled the example you sent me. In my example the line isn't invisible... yet. But it still won't render the second one. http://fiddle.jshell.net/g8z5h/4/ – Cymon Feb 04 '14 at 22:51
  • @Cymon You can't add one path inside another -- which is what happens when you append a path on a selection consisting of other paths. They need to be sibling elements -- it's easiest if you just make a separate selection joined to the same data. See http://fiddle.jshell.net/g8z5h/5/ Note how I now have to specify the class of the path in the `selectAll` method, to keep the two types of lines straight. – AmeliaBR Feb 04 '14 at 23:14
1

I have been referring this since i wanted to achieve the same thing in angular2 using d3. Here is my code...

  private drawLine() {
    this.line = d3Shape.line()
        .y((d: any) => this.y(d.rank))
        .defined(function(d : any) { return d.rank})
        .x((d: any) => this.x(d.year));

   var color = function(i) {
        var colors = ["#35559C","#D9469C","#70D45B","#915ABB","#FF7C26","#50C5F6","#ECBE4B"];
        return colors[i % colors.length];
    };

    var i = 0;
    var j=1;
    for(let d of this.lineData){
        this.g.append('path')
            .attr('d', this.line(d.values))
            .attr("class", "line")
            .attr("stroke", color(i))
            .attr("transform", "translate(0,"+this.margin.top+")")
            .attr("id","row-"+j)
            .on("mouseover",function(){
                d3.select(this)
                    .attr("class","line1");
                d3.select("tr#"+this.id)
                    .style("background-color","gainsboro")
                //console.log( d3.select("tr#"+this.id))
            })
            .on('mouseout', function(){
                d3.select(this)
                    .attr("class","line");
                d3.select("tr#"+this.id)
                    .style("background-color","")
            });

            j++;
            i++;
            if(i > 9) {
                i = 0;
            }
  }
}
private mouseover(event){
d3.select(event.currentTarget)
    .style("background-color","gainsboro")
d3.select("path#"+event.currentTarget.id)
    .attr("class","line1");

}

private mouseout(event){
d3.select(event.currentTarget)
    .style("background-color","")
d3.select("path#"+event.currentTarget.id)
    .attr("class","line");
}

while creating line i assigned id to it and the same id to every row in table.

<tr *ngFor="let object of jsonFile; let i = index" id="{{'row-'+[i+1]}}" 
     (mouseover)="mouseover($event)" (mouseout)="mouseout($event)">
    <td *ngFor="let column of columnHeaders" [ngStyle]="{'width': 
           column.length}">
        <div *ngIf="isString(column.columnType) == true">
          {{object[column.id] | trim}}
        </div>
        <div *ngIf="isString(column.columnType) == false">
          {{object[column.id]}}
        </div>
    </td>
  </tr>

and called the mouseover and mouseout function in table row. I would like to have some recommendations if m wrong somewhere.. :)

Ankita
  • 11
  • 1