Change the axes from absolute values to percentages via button or pull down menu, and tooltip

I would like to add a button or a pull down menu to change the left y axis from absolute values to percentages. I found this:

So they use:

var xAxis = d3.axisBottom(xScale)
	.tickFormat(d => Math.round(d*100/d3.max(data)) + "%");

var gX = svg.append("g")
	.attr("transform", "translate(0," + (h - padding) + ")")
	.call(xAxis);

I use

            var axLft = g.append("g")
             .attr('id', 'y-axis')
             .call(d3.axisLeft(yScale));

The combination gives some results, but there is still an error message.

                function NumtoPerc(){
                    var axLft = g.append("g")
                        .attr('id', 'y-axis')
                        .call(d3.axisLeft(yScale))
                        .tickFormat(d => Math.round(d*100/d3.max(dataset1)) + "%")
                        //.call(axLft)
                }

It should delete the old writings of course. There are two ways, one click = to percentages, 2nd click = back to absolute.
Or use the pull down menu and depending on the option that version is displayed.

Also trying to add tooltips like this:

                var tooltip = d3.select("svg")
                .append("g")
                .style("opacity", 0)
                .attr("class", "tooltip")
                .style("background-color", "white")
                .style("border", "solid")
                .style("border-width", "1px")
                .style("border-radius", "5px")
                .style("padding", "10px")

                var mouseover = function(d) {
                    tooltip
                    .style("opacity", 1)
                }

                var mousemove = function(d) {
                    tooltip
                    .html("Value" + xScale(d[0]))
                    .style("left", (d3.mouse(this)[0]+90) + "px") // It is important to put the +90: other wise the tooltip is exactly where the point is an it creates a weird effect
                    .style("top", (d3.mouse(this)[1]) + "px")
                }

                // A function that change this tooltip when the leaves a point: just need to set opacity to 0 again
                var mouseleave = function(d) {
                    tooltip
                    .transition()
                    .duration(200)
                    .style("opacity", 0)
                }

But it doesn’t work.

Watch your close parenthesis.

tickFormat is tacked onto the end of axisBottom. (It’s a method associated with the axis object)

What’s tickFormat attached to there?

Attached to the call argument? Only the left y axis should change to percentages, not the one at the bottom.

Not the call argument, but the call itself. But the call is actually a chain off of the “g” element creating method, append. So what’s being passed there is is an Element.

tickFormat is a method for Axis objects. Not an Element object.

Move the second close parenthesis on the call line, such that tickFormat is attached to the axis, not the call.

I am not sure by what you mean with second close.

I tried the following:

            function NumtoPerc(){
                var axLft = g.append("g")
                    .attr('id', 'y-axis')
                    .call(d3.axisLeft(yScale)
                    .tickFormat(d => Math.round(d*100/d3.max(dataset1)) + "%"))

That gives me NaN%

            function NumtoPerc(){
                var axLft = g.append("g")
                    .attr('id', 'y-axis')
                    .tickFormat(d => Math.round(d*100/d3.max(dataset1)) + "%")
                    .call(d3.axisLeft(yScale))

This gives me nothing.

So the way it is now gives me numbers from 1 to 100 but without the % sign, but the old numbers still exist and I think the percentages are not getting scaled.
I prefer the pull down menu option rather than a button. But best if both could work.

After that I would like that the graph dissappears behind the axes when zoomed in.
And a tooltip when the mouse is placed over the line/circles.

I found something using Angular, don’t wanna use that, stick to D3.

see the two comments above (you may need to shift the window right to see both)

Yes, that is what i did and it gives me NaN %, the old scale is still visible and being scaled when zoomed.

So there’s two problems at this point.

1: Dont re-create the axis, modify your existing one. (You’ve already got axLft defined, so change it by just using the call on it.)

2: Each element of dataset1 is an array, so d3.max cant determine what the “max” of an array of arrays is. You’ll have to tell max how to evaluate each array. In this case, d3.max(dataset1, x => x[1]) (because we want the Y’s). Computationally, you may want to calculate the max first and once, rather than calculating it on each tick, but the overhead is relatively small, so… shrug.

Using x[1] worked. This didn’t:

            function NumtoPerc(){
                d3.select('svg') // or 'g'
                    .call(axLft)
                    .tickFormat(d => Math.round(d*100/d3.max(dataset1, x => x[1])) + "%")
                    
            }

Close… Also you’ve reintroduced the problem of where your parentheses are… maybe some extra spacing will help highlight what we’re doing…

function NumtoPerc(){
    axLft.call(
       d3.axisLeft(yScale)
         .tickFormat(d => Math.round(d*100/d3.max(dataset1, x => x[1])) + "%")
    )                    
}

Or (to remove the max being recalculated over and over…)

function NumtoPerc(){
    const maxval = d3.max(dataset1, x => x[1]);
    axLft.call(
       d3.axisLeft(yScale)
         .tickFormat(d => Math.round(d*100/maxval) + "%")
    )                    
}

Ok, that works in that way that whenever I click on the button I see the axes changed to percentages. But when I zoom in, it changes back to absolute values and also when zoomed in already, the percentages should be adapted.

A second click should turn it back to normal. I remember for that I have to check if it has been clicked before.
Easier to do it with the drop down menu.

Now I am trying to implement a tooltip like this:

With a vertical line showing the values of every graph shown.

So that’s because your zoom function is re-rewriting the axis.

You can do it with either a variable, or the dropdown as you say; but whichever you do, you’ll need to code it in two places - the zoom, and the actual “onclick” (for a button) or “onchange” (for a dropdown) function responder. Basically you’ll copy your code from one place to the other, and your function should be able to tell what the current state is (numbers or percentage); either by reading the dropdown or checking the variable.

The tooltip’s tutorial will give you most of what you need - it’s the mousemove part that you’ll be interested in, obviously. You’ll want to be careful about your zoom function not accidentally sucking up the tooltip’s circle, so you may need to refine the “selectAll” there (like we did with the line before).

Have a play, see where you end up.

This works for at the beginning:

       <div id="auswahl_id" class = "auswahl_class">
        <select id="changeAxisYLabel" onChange = "update()">
            <option value ="absoluteValue">Absolute Value</option>
            <option value ="percentage">Percentage</option>
        </select>
       </div>

            function update() {
			var select = document.getElementById('changeAxisYLabel');
			var option = select.options[select.selectedIndex];

            let vv = option.value;
            let tt = option.text;

                if (vv == "percentage") {
                    console.log("In Prozent angegeben");
                    const maxval = d3.max(dataset1, x => x[1]);
                    axLft.call(
                    d3.axisLeft(yScale)
                    .tickFormat(d => Math.round(d*100/maxval) + "%")
                    )                    

                } else if (vv == "absoluteValue") {
                    console.log("In Zahlen angegeben");

                    const maxval = d3.max(dataset1, x => x[1]);
                    axLft.call(
                    d3.axisLeft(yScale)
                    .tickFormat(d => d)
                    )                    
                }
            }

		    update();

If I just place the update call at the end of the ZoomCallback function then it stays but doesn’t rescale. So I think I have to put the if statement in here as well where the axes are rescaled, so it either rescales the values or percentages:

                var newX = d3.event.transform.rescaleX(xScale);
                var newY = d3.event.transform.rescaleY(yScale);
            
                axBot.call(d3.axisBottom(newX));
                axLft.call(d3.axisLeft(newY));

This doesn’t work:

            var zoomCallback = function(){

                const maxval_z = d3.max(dataset1, x => x[1]);

                var select = document.getElementById('changeAxisYLabel');
				var option = select.options[select.selectedIndex];

                let vv = option.value;
                let tt = option.text;

                if (vv == "percentage") {
                    var yScale_P = axLft.call(
                        d3.axisLeft(yScale)
                        .tickFormat(d => Math.round(d*100/maxval_z) + "%"))
                    var newY = d3.event.transform.rescaleY(yScale_P);
                }
                
                else if (vv == "absoluteValue") {
                    var yScale_A = axLft.call(
                        d3.axisLeft(yScale)
                        .tickFormat(d => d))
                    var newY = d3.event.transform.rescaleY(yScale_A);
                }

                var newX = d3.event.transform.rescaleX(xScale);
            
                axBot.call(d3.axisBottom(newX));
                axLft.call(d3.axisLeft(newY));
            
                d3.selectAll('circle')
                .attr("cx", (d) => { return newX(d[0]); })
                .attr("cy", (d) => { return newY(d[1]); });
            
                d3.selectAll('#pointline')
                .attr("d",
                    d3.line()
                    .x(function(d) { return newX(d[0]); }) 
                    .y(function(d) { return newY(d[1]); }) 
                    .curve(d3.curveMonotoneX)
                );

// update();

            }

Right. So… this one got a bit messy, because we’ve got multiple things pinging around with the scales.

Here’s what I did. (Trying to minimize changes to your current attempt.)

Step 1: Define newX and newY in the global scope, when you create the original scales. Mostly this is so i know we’re not screwing up the original scale, just in case we need it later.

            // Step 4 
            let xScale = d3.scaleLinear().domain([0 , 365]).range([0, width]),
                yScale = d3.scaleLinear().domain([0, 105]).range([height, 0]),           
                newX = xScale,
                newY = yScale;

Step 2: Change the zoom function (Part A): Tell the function to use the global versions of newX and newY:

                var zoomCallback = function(){
                    newX = d3.event.transform.rescaleX(xScale);
                    newY = d3.event.transform.rescaleY(yScale);

(basically, remove the var’s.)

Part B:
I simplified this part a bit, but where you’ve currently got axBot and axLft doing their calls…

                axBot.call(d3.axisBottom(newX));
                axLft.call(d3.axisLeft(newY));

I skipped all the vv and tt stuff, and injected the following:

axBot.call(d3.axisBottom(newX));
                  
let axleft = d3.axisLeft(newY);
if (document.getElementById("changeAxisYLabel").value == "percentage") {
  const maxval = d3.max(dataset1, x => x[1]);
  axleft.tickFormat(d => Math.round(d*100/maxval) + "%")                    
}
axLft.call(axleft);

Finally, step 3:
In your update() code, I changed the scale that is being measured to the new, zoomed scales:

             if (vv == "percentage") {
                    console.log("In Prozent angegeben");
                    const maxval = d3.max(dataset1, x => x[1]);
                    axLft.call(
                    d3.axisLeft(newY) // <-- here
                    .tickFormat(d => Math.round(d*100/maxval) + "%")
                    )                    

                } else if (vv == "absoluteValue") {
                    console.log("In Zahlen angegeben");

                    const maxval = d3.max(dataset1, x => x[1]);
                    axLft.call(
                    d3.axisLeft(newY) //<-- and here.
                    )                    
                }

With those 4 changes, zooming works with update.

2 Likes

Yes, this is great. The difficulty is that the code in itself is right but the positioning and the minor changes make it work.

Gonna try to do the tooltip now and I also wanna hide the graph that goes beyond the axes when I zoom in, it should only be visible within the axes.

You’ve got examples for the tooltip (that you’ve linked to above) and the going-beyond-the-axes (go back and look at the scatter plot you showed me in the previous thread…), so… give it a whack. We’ll be here. :wink:

1 Like

I tried examples from here:

https://www.pluralsight.com/guides/create-tooltips-in-d3js

https://gramener.github.io/d3js-playbook/tooltips.html

www.d3noob.org/2014/07/my-favourite-tooltip-method-for-line.html

Some use focus as a variable, some use append body, append rect, append svg. Some only have one simple data set so they add the on.mousemove directly under the added data. I can’t or don’t want to add this to every case. Some also use the functions directly and other name them mousemove, leave, out, mouseover. The over should display the data so I guess I add something like this: “.text((d) => { return newX(d[0])})”

This is mixing all possible solutions together, sometimes I used one, then the other:

        // Tooltip

        var focus = svg.append("g") 
            .style("display", "none");

        /*
        // append the circle at the intersection 
        focus.append("circle")
            .attr("class", "y")
            .style("fill", "none")
            .style("stroke", "blue")
            .attr("r", 4);
        */
        // append the rectangle to capture mouse
        var tooltip = svg.append("rect")
            .attr("width", width)
            .attr("height", height)
            .style("fill", "none")
            .style("pointer-events", "all")
            .attr('id', 'tooltip')
            .attr("class", "tooltip")
            .on("mouseover", mouseover /*{ focus.style("display", null); }*/)
            .on("mouseout", mouseleave /*{ focus.style("display", "none"); }*/)
            .on("mousemove", mousemove);

            /*
            d3.select('body')
            .append('div')
            .attr('id', 'tooltip')
            .attr('style', 'position: absolute; opacity: 0;');

            var tooltip = d3.select("svg")
            .append("g")
            .style("opacity", 0)
            .attr("class", "tooltip")
            .style("background-color", "white")
            .style("border", "solid")
            .style("border-width", "1px")
            .style("border-radius", "5px")
            .style("padding", "10px")
            */

            var mouseover = function(d) {
                tooltip
                .transition()
                .duration(200)
                .style("opacity", 1)
                .text((d) => { return newX(d[0])})
            }

            var mousemove = function(d) {
                tooltip
                .html("Value" + xScale(d[0]))
                .style("left", (d3.mouse(this)[0]+90) + "px") // It is important to put the +90: other wise the tooltip is exactly where the point is an it creates a weird effect
                .style("top", (d3.mouse(this)[1]) + "px")
            }

            // A function that change this tooltip when the leaves a point: just need to set opacity to 0 again
            var mouseleave = function(d) {
                tooltip
                .transition()
                .duration(200)
                .style("opacity", 0)
            }

https://codepen.io/Dvdscot/pen/zYjpzVP

Nothing worked so far. Where should I add the on mouse functions first? And are those correct at least?

And here I am trying to hide everything beyond the axes:

Using code from here:

Half works for the left axis, the lines are removed too soon.

            // Don't draw beyond the axes
            var clip = svg.append("defs")
               .append("SVG:clipPath")
               .attr("id", "clip")
               .append("SVG:rect")
               .attr("width", width + 100 )
               .attr("height", height + 100)
               .attr("x", 100)
               .attr("y", 100);

            var scatter = svg.append('g')
               .attr("clip-path", "url(#clip)")

            // Step 7
            scatter
//            svg.append('g')
//            .select('#points')
            .selectAll("dot")
            .data(dataset1)
            .enter()
            .append("circle")
            .attr("cx", function (d) { return xScale(d[0]); } )
            .attr("cy", function (d) { return yScale(d[1]); } )
            .attr("r", 3)
            .attr("transform", "translate(" + 100 + "," + 100 + ")")
            .style("fill", "#CC0000")

            var scatterline = svg.append("path")
               .attr("clip-path", "url(#clip)")

            scatterline
//            svg.append("path")
            .datum(dataset1) 
            .attr("id", "pointline")
            .attr("class", "line") 
            .attr("transform", "translate(" + 100 + "," + 100 + ")")
            .attr("d", line)
            .style("fill", "none")
            .style("stroke", "#CC0000")
            .style("stroke-width", "2")

Gonna take the cutoff first, because its the more concrete solution. And more concrete problem.

So… lets look at your lines for a second.

       var scatterline = svg.append("path")
           .attr("clip-path", "url(#clip)")

So… scatterline is a path element.

            scatterline
//            svg.append("path")
            .datum(dataset1) 
            .attr("id", "pointline")
            .attr("class", "line") 
            .attr("transform", "translate(" + 100 + "," + 100 + ")")
            .attr("d", line)
            .style("fill", "none")
            .style("stroke", "#CC0000")
            .style("stroke-width", "2")

You write the red line to that path. Cool.

            scatterline
//            svg.append("path")
            .datum(dataset2) 
            .attr("id", "pointline")
            .attr("class", "line") 
            .attr("transform", "translate(" + 100 + "," + 100 + ")")
            .attr("d", line)
            .style("fill", "none")
            .style("stroke", "#008800")
            .style("stroke-width", "2")

you… write… the green line to that same path, overwriting it… uhhh… thats gonna be a problem…

I can see where you’re trying to go with this. Let’s tweak it juuuust a little bit. For the moment, lets solve this by putting all of our lines in a group (<g>) element.

            var scatterline = svg.append("g")
               .attr("clip-path", "url(#clip)")

Now create each line as a path inside the group.

scatterline.append("path")
            .datum(dataset1)
...etc 

So that’ll at least draw the lines. Now why isnt it clipping anything other than the top/left of the graph.

Let’s look at your clippath.

            var clip = svg.append("defs")
               .append("SVG:clipPath")
               .attr("id", "clip")
               .append("SVG:rect")
               .attr("width", width + 100 )
               .attr("height", height + 100)
               .attr("x", 100)
               .attr("y", 100);

…why are we adding 100 to the width and height? The width and height of your graph are…width and height… so if we add to those numbers, we’re going beyond the size of your graph…

x and y are absolute values. width and height are relative values. Specifically, they’re relative from the point (x,y).