d3 tooltip | passing in a variable - javascript

I have a webpage with 4 d3 charts for each of 11 different regions. One of those charts is an area chart and the code snippet is:
for (mm=0; mm<regions.length; mm++) {
areas.append("path")
.attr("class",function(d,i){ return "area cons ar"+i+" region"+mm;} )
.attr("id", function(d,i) { return "area_"+i+"_"+mm})
.attr("d", function(d,i) { return area(d)} );
var test = d3.selectAll("path.region"+mm)
.call(d3.helper.tooltip()
.attr("class", function(d, i) { return "tooltip"; })
.text(function(d, i){
console.log(mm);
return "i "+consSubProducts[i]+', i: '+i;}));
}
I want to add tooltips to the charts. In the area plot, each region has different products. Some have 7 products, others have 5. I need to use the mm variable at its runtime values (0-10) to call the correct product array where consSubProducts is currently. (ConsSubProducts is set to a different product array at the top of the for...loop, but as with mm, the code can only see the finally-set array and not the runtime arrays.)
In this code, mm always returns 11, i.e. it returns the final value of mm rather than the values at runtime.
I have tried passing mm in within tooltip() and within .text(function(d,i,mm) - the latter clearly doesn't work as it's expecting a j. I've also tried attaching mm to the class or ID of an object, but within the call() console.log(this) logs object.window.
I've tried modifying tooltip.js but although I can generate the label I want I can't work out how to override the text. Tooltip.js:
d3.helper = {};
d3.helper.tooltip = function(){
var tooltipDiv;
var bodyNode = d3.select('body').node();
var attrs = {};
var text = '';
var styles = {};
function tooltip(selection){
selection.on('mouseover.tooltip', function(pD, pI){
var name, value;
// Clean up lost tooltips
d3.select('body').selectAll('div.tooltip').remove();
// Append tooltip
tooltipDiv = d3.select('body').append('div');
tooltipDiv.attr(attrs);
tooltipDiv.style(styles);
var absoluteMousePos = d3.mouse(bodyNode);
tooltipDiv.style({
left: (absoluteMousePos[0] + 10)+'px',
top: (absoluteMousePos[1] - 15)+'px',
position: 'absolute',
'z-index': 1001
});
// Add text using the accessor function, Crop text arbitrarily
tooltipDiv.style('width', function(d, i){ return (text(pD, pI).length > 80) ? '300px' : null; })
.html(function(d, i){return text(pD, pI);});
})
.on('mousemove.tooltip', function(pD, pI){
// Move tooltip
var absoluteMousePos = d3.mouse(bodyNode);
tooltipDiv.style({
left: (absoluteMousePos[0] + 10)+'px',
top: (absoluteMousePos[1] - 15)+'px'
});
// Keep updating the text, it could change according to position
tooltipDiv.html(function(d, i){ return text(pD, pI); });
})
.on('mouseout.tooltip', function(pD, pI){
// Remove tooltip
tooltipDiv.remove();
});
}
tooltip.attr = function(_x){
if (!arguments.length) return attrs;
attrs = _x;
return this;
};
tooltip.style = function(_x){
if (!arguments.length) return styles;
styles = _x;
return this;
};
tooltip.text = function(_x){
if (!arguments.length) return text;
text = d3.functor(_x);
return this;
};
return tooltip;
};
Any help appreciated!

It looks like this is way over the top for something that is super simple. Unless I'm missing something. Let me post some code for how I handle tooltips.
Predefine the tooltip div in the page HTML and class it .tooltip and apply the css of display:none; position:absolute; and add .tooltip.show{display:block;}
var tooltip = d3.select('.tooltip');
function tipUpdate(d,i) {
tooltip.classed('show',true).html(d+" : "+i);
}
areas.append('path')
//all your path attributs
.on('mouseover', tipUpdate)
.on('mouseout', function() { tooltip.classed('show',false); });
areas.on('mousemove', function(d,i) {
var mouse = d3.mouse('body').map( function(d) { return parseInt(d); });
tooltip.attr("style", "left:"+(mouse[0]+10)+"px;top:"+(mouse[1]-15)+"px");
});
This will show the tooltip div on mouseover of the path and move the tooltip relative to it's position in the body and hide the tooltip when the mouse leaves the path. It will display the data and index of the point in the path that it is currently over. You might want to change that but it should be easy.
One problem I thought about later is you cannot do a working tooltip like this on a path. It will always show the data from the last point in the path.
I have posted a working fiddle. D3 tooltip

Related

Is there a way to get specified data through click event using tooltip function in d3 v6.1.1

As per migration guide the click event have to show the data in the clicked bar.
Unable to get the error in this code:
https://codepen.io/bablu4195-1/pen/oNxGWRw
d3.on('click', (event,d,i)=> {
tooltip.classList.add('show');
tooltip.style.left = i * barWidth + padding * 2 + 'px';
tooltip.style.top = height - padding * 4 + 'px';
tooltip.innerHTML = `<small>${d[0]}</small>$${d[1]} billions`;
tooltip.setAttribute("data-date", d[0]);
})
.on('click', () => {
tooltip.classList.remove('show')
}
You seem to have missed something about d3 scaling. Normally, you assign an object using .data(), like .data([{ name: 'hello world', value: 12 }]). However, you use scaledGDP, which means it has lost all other information and is just a number. If you use GDP, you can then access the scaled value using, for example, rect.attr('height', function(d) return linearScale(d); }).
Other than that, your tooltip didn't work for a couple of reasons. Firstly, you assigned on('click') twice. Once to add the tooltip and once to remove it. The second one overwrote the first. Secondly, you used tooltip like a HTML element, but it was a d3 selection, so use .classed() instead of classList(), etc. Finally, as it said in the migration guide, event is now the first argument to the function, not d.
All this gives the following:
CSS:
#tooltip {
opacity: 0;
}
#tooltip.show {
opacity: 1;
}
Javascript
.on("click", (event, d, i) => {
if(!tooltip.classed("show")) {
tooltip.classed("show", true)
.html(`<small>${d[0]}</small>$${d[1]} billions`)
.attr("data-date", d[0])
.style("left", i * barWidth + padding * 2 + "px")
.style("top", height - padding * 4 + "px");
} else {
tooltip.classed("show", false);
}
});
It doesn't yet fill the tooltip - because of the scaledGDP vs GDP thing, but it shows it and positions it.

labels inside the heatmap rects

I would like to display the value of heatMap boxes inside the boxes not just in the tool tip.
Looks although this feature has yet to be added according to: https://github.com/dc-js/dc.js/issues/1303 , would this be able to be accomplished with d3.js? Thanks.
HeatMap:
heatMap
.width(1000)
.height(400)
.dimension(dimension)
.group(filtered_heatMap_group)
.margins({
left: 200,
top: 30,
right: 10,
bottom: 50
})
.keyAccessor(function (d) {
return d.key[0];
})
.valueAccessor(function (d) {
return d.key[1];
})
.colorAccessor(function (d) {
return +d.value.color;
})
.title(function (d) {
return "Manager: " + d.key[1] + "\n" +
"FTE: " + d.value.toolTip + "\n" +
"Date: " + d.key[0] + "";
})
.rowOrdering(d3.descending);
https://jsfiddle.net/_M_M_/jvf5ot3p/3/
The devil is in the details.
It's pretty easy to get some text onto the boxes. Getting them nicely positioned and formatted will take some more work. I'll try to get you started anyway.
You can use the renderlet event. pretransition would be nicer, but we will need to steal some attributes from the rects because the chart's scales are not accessible, and these attributes aren't all initialized at pretransition time.
This is a common pattern for annotations in dc.js: wait for a chart event, then select existing elements and add stuff to them. The element selectors are documented here; we want g.box-group.
The rest is standard D3 general update pattern, except that as I mentioned, we don't have access to the scales. Rather than trying to duplicate that language and possibly getting it wrong, we can read attributes like x, y, width, height from the rects that already reside in these gs.
SVG doesn't have multiline text, so we have to create tspan elements inside of text elements.
Putting that all together:
.on('renderlet.label', function(chart) {
var text = chart.selectAll('g.box-group')
.selectAll('text.annotate').data(d => [d]);
text.exit().remove();
// enter attributes => anything that won't change
var textEnter = text.enter()
.append('text')
.attr('class', 'annotate')
textEnter.selectAll('tspan')
.data(d => [d.key[1], d.value.toolTip, d.key[0]])
.enter().append('tspan')
.attr('text-anchor', 'middle')
.attr('dy', 10);
text = textEnter.merge(text);
// update attributes => position and text
text
.attr('y', function() {
var rect = d3.select(this.parentNode).select('rect');
return +rect.attr('y') + rect.attr('height')/2 - 15;
});
text.selectAll('tspan')
.attr('x', function() {
var rect = d3.select(this.parentNode.parentNode).select('rect');
return +rect.attr('x') + rect.attr('width')/2;
})
.text(d => d);
})
Notice that I gave up and nudged the text 15px up... there may or may not be a better way to get vertical centering of multiline text.
The text doesn't quite fit, even without descriptions. I'll let you figure that out.
But it's there and it updates correctly:
Fork of your fiddle.

How to make the circles disappear based on keyboard input?

based on this example: http://bl.ocks.org/d3noob/10633704 i wish to make an input on my keyboard (a number) and make the circles disappear with the help of array.slice(). Unfortunally it did not worked well. In my code, i created some circles based on the values of the array days. With the HTML part i am able to create a button, where i can make a number input. With the last part days.slice(nValue) i want that the input number is the same like the number inside the brackets of the slice() function, so the array days is getting shorter and automatically let circles based on the value of the array disappear. But unfortunally there is a mistake i made in this code. Can someone maybe be so kind and help? I am using D3 to solve this problem.
Thanks
<!DOCTYPE html>
<meta charset="utf-8">
<title>Input (number) test</title>
<p>
<label for="nValue"
style="display: inline-block; width: 120px; text-align: right">
angle = <span id="nValue-value"></span>
</label>
<input type="number" min="0" max="360" step="4" value="0" id="nValue">
</p>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var width = 600;
var height = 300;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var days = [7, 12, 20, 31, 40, 50];
console.log(days);
var circle = svg.selectAll("circle")
.data(days)
.enter().append("circle")
.attr("cy", 60)
.attr("cx", function(d, i) { return i * 100 + 40; })
.attr("r", function(d) { return Math.sqrt(d); });
d3.select("#nValue").on("input", function() {
update(+this.value);
});
// Initial update value
update(0);
function update(nValue) {
days.slice(nValue);
}
It took me a while to see what you're after here, and I might still be off a bit in my understanding.
The Problem
As I see understand it, you are modifying an array of data (with a select menu in this case), but the modified array does not appear to modify your visualization. Essentially, as "the array days is getting shorter ... let circles based on the value[s] of the array disappear."
Updating the visualization
To update the visualization you need to bind the new data to your selection. After this you can remove unneeded elements in the visualization, add new ones (not relevant to this question), or modify existing elements. Changing the data array by itself will not update the visualization. To have the visualization utilize the new information you need to bind that data to the selection:
circle.data(data);
Then you can remove the old items:
circle.exit().remove();
Then you can modify properties of the old items:
circle.attr('cx',function(d,i) {...
Your update function needs to at least update the data and remove unneeded elements.
Changing the Array
In the following snippet I append both a select menu and the circles with d3 based on the data in the array. Selecting an item in the menu will remove a circle:
var data = [10,20,30,40,50,60,70,80,90,100];
var color = d3.schemeCategory10; // color array built in
//// Add the select and options:
var select = d3.select('body')
.append('select')
.on('change',function() { update(this.value) });
var start = select.append('option')
.html("select: ");
var options = select.selectAll('.option')
.data(data)
.enter()
.append('option')
.attr('class','option')
.attr('value',function(d,i) { return i; })
.html(function(d) { return d; });
//// Add the circles (and svg)
var svg = d3.selectAll('body')
.append('svg')
.attr('width',500)
.attr('height',200);
var circles = svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx',function(d,i) { return i * 30 + 50; })
.attr('cy',50)
.attr('r',10)
.attr('fill',function(d,i) { return color[i]; });
// Update everything:
function update(i) {
data.splice(i,1); // remove that element.
// Update and remove option from the select menu:
options.data(data).exit().remove();
// Remove that circle:
circles.data(data).exit().remove();
circles.attr('cx',function(d,i) { return i * 30 + 50; })
.attr('fill',function(d,i) { return color[i]; });
// reset the select menu:
start.property('selected','selected');
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
There is a problem here, only the last circle and menu item is removed each time. Why? Imagine a four element array, if you remove the second item, d3 does not know that you removed the second item, you might have modified elements two and three and removed element four.
Since all your items are appended with their increment (which position they are in the array), and this doesn't account for holes that were created when other items were removed, you need to change the approach a little.
A solution
Instead of relying on the increment of an item in the array (as this will change every time an element that is before another element is removed from the array), you could use an id property in your data.
This would require restructuring you data a little. Something like:
var data = [ {id:1,value:1},{id2....
As the id property won't change, this makes a better property to set attributes. Take a look at the following snippet:
var data = [{id:0,value:10},{id:1,value:20},{id:2,value:23},{id:3,value:40},{id:4,value:50},{id:5,value:60},{id:6,value:70},{id:7,value:77},{id:8,value:86},{id:9,value:90}];
var color = d3.schemeCategory10; // color array built in
//// Add the select and options:
var select = d3.select('body')
.append('select')
.on('change',function() { update(this.value); } ); // add an event listener for changes
// append a default value:
var start = select.append('option')
.html("Select:");
var options = select.selectAll('.option')
.data(data)
.enter()
.append('option')
.attr('class','option')
.attr('value',function(d,i) { return i; })
.html(function(d) { return d.value; });
//// Add the circles (and svg)
var svg = d3.selectAll('body')
.append('svg')
.attr('width',500)
.attr('height',200);
var circles = svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx',function(d) { return d.id * 30 + 50; })
.attr('cy',50)
.attr('r',10)
.attr('fill',function(d) { return color[d.id]; });
// Update everything:
function update(i) {
data.splice(i,1); // remove the element selected
// Update and remove option from the select menu:
options.data(data).exit().remove();
// Remove that circle:
circles.data(data).exit().remove();
// update the options (make sure each option has the correct attributes
options.attr('value',function(d,i) { return i; })
.html(function(d) { return d.value; })
// Make sure circles are in the right place and have the right color:
circles.attr('cx',function(d) { return d.id * 30 + 50; })
.attr('fill',function(d) { return color[d.id]; });
// reset the default value so the change will work on all entries:
start.property('selected', 'selected');
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
Try changing your update function to this:
function update(nValue) {
days = days.slice(nValue);
}

How to redraw chart : d3.js chaining function and crossfilter filtering

Edit : JSBin dont work go to JS fiddle to see my code
I'm training my self with d3 and crossfilter and i face some difficulties.
I'm trying to make a chaining function to create bar chart. The idea is to not write all the code again and again for each bar chart i want to create.
I was inspired by the example Fast Multidimensional Filtering for Coordinated Views which is what i wanted to do (you can find code Here).
But I want to bring some personal touches in my generic barChart function.
I decided to do it with chaining function like in the example.
I understood how to create as many graphics as you wish, but i dont understand, (when brushes event appears), how to redraw all the bar depending on the filter.
If i wanted to do it outside a function i would define again the all the properties like x, y, axis etc. depending on the new data which is the filtered data like this:
var updateRange = function(filt){
data = dimension.filter(filt) //assuming dimension is a crossfilter dimension
// Scale the range of the data again
x.domain([0, d3.max(data, function(d) { return d.key; })+1]);
// Select the section we want to apply our changes to
var chart = d3.select(".chart")
//Update all rects
chart.selectAll("rect.hidden")
.data(data)
.transition()
.delay(function(d, i) {
return i * 50;
})
.duration(500)
.attr("y", function(d) {
return y2(d.value);
})
.attr("height", function(d) {
return height - y2(d.value);
});
I made a JSBin to discuss on how we can make the chart updated.
And this is the brush functions i use.
brush.on("brushstart", function() {
var div = d3.select(this.parentNode.parentNode.parentNode);
div.select(".title a").style("display", null);
});
brush.on("brush", function() {
var g = d3.select(this.parentNode),
extent = brush.extent();
if (round) g.select(".brush")
.call(brush.extent(extent = extent.map(round)))
.selectAll(".resize")
.style("display", null);
g.select("#clip-" + id + " rect")
.attr("x", x(extent[0]))
.attr("width", x(extent[1]) - x(extent[0]));
dimension.filter(extent);
});
brush.on("brushend", function() {
if (brush.empty()) {
var div = d3.select(this.parentNode.parentNode.parentNode);
div.select(".title a").style("display", "none");
div.select("#clip-" + id + " rect").attr("x", null).attr("width", width);
dimension.filterAll();
}
)};
waiting for your comments,
Chris.
Edit : some clarification
To be clearer, when i render the chart and use the brush the data are correctly filtered. (if i put some console.log i see the data filtered depending on the brush)
But the chart are not updated depending on the brush. I know the issue come from th way i used the brush event (brush.on().
I think i need to call the render function in some way but dont know how to do it with chaining function to be applied to all the charts.
Edit : Chart updated with external brush
The chart are now successfully updated when the brush is set externally (by clicking on the link).
Just adding this lines
if (brushDirty) {
brushDirty = false;
g.selectAll(".brush").call(brush);
div.select(".title a").style("display", brush.empty() ? "none" : null);
if (brush.empty()) {
g.selectAll("#clip-" + id + " rect")
.attr("x", 0)
.attr("width", width);
} else {
var extent = brush.extent();
g.selectAll("#clip-" + id + " rect")
.attr("x", x(extent[0]))
.attr("width", x(extent[1]) - x(extent[0]));
}
}
In order to update charts you can remove it and then redraw with new filters.
Something like this :
d3.selectAll(".chart").selectAll("svg").remove();
Or
$('#chart'+chart_id+' svg').remove();
and then redraw it by calling again your drawing function with updated data.
Hope this will help you. Sorry If I misunderstood you. I need to train my english =P
EDIT :
I found these examples without remove. It might help you.
http://jsfiddle.net/sx9myywh/
https://bl.ocks.org/RandomEtc/cff3610e7dd47bef2d01
I found where the problem was. And i feel stupid.
The brush.onevent was placed incide the function which generate the chart and have to be outside.
function my(div){
}
brush.on(...
I have updated the JS Fiddle with the correct answer.
I will create a github repository it will be cleaner.

Refresh function not being called properly d3

I am working with a scatterplot in d3. Dots on the graph represent a paper. On right click of a dot I have a context menu where there are 2 options: 1) to add that paper to the library (change type to In_library) and 2) Remove from library (remove paper from data completely).
I call the refreshGraph() function after each of these updates which redraws the graph with the updated data. But nothing happens which I assume is because the refreshGraph() is not being called properly? Or for option 1 type library is not being set properly? When refreshGraph is called after option 1 the dot should turn blue and on calling it for option 2 the dot should disappear from display as it has been removed from the alldata which is the data that is being used to draw the circles. Here is the relevant code:
allData = [];
var menu = [{
title: 'Add to Library',
action: function addToLibrary(elem, d, i) {
d3.json("connection6.php?paperID="+d.ID, function(error, dataJson) {
for(i=0;i<allData.length;i++){
if (d.type === "In_library")
{
alert("The paper: " + d.TITLE + " is already in your Library!");
return;
}
}
d.type = "In_library"; // is this the correct way to change the type if the input has a different type??
refreshGraph();
})
refreshGraph();
}
},
{
title: 'Remove from Library',
action: function removeFromLibrary (elem, d, i) {
d3.json("connection9.php?paperID="+d.ID, function(error, dataJson) {
//loop through allData and if selected ID has type In_library, remove from allData
for(i=0;i<allData.length;i++){
if (d.type == "In_library"){
allData.splice(i--,1);
}
}
refreshGraph();
})
}
}
]
function refreshGraph() {
// draw dots
var circles = svg.selectAll("circle")
.data(allData)
circles.transition()
.attr("cx", function(d) {return x(YearFn(d))})
.attr("cy", function(d) {return y(Num_citationsFn(d))})
circles.enter()
.append("circle")
.attr("class", "dot")
.attr("r", 3.5)
.attr("cx", function(d) {return x(YearFn(d))})
.attr("cy", function(d) {return y(Num_citationsFn(d))})
.style("fill",function(d){
var colour = "black"
switch(d.type){
case "In_library":
colour = "blue";
break;
case "cited by":
colour = "red";
break;
case "cites":
colour = "green";
break;
case "selected":
colour = "magenta";
break;
default:
colour = "black";
}
return colour;
})
.on("mouseover", mouseHandler)
.on("mouseout", mouseoutHandler)
.on("click", clickHandler)
.on("contextmenu", rightClickHandler);
svg.select(".y.axis").call(yAxis);
svg.select(".x.axis").call(xAxis);
//don't want dots overlapping axis, so add in buffer to data domain
x.domain([d3.min(allData, YearFn)-1, d3.max(allData, YearFn)+1]);
y.domain([d3.min(allData, Num_citationsFn)-1, d3.max(allData, Num_citationsFn)+1]);
}
Any help is much appreciated I am new to d3 so thanks in advance!
You don't need to re-plot all the data each time a single point changes. Just update that one point.
function rightClickHandler() {
// if option 1
d3.select(this).style("fill", "blue");
// if option 2
d3.select(this).remove();
}
Your problem likely arises because when you call refreshGraph a second time (or third) your aren't clearly the circles that are already plotted. Your refreshGraph function isn't updating the points already plotted it's recreating them each time, and if you aren't clearing the points that are already there, you won't see the new points (or the absence of them, or the change in color), because they are hidden behind your old points.
EDIT:
If you want to re-add the data each time, you first have to clear the existing data. At the start of your refreshGraph function, add this line:
if(!d3.selectAll("circle").empty()) d3.selectAll("circle").remove();
i.e. if there are circle elements, remove them. This assumes you are only creating circle elements within the refreshGraph function. If you create them elsewhere, the you should probably use the .dot selector instead.

Categories