I have a D3 bar chart with the associated data points displayed as text on top of each bar. I want to display the text only on mouseover and also make the bar have a different fill color. So, essentially, on mouseover, the bar has to be styled to have a different fill color and the text opacity should go to 1 (from '0').
I am having trouble effecting two separate events on mouseover. I have given an index_value attribute to both elements in order to use d3.select(this).attr(index_value). But my mouseover function does not work. I have no idea why. Here's my relevant code section.
The bar chart
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr('data-value', function(d){return d[region]})
.attr("x", function(d) { return x(d.year); })
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d[region]); })
.attr("height", function(d) { return height - y(d[region]); })
.attr("fill", color)
.attr("index_year", function(d, i) { return "index-" + d.year; })
.attr("class", function(d){return "bar " + "bar-index-" + d.year;})
.attr("color_value", color)
.on('mouseover', synchronizedMouseOver)
.on("mouseout", synchronizedMouseOut);
The text overlay
svg.selectAll(".bartext")
.data(data)
.enter()
.append("text")
.attr("text-anchor", "middle")
.attr("x", function(d,i) {
return x(d.year)+x.rangeBand()/2;
})
.attr("y", function(d,i) {
return height - (height - y(d[region])) - yTextPadding;
})
.text(function(d){
return d3.format(prefix)(d3.round(d[region]));
})
.attr("index_year", function(d, i) { return "index-" + d.year; })
.attr("class", function(d){return "bartext " + "label-index-" + d.year;})
.on("mouseover", synchronizedMouseOver)
.on("mouseout", synchronizedMouseOut);
And the mouseover function
var synchronizedMouseOver = function() {
var bar = d3.select(this);
console.log(bar);
var indexValue = bar.attr("index_year");
var barSelector = "." + "bar " + "bar-" + indexValue;
var selectedBar = d3.selectAll(barSelector);
selectedBar.style("fill", "#f7fcb9");
var labelSelector = "." + "bartext " + "label-" + indexValue;
var selectedLabel = d3.selectAll(labelSelector);
selectedLabel.style("opacity", "1");
};
This can be achieved by simplifying your listeners. You don't need to add listeners to both rects and text. Just add them to the rects. Here are the simplified listeners:
function synchronizedMouseOver(d) {
var bar = d3.select(this)
.style("fill","red");
var text = d3.select(".label-index-" + d.year)
.style("opacity","1");
};
function synchronizedMouseOut(d) {
var bar = d3.select(this)
.style("fill",color);
var text = d3.select(".label-index-" + d.year)
.style("opacity","0");
};
Your two friends here are this and d, the DOM element for the rect and its data node, respectively.
Here is a FIDDLE with the behavior that you desire.
Related
my coding is to plot x axis with location, y axis with (value1,value2 or value3) and legend with types(high, medium,low). what I'm trying to do is to add menu with value1,2,3 and add legend with different types so if I change from either menu or click on legend, plot got updated with only selected data.
however, my code below is only able to create legend set as default type or clicked but not able to include all types. is there any way to include all types in legends constantly no matter what type is clicked and only update chart accordingly?
thank you,
<script>
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960- margin.left - margin.right,
height = 900 - margin.top - margin.bottom,
radius = 3.5,
padding = 1,
xVar = "location",
cVar= " type";
default = "high";
// add the tooltip area to the webpage
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// force data to update when menu is changed
var menu = d3.select("#menu select")
.on("change", change);
// load data
d3.csv("sample.csv", function(error, data) {
formatted = data;
draw();
});
// set terms of transition that will take place
// when new indicator from menu or legend is chosen
function change() {
//remove old plot and data
var svg = d3.select("svg");
svg.transition().duration(100).remove();
//redraw new plot with new data
d3.transition()
.duration(750)
.each(draw)
}
function draw() {
// add the graph canvas to the body of the webpage
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 + ")")
// setup x
var xValue = function(d) { return d[xVar];}, // data -> value
xScale = d3.scale.ordinal()
.rangeRoundBands([0,width],1), //value -> display
xMap = function(d) { return (xScale(xValue(d)) + Math.random()*10);}, // data -> display
xAxis = d3.svg.axis().scale(xScale).orient("bottom");
// setup y
var yVar = menu.property("value"),
yValue = function(d) { return d[yVar];}, // data -> value
yScale = d3.scale.linear().range([height, 0]), // value -> display
yMap = function(d) { return yScale(yValue(d));}, // data -> display
yAxis = d3.svg.axis().scale(yScale).orient("left");
// setup fill color
var cValue = function(d) { return d[cVar];},
color = d3.scale.category10();
// filter the unwanted data and plot with only chosen dataset.
data = formatted.filter(function(d, i)
{
if (d[cVar] == default)
{
return d;
}
});
data = formatted;
// change string (from CSV) into number format
data.forEach(function(d) {
d[xVar] = d[xVar];
d[yVar] = +d[yVar];
});
xScale.domain(data.sort(function(a, b) { return d3.ascending(a[xVar], b[xVar])})
.map(xValue) );
// don't want dots overlapping axis, so add in buffer to data domain
yScale.domain([d3.min(data, yValue)-1, d3.max(data, yValue)+1]);
// x-axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text(xVar);
// y-axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text(yVar);
// draw dots
var dot = svg.selectAll(".dot")
.data(data)
.enter()
.append("circle")
.attr("class", "dot")
.attr("r", radius)
.attr("cx", xMap)
.attr("cy", yMap)
.style("fill", function(d) { return color(cValue(d));})
.on("mouseover", function(d) {
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(d[SN] + "<br/> (" + xValue(d)
+ ", " + yValue(d) + ")")
.style("left", (d3.event.pageX + 5) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
// draw legend
var legend = svg.selectAll(".legend")
.data(color.domain().slice())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
// draw legend colored rectangles
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color)
.on("click", function (d){
default = d;
return change();
});
// draw legend text
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d;})
.on("click", function (d) {
default = d;
return change();
});
};
</script>
</body>
sample.csv
location type value1 value2 value3
A high 1 -2 -5
B medium 2 3 4
C low 4 1 2
C medium 6 3 4
A high 4 5 6
D low -1 3 2
I found a way to include all types in the legend.
first, extract unique types from column "type" and save them in the "legend_keys" as array. second, instead of pre-define "default", set the first type in the "legend_keys" as a default. but next default will be set by the event on click out of legend.
d3.csv("sample.csv", function(error, data) {
formatted = data;
var nest = d3.nest()
.key(function(d) { return d[cVar]; })
.entries(formatted);
console.log(nest);
legend_keys = nest.map(function(o){return o.key});
default = legend_keys[0];
//console.log(legend_keys[0]);
draw();
});
Finally, when define the legend, read "legend_keys" as data as below.
By doing this, I can always keep all types in the legend.
var legend = svg.selectAll(".legend")
.data(legend_keys)
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; })
.on("click", function (d){
default = d;
console.log(default);
return change();
});
I am trying to plot a network graph using networkD3 in R. I wanted to make some changes to the display so that the text labels (which appears when mouseover) can be easily read.
Please refer to the link here for an example. Note: Jump to the d3ForceNetwork plot.
As seen in the example, the labels are hard to read due to its colour and it often gets obstructed by the surrounding nodes. I have been messing around with the JS file and managed to change the text label color to black. However, having no knowledge of JS or CSS (I can't even tell the difference between the 2 actually), I have no idea how I can change the stack order such that the text labels will always be displayed above any other objects.
Can anyone advise me on how I can achieve the desired outcome?
Below is the full JS file:
HTMLWidgets.widget({
name: "forceNetwork",
type: "output",
initialize: function(el, width, height) {
d3.select(el).append("svg")
.attr("width", width)
.attr("height", height);
return d3.layout.force();
},
resize: function(el, width, height, force) {
d3.select(el).select("svg")
.attr("width", width)
.attr("height", height);
force.size([width, height]).resume();
},
renderValue: function(el, x, force) {
// Compute the node radius using the javascript math expression specified
function nodeSize(d) {
if(options.nodesize){
return eval(options.radiusCalculation);
}else{
return 6}
}
// alias options
var options = x.options;
// convert links and nodes data frames to d3 friendly format
var links = HTMLWidgets.dataframeToD3(x.links);
var nodes = HTMLWidgets.dataframeToD3(x.nodes);
// get the width and height
var width = el.offsetWidth;
var height = el.offsetHeight;
var color = eval(options.colourScale);
// set this up even if zoom = F
var zoom = d3.behavior.zoom();
// create d3 force layout
force
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(options.linkDistance)
.charge(options.charge)
.on("tick", tick)
.start();
// thanks http://plnkr.co/edit/cxLlvIlmo1Y6vJyPs6N9?p=preview
// http://stackoverflow.com/questions/22924253/adding-pan-zoom-to-d3js-force-directed
var drag = force.drag()
.on("dragstart", dragstart)
// allow force drag to work with pan/zoom drag
function dragstart(d) {
d3.event.sourceEvent.preventDefault();
d3.event.sourceEvent.stopPropagation();
}
// select the svg element and remove existing children
var svg = d3.select(el).select("svg");
svg.selectAll("*").remove();
// add two g layers; the first will be zoom target if zoom = T
// fine to have two g layers even if zoom = F
svg = svg
.append("g").attr("class","zoom-layer")
.append("g")
// add zooming if requested
if (options.zoom) {
function redraw() {
d3.select(el).select(".zoom-layer").attr("transform",
"translate(" + d3.event.translate + ")"+
" scale(" + d3.event.scale + ")");
}
zoom.on("zoom", redraw)
d3.select(el).select("svg")
.attr("pointer-events", "all")
.call(zoom);
} else {
zoom.on("zoom", null);
}
// draw links
var link = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("class", "link")
.style("stroke", function(d) { return d.colour ; })
//.style("stroke", options.linkColour)
.style("opacity", options.opacity)
.style("stroke-width", eval("(" + options.linkWidth + ")"))
.on("mouseover", function(d) {
d3.select(this)
.style("opacity", 1);
})
.on("mouseout", function(d) {
d3.select(this)
.style("opacity", options.opacity);
});
// draw nodes
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.style("fill", function(d) { return color(d.group); })
.style("opacity", options.opacity)
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", click)
.call(force.drag);
node.append("circle")
.attr("r", function(d){return nodeSize(d);})
.style("stroke", "#fff")
.style("opacity", options.opacity)
.style("stroke-width", "1.5px");
node.append("svg:text")
.attr("class", "nodetext")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name })
.style("font", options.fontSize + "px " + options.fontFamily)
.style("opacity", options.opacityNoHover)
.style("pointer-events", "none");
function tick() {
node.attr("transform", function(d) {
if(options.bounded){ // adds bounding box
d.x = Math.max(nodeSize(d), Math.min(width - nodeSize(d), d.x));
d.y = Math.max(nodeSize(d), Math.min(height - nodeSize(d), d.y));
}
return "translate(" + d.x + "," + d.y + ")"});
link
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
}
function mouseover() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", function(d){return nodeSize(d)+5;});
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 13)
.style("stroke-width", ".5px")
.style("font", options.clickTextSize + "px ")
.style('fill', 'black')
.style('position','relative')
.style("opacity", 1);
}
function mouseout() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", function(d){return nodeSize(d);});
d3.select(this).select("text").transition()
.duration(1250)
.attr("x", 0)
.style("font", options.fontSize + "px ")
.style("opacity", options.opacityNoHover);
}
function click(d) {
return eval(options.clickAction)
}
// add legend option
if(options.legend){
var legendRectSize = 18;
var legendSpacing = 4;
var legend = svg.selectAll('.legend')
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * color.domain().length / 2;
var horz = legendRectSize;
var vert = i * height+4;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', color)
.style('stroke', color);
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.style('fill', 'darkOrange')
.text(function(d) { return d; });
}
// make font-family consistent across all elements
d3.select(el).selectAll('text').style('font-family', options.fontFamily);
},
});
I suspect I need to make some changes to the code over here:
function mouseover() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", function(d){return nodeSize(d)+5;});
d3.select(this).select("text").transition()
.duration(750)
.attr("x", 13)
.style("stroke-width", ".5px")
.style("font", options.clickTextSize + "px ")
.style('fill', 'black')
.style("opacity", 1);
}
You need to resort the node groups holding the circles and text so the currently mouseover'ed one is the last in that group, and thus the last one drawn so it appears on top of the others. See the first answer here -->
Updating SVG Element Z-Index With D3
In your case, if your data doesn't have an id field you may have to use 'name' instead as below (adapted to use the mouseover function you've got):
function mouseover(d) {
d3.selectAll("g.node").sort(function (a, b) {
if (a.name != d.name) return -1; // a is not the hovered element, send "a" to the back
else return 1; // a is the hovered element, bring "a" to the front (by making it last)
});
// your code continues
The pain might be that you have to do this edit for every d3 graph generated by this R script, unless you can edit the R code/package itself. (or you could suggest it to the package author as an enhancement.)
I am creating the legends with triangle shapes. One is "Yes", the other one is "No". By running the code below, it generate two triangles but they are overlapping. I am trying to seperate them by using this line of code .attr("y", function(d,i) {return 50+i*40;}) but seems like it doesn't work.
Can anyone tell me how to fix it? Thanks!
Click here! This is an html sreenshot for this part of script
var legendname = ["Yes","No"];
var legend = svg.selectAll(".legend")
.data(legendname)
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(" + (w + 150) + "," + (m.t - 30) + ")";
});
legend.append("path")
.attr("d", d3.svg.symbol().type("triangle-up").size(128))
*** .attr("y", function(d,i) {return 50+i*40;})
.style('fill', function(d) {return color(d);});
legend.append("text")
.attr("y", function(d,i) {return 50+i*20;})
.attr("x", 30)
.text(function(d) { return d; })
You will have to update the translate y attribute of groups instead of the paths. And also there is no need for extra calculations for y attributes of texts and paths then.
.attr("transform", function(d, i) {
return "translate(" + (w + 150) + "," + (30+i*40) + ")";
});
Working Code Snippet:
var w=40; //Sample chart width
var color = d3.scale.category20c();
var svg = d3.select("body").append("svg").attr({ height: 500, width: 400 });
var legendname = ["Yes", "No"];
var legend = svg.selectAll(".legend")
.data(legendname)
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(" + (w + 150) + "," + (30+i*40) + ")";
});
legend.append("path")
.attr("d", d3.svg.symbol().type("triangle-up").size(128))
.style('fill', function(d,i) {
return color(i);
});
legend.append("text")
.attr("dx",10)
.attr("dy",".4em")
.text(function(d) {
return d;
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I've got this linechart, on each value in the chart I am placing a dot.
when hovering over the dot I would like to show the value using a d3-tip tooltip.
Here is what I got so far:
var svg = chart.append("svg")
.attr("width", outerWidth)
.attr("height", outerHeight)
.append("g")
.attr("transform", "translate(0,10)");
newData.graphSeries.forEach(function (current, index, all) {
//current = this exact part of the array
//index = the array index nr [0][1][2] etc
//all = the complete array
var graph = current.Values,
color = current.Color;
var nextLine = d3.svg.line()
.interpolate(current.Interpolate)
.x(function (d) {
return x(d.x);
})
.y(function (d) {
return y(d.y);
});
svg.append("path")
.datum(graph)
.attr("transform", "translate(" + yAxisWidth + ",0)")
.attr("class", "line stroke-" + color)
.attr("d", nextLine);
//Placing tooltips
if (current.Tooltips == true) {
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "<strong> TEST: " + newData.y.Unit + "</strong><span>" + d.x + "</span>"
});
//create circles to place the tooltip on
svg.selectAll('dot')
.data(graph)
.enter().append("circle")
.attr("r", 3,5)
.attr("style", "cursor: pointer")
.attr("class", "circle-" + color)
.attr("transform", "translate(" + yAxisWidth + ",0)")
.attr("cx", function(d) { return x(d.x) })
.attr("cy", function(d) { return y(d.y) })
.on('mouseover', tip.show )
.on('mouseout', tip.hide);
svg.call(tip);
}
});
I checked if d3-tip exists in the code and it does.
I can console.log the tip variable, also the dots are showing and even the mouseover and mouseout are working correctly.
Still somehow tip.show doesn't seem to work.
I thought maybe it would show somewhere else in the document, but can't see it anywhere.
Could you please help out.
Best,
Bart
The problem was actually easier to solve then expected.
The tooltip might be 'pushed' away by all other html code.
Adding .style('z-index', '99999999999'); will help to get that straight.
See:
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.style('z-index', '99999999')
.html(function(d) {
return "<strong> TEST: " + newData.y.Unit + "</strong><span>" + d.x + "</span>"
});
I'm building a bubble chart from d3js but keep receiving a TypeError: undefined is not a function with the enter() method. I've tried just about everything and I can't determine why this error is being produced besides the fact that the filter method is returning null. Currently the bubble chart itself is not displayed.
var diameter = 310,
format = d3.format(",d"),
color = d3.scale.category20c();
var bubble = d3.layout.pack()
.sort(null)
.size([diameter, diameter])
.padding(1.5);
var svg = d3.select("bubble")
.append("svg")
.attr("width", diameter)
.attr("height", diameter)
.attr("class", "bubble");
var node = svg.selectAll(".node")
.data(bubble.nodes({children: [{packageName: "food", className: "food", value: 100}]}))
.filter(function(d) { return !d.children; })
// ================================
// This is causing the error below
// ================================
.enter()
.append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
node.append("title")
.text(function(d) { return d.className + ": " + format(d.value); });
node.append("circle")
.attr("r", function(d) { return d.r; })
.style("fill", function(d) { return color(d.packageName); });
node.append("text")
.attr("dy", ".3em")
.style("text-anchor", "middle")
.text(function(d) { return d.className.substring(0, d.r / 3); });
d3.select(self.frameElement).style("height", diameter + "px");
JsFiddle: http://jsfiddle.net/26Tra/
In your data binding, you are using keys such as packageName and className that are generated by the classes function, which is missing from your code. I added it and now your data binding is correct:
var node = svg.selectAll(".node")
.data(bubble.nodes(classes(root)).filter(function (d) {return !d.children;}))
.enter()
...
NOTE: because I mocked some very simple data, with only one packageName, fruits, I used className instead of packageName for the coloring. I also changed the color category to category10() to give it more contrast.
Complete FIDDLE.