How to add popup on D3.js visualization (tree nodes) - javascript

We are using d3.js tree (d3.tree()) to render org chart visualization. It is very similar to
https://observablehq.com/#julienreszka/d3-v5-org-chart
I want to display small popup over the node on click/mouseover of some button. e.g. clicking on person image display small popup with some more actions. So user can click on any of the link in popup.
Is there any recommended approach to achieve it in D3js?

You can attach the event listener for click or mouseover when you update Nodes. For instance, the example you mentioned has this piece of code:
// Updating nodes
const nodesSelection = centerG.selectAll('g.node')
.data(nodes, d => d.id)
// Enter any new nodes at the parent's previous position.
var nodeEnter = nodesSelection.enter().append('g')
.attr('class', 'node')
.attr("transform", function(d) {
return "translate(" + source.x0 + "," + source.y0 + ")";
})
.attr('cursor', 'pointer')
.on('click', function(d) {
if ([...d3.event.srcElement.classList].includes('node-button-circle')) {
return;
}
attrs.onNodeClick(d.data.nodeId);
})
If you check the onclick here, it is calling the NodeClick method; you will need to change NodeClick or, if you want a mouseover method, add an .on('mouseover') event. If you want to target the image in node, then add the event at this place:
nodeUpdate.selectAll('.node-image-group')
.attr('transform', d => {
let x = -d.imageWidth / 2 - d.width / 2;
let y = -d.imageHeight / 2 - d.height / 2;
return `translate(${x},${y})`
})
nodeUpdate.select('.node-image-rect')
.attr('fill', d => `url(#${d.id})`)
.attr('width', d => d.imageWidth)
.attr('height', d => d.imageHeight)
.attr('stroke', d => d.imageBorderColor)
.attr('stroke-width', d => d.imageBorderWidth)
.attr('rx', d => d.imageRx)
.attr('y', d => d.imageCenterTopDistance)
.attr('x', d => d.imageCenterLeftDistance)
.attr('filter', d => d.dropShadowId)

Related

r2d3 tree missing nodes and path. Only root node appears with no labels

Maybe I bit off more than I can chew as someone who has a vague memory of only seeing D3... but here I go. I am using R shiny and r2d3. I've copy pasted some very basic examples of r2d3 from here (https://rstudio.github.io/r2d3/) to kind of feel out how to actually incorporate any d3 into r/ 'kicking the tires'. For what I'm currently working with and what I actually want to modify for r2d3 is this here ->(https://observablehq.com/#d3/tree). I've made a few modifications just playing around with it, as pasted below. Now it comes to actually modifying it to be used for r2d3.
I know that the .js file already includes the svg, data which was passed into the function, width, height, options, theme. So I did my best to remove all those things, but it could be I missed it because I don't exactly know what Im looking at for each of these lines. Now I'm at a point where something is displaying; however, it is just what I believe to be only the root node circle with no labels. When the node is grey, it means it has no children, so this lonely node appearing has no children. Also when I hover my mouse it does recognize a mouseover and change in color. This makes me believe I'm missing something really small and dumb - At least I hope that is the case.
I inspected the element. I see an error that says
Uncaught TypeError: d3.select(...).append(...).duration is not a function
r2d3-script-454:192 at SVGCircleElement.
So I look this up and I come across this is stack overflow (Functions not recognised when using R2D3). I realize I don't actually know which of these functions need a dependency or how to find that out. If it is a dependency that I need to resolve this, I don't know what the URL would be or how to figure that out.
Server.R
library(shiny)
library(r2d3)
# download.file("https://d19vzq90twjlae.cloudfront.net/leaflet-0.7/leaflet.js", "leaflet-0.7.js")
# download.file("https://cdn.jsdelivr.net/gh/holtzy/D3-graph-gallery#master/LIB/d3-scale-radial.js", "d3-scale-radial.js")
# download.file("https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.7.1/d3-tip.min.js", "d3-tip.min.js")
server <- function(input, output) {
output$d3 <- renderD3({
r2d3(
runif(5, 0, input$bar_max),
script = system.file("examples/baranims.js", package = "r2d3")
)
})
output$d3dendrogram <- renderD3({
r2d3(
data = read.csv("flare.csv"),
d3_version = 4,
script = "r2d3dendrogram.js",
dependencies = list("leaflet-0.7.js",
"d3-scale-radial.js",
"d3-tip.min.js"))
})
}
ui.R
ui <- fluidPage(
inputPanel(
sliderInput("bar_max", label = "Max:",
min = 0, max = 1, value = 1, step = 0.05)
),
# d3Output("d3"),
d3Output("d3dendrogram")
)
r2d3dendrogram.js
//hardcoded variables
dy = 192
dy = width/6
dx = 30
tree = d3.tree().nodeSize([dx, dy])
margin = ({top: 10, right: 120, bottom: 10, left: 40})
diagonal = d3.linkHorizontal().x(d => d.y).y(d => d.x)
const root = d3.hierarchy(data);
root.x0 = dy / 2;
root.y0 = 0;
root.descendants().forEach((d, i) => {
d.id = i;
d._children = d.children;
if (d.depth && d.data.name.length !== 7) d.children = null;
});
const gLink = svg.append("g")
.attr("fill", "none")
.attr("stroke", "#555") //color of the tree link branches
.attr("stroke-opacity", 0.4)
.attr("stroke-width", 1.5);
const gNode = svg.append("g")
.attr("cursor", "pointer") //changes mouse into the pointing hand
.attr("pointer-events", "all");
//creates the div element to appear on hovering on a node
var div = d3.select("body").append("div")
//.attr("class", "tooltip")
.style("position", "absolute")
.style("text-align", "center")
.style("background", "black")
.style("border-radius", "8px")
.style("border", "solid 1px green")
.style("opacity", 0);
//controls the display that shows up on hovering on a node
function mouseover(d){
div.html("hello" + "<br/>"+"Everyone")
.style("left", (d3.event.pageX + 10) + "px")
.style("top", (d3.event.pageY - 15) + "px")
.style("opacity",1);
}
//controls how the div element from the hover acts when mouse goes away
function mouseout(){
div.style("opacity", 1e-6);
}
d3.selectAll("circle")
.on("mouseover", mouseover)
.on("mouseover", mouseout);
function update(source) {
const duration = d3.event && d3.event.altKey ? 2500 : 550;
const nodes = root.descendants().reverse();
const links = root.links();
// Compute the new tree layout.
tree(root);
let left = root;
let right = root;
root.eachBefore(node => {
if (node.x < left.x) left = node;
if (node.x > right.x) right = node;
});
const height = right.x - left.x + margin.top + margin.bottom;
const transition = svg.transition()
//.duration(duration)
.attr("viewBox", [-margin.left, left.x - margin.top, width, height])
.tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle"));
// Update the nodes…
const node = gNode.selectAll("g")
.data(nodes, d => d.id);
// Enter any new nodes at the parent's previous position.
const nodeEnter = node.enter().append("g")
.attr("transform", d => `translate(${source.y0},${source.x0})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0)
.on("click", (event, d) => {
d.children = d.children ? null : d._children;
update(d)
})
//adds on the tooltip for the info that shows up on the hover
//content inside
nodeEnter.append("circle")
.attr("r", 6.5)
.attr("fill", d => d._children ? "#64B4FF" : "#999") /*creates the blue color or the grey color*/
.attr("stroke-width", 10)
.attr('transform', 'translate(0, 0)')
.on('mouseover', function (d, n, i) {
//Below is the hover feature for the nodes to change light blue
d3.select(this).transition()
.duration('10')
.attr('fill', '#E2F1FF') //fill color to be light blue
//Makes the new div appear on hover:
d3.select(this).append("div")
. duration('50')
.style("opacity", 1)
})
.on('mouseout', function (d,i) {
d3.select(this).transition()
//.duration('500')
.attr("fill", d => d._children ? "#64B4FF" : "#999"); /*creates the blue color || grey color*/
//Makes the new div disappear:
div.transition()
.duration('50')
.style("opacity", 1)
});
nodeEnter.append("text")
.attr("dy", "0.31em")
.attr("x", d => d._children ? -6 : 6)
.attr("text-anchor", d => d._children ? "end" : "start")
.text(d => d.data.name)
.clone(true).lower()
.attr("stroke-linejoin", "round")
.attr("stroke-width", 3)
.attr("stroke", "white");
// Transition nodes to their new position.
const nodeUpdate = node.merge(nodeEnter).transition(transition)
.attr("transform", d => `translate(${d.y},${d.x})`)
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1);
// Transition exiting nodes to the parent's new position.
const nodeExit = node.exit().transition(transition).remove()
.attr("transform", d => `translate(${source.y},${source.x})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0);
// Update the links…
const link = gLink.selectAll("path")
.data(links, d => d.target.id);
// Enter any new links at the parent's previous position.
const linkEnter = link.enter().append("path")
.attr("d", d => {
const o = {x: source.x0, y: source.y0}
return diagonal({source: o, target: o});
});
// Transition links to their new position.
link.merge(linkEnter).transition(transition)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition(transition).remove()
.attr("d", d => {
const o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
});
// Stash the old positions for transition.
root.eachBefore(d => {
d.x0 = d.x;
d.y0 = d.y;
});
}
update(root);
return svg.node();
Sidenote: I know d3 is huge. If anyone can offer any tips or anything else to learn about all of this (free would be a nice plus), let me know. I've watched some sections of a 19hr video online with Curran. It was somewhat useful, but this is some learning curve for me; I had to re-watch a few things over and over again.
Sidenote: Sorry, please ignore the slider bar. It shouldn't do anything.
edit - adding the javascript tag in case this is requires a javascript related solution

D3 v4 graph not updating bars - but appending new axis data

I have seen this type of question asked a lot but no answers i found solved it for me.
I created a fiddle here of a stripped down simplified version of my code
https://jsfiddle.net/00m2cv84/4/
here is a snippet of the ganttish object - best to check out the fiddle though for context
const ganttish = function (data) {
this.dataSet = data
this.fullWidth = +document.getElementById('chart').clientWidth
this.fullHeight = 700
this.pad = { top: 50, right: 50, bottom: 50, left: 50 }
this.h = this.fullHeight - this.pad.top - this.pad.bottom
this.w = this.fullWidth - this.pad.left - this.pad.right
this.svg = d3.select('#chart')
.append('svg')
.attr('width', this.fullWidth)
.attr('height', this.fullHeight)
}
ganttish.prototype = {
redraw (newDataSet) {
this.dataSet = newDataSet // this should overwrite the old data
this.draw()
},
draw () {
let y = d3.scaleBand()
.padding(0.1)
.range([0, this.h])
y.domain(this.dataSet.map(d => d.idea_id))
let x = d3.scaleTime().range([this.pad.left, this.w + this.pad.left])
x.domain([
d3.min(this.dataSet, d => new Date(Date.parse(d.start_date))),
d3.max(this.dataSet, d => new Date(Date.parse(d.end_date)))
])
let xAxis = d3.axisBottom(x).tickSize(-(this.h), 0, 0)
let yAxis = d3.axisLeft(y).tickSize(-(this.w), 0, 0)
let xA = this.svg.append('g')
.attr('transform', 'translate(0,' + this.h + ')')
.attr('class', 'x axis')
.call(xAxis)
let yA = this.svg.append('g')
.attr('transform', 'translate(' + this.pad.left + ', 0)')
.attr('class', 'y axis')
.call(yAxis)
let timeBlocks = this.svg.selectAll('.timeBlock')
.data(this.dataSet)
let tbGroup = timeBlocks
.enter()
.append('g')
tbGroup
.append('rect')
.attr('class', 'timeBlock')
.attr('rx', 5)
.attr('ry', 5)
.attr('x', d => x(new Date(Date.parse(d.start_date))))
.attr('y', d => y(d.idea_id))
.attr('width', d => parseInt(x(new Date(Date.parse(d.end_date))) - x(new Date(Date.parse(d.start_date)))))
.attr('height', d => y.bandwidth())
.style('fill', (d) => {
return d.color ? d.color : 'rgba(123, 173, 252, 0.7)'
})
.style('stroke', 'black')
.style('stroke-width', 1)
tbGroup
.append('text')
.attr('class', 'timeBlockText')
.attr('x', d => x(new Date(Date.parse(d.start_date))) + 10)
.attr('y', d => y(d.idea_id) + (y.bandwidth() / 2))
.attr('alignment-baseline', 'middle')
.attr('font-size', '1em')
.attr('color', 'black')
.text(d => d.name)
/**
I have literally tried exit().remove() on pretty much everything i could think of :(
this.svg.exit().remove()
timeBlocks.exit().remove()
this.svg.selectAll('.timeBlock').exit().remove()
this.svg.selectAll('.timeBlockText').exit().remove()
this.svg.select('.x').exit().remove()
this.svg.select('.y').exit().remove()
*/
}
Sidenote:
I have a Vue js application and i'm implementing a Gantt(ish) style horizontal bar chart. In the fiddle i have simulated the Vue part by creating an Object which then you call the redraw method on it, this pattern simulates a watcher in the component updating the dataset when the parent data changes. The issue i face is the same.
issue:
When i change the data to the chart it does not update my chart rects or text. It does however append the new axis' on top of the old ones.
i understand that i should be calling .exit().remove() on anything .enter() 'd when finished in order to clear them for the next data push, but everywhere i try this it fails. I can get it to work by creating a fresh svg on every draw, but i understand i won't be able to do any transitions - and it seems a very bad approach :)
What is twisting my noodle is that if i push extra data to the new data object, it appends it fine, once - then does not do it again. It seems once element X in the data array has been added it will not update again.
https://jsfiddle.net/00m2cv84/5/
I know that the this.dataSet is updating, it just does not seem to be accepted by D3
Any help would be greatly appreciated for a D3 noob :)
You're problem is that you're not handling your data join properly. I'd highly recommend reading some of Mike Bostock's examples probably starting with Three Little Circles. To summarize though:
D3 joins created by calling .data() contain 3 selections:
Enter (DOM elements that need to be created as they exist in the data but don't yet exist in the DOM)
Exit (DOM elements that need to be removed, as they're not longer represented in the data)
Update (DOM elements that already exist and still exist in the data)
Within your code you're handling the enter() selection with this bit of code:
let tbGroup = timeBlocks
.enter()
.append('g')
The problem is you're not handling any of the other selections. The way I'd go about doing this is:
let join = this.svg.selectAll('.timeBlock').data(this.dataSet);
// Get rid of any old data
join.exit().remove();
// Create the container groups. Note that merge takes the "new" selection
// and merges it with the "update" selection. We'll see why in a minute
const newGroups = join.enter()
.append('g')
.merge(join);
// Create all the new DOM elements that we need
newGroups.append("rect").attr("class", "timeBlock");
newGroups.append('text').attr('class', 'timeBlockText');
// We need to set values for all the "new" items, but we also want to
// reflect any changes that have happened in the data. Because we used
// `merge()` previously, we can access all the "new" and "update" items
// and set their values together.
join.select(".timeBlock")
.attr('rx', 5)
.attr('ry', 5)
.attr('x', d => x(new Date(Date.parse(d.start_date))))
.attr('y', d => y(d.idea_id))
...
join.select(".timeBlockText")
.attr('x', d => x(new Date(Date.parse(d.start_date))) + 10)
.attr('y', d => y(d.idea_id) + (y.bandwidth() / 2))
.attr('alignment-baseline', 'middle')
.attr('font-size', '1em')
.attr('color', 'black')
.text(d => d.name)

Fixed nodes not being fixed (d3 Force Directed Graph)

I am setting the nodes to be fixed with
let link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line")
.attr("stroke-width", () => 4)
let node = svg.append('g')
.attr("class", "nodes")
.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.call(
d3.drag()
.on("start", Node.dragstarted)
.on("drag", Node.dragged)
.on("end", Node.dragended))
node.append("title")
.text(d => d.country)
node.append('image')
.attr('xlink:href', d => 'https://rawgit.com/hjnilsson/country-flags/master/svg/' + d.code + '.svg')
.attr('height', d => 2.5 * (area[d.code]||5))
.attr('width', d => 4 * (area[d.code])||5)
simulation
.nodes(graph.nodes.map(c => {
if(latlong[c.code] === undefined) {
console.log(c,'missing lat/long data')
return c
}
c.x = (+latlong[c.code][0])
c.y = (+latlong[c.code][1])
c.fixed = true
return c
}))
.on("tick", ticked)
This does correctly display images in apparently different locations than without the x and y values, but .. the fixed property isn't working.
Here's my code: codepen
If anyone also knows how I can set the canvas up so that nothing escapes out the top or bottom I'd appreciate that as well.
d3.js v4 does not have a fixed property. Instead, you need to set the nodes fx and fy attributes. Note, your drag functions are already doing this.
.nodes(graph.nodes.map(c => {
if(latlong[c.code] === undefined) {
console.log(c,'missing lat/long data')
return c
}
c.fx = c.x = (+latlong[c.code][0])
c.fy = c.y = (+latlong[c.code][1])
return c
}))
https://codepen.io/anon/pen/ZKXmVe

Updating bar chart in d3 generating multiple charts

I'm following the given tutorial on D3
bar chart -2
I've setup my code in two functions one is init and one is update
var xScale = null;
var chart = null;
function init(w, c) {
xScale = d3.scale.linear()
.range([0, w]);
chart = d3.select(c)
.append('svg')
.attr('width', w);
function update(data) {
xScale.domain([0, d3.max(data, function(d) { return +d.value; })]);
chart.attr('height', 20 * data.length);
var bars = chart.selectAll('g')
.data(data);
bars.exit().remove();
bars.enter().append('g')
.attr('transform', function(d, i) { return 'translate(0,' + i * 20 + ')'; });
bars.append('rect')
.attr('width', function(d) { return xScale(+d.value); })
.attr('height', 18);
bars.append('text')
.attr('x', function(d) { return xScale(+d.value); })
.attr('y', 10)
.attr('dy', '.45em')
.text(function (d) { return d.name; });
}
When I call update first time, the bar chart is created correctly, on subsequenet update calls, it creates rect and text elements under tags instead of updating
My data is a dict {'name': a, 'value': 12, .....} The number of elements per update can be different. There might be same keys(names) with different values in each update
bars = chart.selectAll('g')
You are selecting all of the g elements (both new and existing).
bars.append('rect');
bars.append('text');
As a result, when you call append on bars, you are appending rect and text elements to both the new and existing g elements.
/* Enter */
enter = bars.enter().append('g');
enter.append('rect');
enter.append('text');
/* Update */
bars.attr('transform', function(d, i) {
return 'translate(0,' + i * 20 + ')';
});
bars.select('rect')
.attr('width', function(d) { return xScale(+d.value); })
.attr('height', 18);
bars.select('text')
.attr('x', function(d) { return xScale(+d.value); })
.attr('y', 10)
.attr('dy', '.45em')
.text(function (d) { return d.name; });
This allows you to append rect and text elements only to the enter selection, yet still allows you to update all the elements using your new data.
Note:
The enter selection merges into the update selection when you append or insert. Rather than applying the same operators to the enter and update selections separately, you can now apply them only once to the update selection after entering the nodes.
See: https://github.com/mbostock/d3/wiki/Selections#data

d3js: Get hover overlay over multiple graphs

I have a set of graphs that can be dynamically added and removed from the page. Each one has an invisible 'rect' element appended to the base svg hosting each graph, and on that rect element I can append mouseover elements. However, these are all limited to the single svg/rect that the mouse is hovering over; I'd like to extend them to cover all visible graphs. Here's the main code affecting that:
var focus = svg.append('g') // An invisible layer over the top. Problem is, it only overlays on one graph at a time...
.style('display', 'none');
svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "none")
.style("pointer-events", "all")
.on("mouseover", function() { focus.style("display", null); })
.on("mouseout", function() { focus.style("display", "none"); })
.on("mousemove", mousemove);
// append the x line
focus.append("line")
.attr("class", "x")
.style("stroke", "blue")
.style("stroke-dasharray", "3,3")
.style("opacity", 0.5)
.attr("y1", 0)
.attr("y2", height);
function mousemove() {
var x0 = x.invert(d3.mouse(this)[0]),
i = bisectDate(dataset, x0, 1),
d0 = dataset[i - 1],
d1 = dataset[i],
d = x0 - d0.time > d1.time - x0 ? d1 : d0;
focus.select(".x")
.attr("transform", function() {
return "translate(" + x(d.time) + "," + rightDomain(symbol, d) + ")";
})
.attr("y2", height - y(d[symbol]));
}
All of this is inside a forEach() loop, where it loops over an array containing the names of the graphs to be shown, so multiple graphs (albeit in their separate svgs) show up.
I also have a plunker here: http://plnkr.co/edit/s4K84f5HGRjHFWMwiuIA?p=preview. I'm not sure why it's failing to work since I've copied and pasted my code, which I know works elsewhere.
Edit: I've managed to attach another svg element to the body but for some reason I can't get it to overlay on top of the existing svgs (the graphs). Here's my code (where I've tried several ways of adjusting the position):
var overlay = d3.select('html')
.append('div')
.attr('height', function() { return (symbols.length - 1) * 135 + 130; })
.attr('width', 1000)
.attr('z-index', 2)
//.attr('transform', 'translate(' + margin.left + ',' + extraBuffer/2 + ')');
//.attr('x', margin.left)
//.attr('y', extraBuffer/2);
.attr('position', 'absolute')
.attr('top', '20')
.attr('right', '40');
Looking at this in chrome devtools I always see it below existing graphs, even if I explicitly set its x/y values.

Categories

Resources