D3 Map - Context Menu - not able to update path attributes - d3.js

Hi I am trying to add markers, (or changing fill property is also fine) of the D3 map based on Context menu selection. I was referring to the examples at http://jsfiddle.net/1mo3vmja/2/, (pasting the code below) but after clicking the context menu item , I am not able to get the d3 element which invoked the context menu. Can someone help?
var fruits = ["Apple", "Orange", "Banana", "Grape"];
var svgContainer = d3.select("body")
.append("svg")
.attr("width", 200)
.attr("height", 200);
var circle = svgContainer
.append("circle")
.attr("cx", 30)
.attr("cy", 30)
.attr("r", 20)
.on('contextmenu', function(d,i) {
// create the div element that will hold the context menu
d3.selectAll('.context-menu').data([1])
.enter()
.append('div')
.attr('class', 'context-menu');
// close menu
d3.select('body').on('click.context-menu', function() {
d3.select('.context-menu').style('display', 'none');
});
// this gets executed when a contextmenu event occurs
d3.selectAll('.context-menu')
.html('')
.append('ul')
.selectAll('li')
.data(fruits).enter()
.append('li')
.on('click' , function(d) { console.log(d); return d; })
.text(function(d) { return d; });
d3.select('.context-menu').style('display', 'none');
// show the context menu
d3.select('.context-menu')
.style('left', (d3.event.pageX - 2) + 'px')
.style('top', (d3.event.pageY - 2) + 'px')
.style('display', 'block');
d3.event.preventDefault();
});

You can store the element on which you are drawing the context menu something like this:
.on('contextmenu', function(d, i) {
var me = this;//storing the circle instance inside variable me
Now when you select the context menu element you can refer the object me.
.on('click', function(d) {
console.log(d, me);
return d;
})
Working example here
Hope this helps!

Related

D3.js .each() with function that passes data

I would like to put my tooltip into a function such that I can re-use it for multiple elements. When I call the tooltip function for the label element the tooltip displays only the first x value to all labels instead of looping over the X value array. How do I properly access the data in the function?
const tooltip = d3.select('body').append('div')
.attr('id', 'rect-tooltip');
function mouseover(data-x){
d3.select('g')
area.selectAll("text")
.on('mouseover', (d) => {
rect-tooltip.transition()
.duration(100)
.style('opacity', .9)
rect-tooltip.html(`${data-x}`) //Pass in X-values
.style('left', `${d3.event.pageX + 10}px`)
.style('top', `${d3.event.pageY - 18}px`);
})
.on('mouseout', (d) => {
rect-tooltip.transition()
.duration(400)
.style('opacity', 0);
})
}
const label = d3.select('g')
area.selectAll("text")
.data(data)
.join('text')
.attr("class", "label")
.text( (d)=> {return d.name;})
.attr("x", (d)=> {return d.x;})
.attr("y", (d)=> {return d.y;})
.each(function(d) {
mouseover(d.x);}); // Only first data point is added to each label?
Without an example of the this, I may be mis-reading your issue.
Problem
The key problem stems from iterating through the text elements twice:
area.each(function (d) {
// do something with each element/datum in the selection
})
d3.selectAll("text")
.on("mouseover", function(d) {
// apply an event listner and corrsesponding function to each text element.
})
The problem is you nest the second in the first. For every element in area you select all the text elements: if you have 2 elements you're selecting all the text twice. You only need to select each text element once.
In the pattern you have, for each element in area we pass that element's datum to the nested function which takes a property of that datum and with d3.selectAll("text").on("mouseover" ... applies that single datum to all text mouseover events. Since you do this for every element in area, we end up overwriting the event listeners multiple times.
No where do you reference the current datum in the chain following d3.selectAll("text"), so we only have a value from the current datum in the current iteration of .each().
Solution
You shouldn't need to use .each() here to apply an event listener, .on() should be sufficient.
We have our mouseover and mouseout functions:
function mouseover(d) {
tooltip
.style("opacity", 0.9)
.text(d.x)
.style('left', `${d3.event.pageX + 10}px`)
.style('top', `${d3.event.pageY - 18}px`);
}
function mouseout() {
tooltip
.style("opacity",0);
}
Then we can call it with:
selection.on("mouseover",mouseover)
.on("mouseout",mouseout);
And we can resuse this on multiple selections or elements. The datum specific to each element will be used to define the tooltip text.
var tooltip = d3.select(".tooltip");
var svg = d3.select("svg");
var data = [{x: 10},{x:50},{x:90},{x:130},{x:170},{x:210},{x:250},{x:290},{x:330}]
var g = svg.selectAll(null)
.data(data)
.enter()
.append("g")
.attr("transform",function(d) { return "translate("+[d.x,0]+")"; })
var rect = g.append("rect")
.attr("width", 35)
.attr("height", 100)
.attr("fill","steelblue")
.on("mousemove",mouseover)
.on("mouseout",mouseout)
var text = g.append("text")
.attr("y", 120)
.attr("x", 18)
.style("text-anchor","middle")
.text(function(d) { return d.x; })
.on("mouseover",mouseover)
.on("mouseout",mouseout)
function mouseover(d) {
tooltip
.style("opacity", 0.9)
.text(d.x)
.style('left', `${d3.event.pageX + 10}px`)
.style('top', `${d3.event.pageY - 18}px`);
}
function mouseout() {
tooltip
.style("opacity",0);
}
.tooltip {
position: absolute;
padding: 5px;
background: yellow;
}
rect, text {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div class="tooltip"></div>
<svg width="400" height="300"></svg>
Based on Andrew his comment I changed my code to:
const tooltip = d3.select('body').append('div')
.attr('id', 'tooltip');
function mouseover(d){
tooltip.transition()
.duration(100)
.style('opacity', .9)
tooltip.html(d)
.style('left', `${d3.event.pageX + 10}px`)
.style('top', `${d3.event.pageY - 18}px`);
}
function mouseout(){
tooltip.transition()
.duration(400)
.style('opacity', 0);
}
const label = d3.select('g')
area.selectAll("text")
.data(data)
.join('text')
.attr("class", "label")
.text( (d)=> {return d.name;})
.attr("x", (d)=> {return d.x;})
.attr("y", (d)=> {return d.y;})
.on("mouseover", function(d) { mouseover(d.x); })
.on("mouseout", mouseout);

How can i display tooltip at the top of each vertical bar

I have tried writing couple of equations for the same but unabe to get it aligned well. I need to display tooltip at the top of each bar.
Here is fiddle of the same
I am using mouseover events to display tooltips
sets.append("rect")
.attr("class","global")
.attr("width", xScale.rangeBand()/2)
.attr('y', function(d) {
return yScale((d.global/total)*100);
})
.attr("height", function(d){
return h - yScale((d.global/total)*100);
})
.attr('fill', function (d, i) {
return color(d.global);
})
.on('mouseover', function(d, i) {
var xPos = xScale.rangeBand()*i;
//console.log(xScale(i)); 6 190 282
console.log(xScale.rangeBand()*i);
var yPos = yScale((d.global / total) * 100);
d3.select('#hor_tooltip')
.style('left', xPos + 'px')
.style('top', yPos + 'px')
.style('display', 'block')
.html(d.global);
})
.on('mouseout', function() {
d3.select('#hor_tooltip').style('display', 'none');
})
.append("text")
.text(function(d) {
return commaFormat((d.global/total)*100);
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
You should use d3.event inside the mouseover event handler. You can use the x,y coordinates of the event or request the bounding box of the evnet target element.
The following code should help but you still need to adjust for the height of the tootip itself:
var px = d3.event.pageX;
var py = d3.event.target.getBoundingClientRect().top;
You could also try foxTooltip.js. It has an option to always position on a specific side of an element.
https://github.com/MichaelRFox/foxToolTip.js

How to create D3 svg element in drag and drop method

I am have a pallet section using html and SVG canvas.I want to create SVG element when click a button and paste it in to SVG in drag and drop method.my pallet section i creates like this
<div id="toolbox">
<input id="task-button" type="image" title="Activity" src="img/rec.png" alt="Activity">
</div>
I create SVG element like this when button click
d3.select("#task-button").on("click", function(){
var sampleSVG = svg;
sampleSVG.append('rect')
.attr('id', 'task'+(++idtaskelement))
.style("stroke", "black")
.style("stroke-width", "2")
.style("fill", "white")
.attr('transform', 'translate(' + d3.event.pageX + ',' + d3.event.pageY+ ')')
.attr("rx", 10)
.attr("ry", 10)
.attr("width", 120)
.attr("height", 80)
.on("mouseover", function(){d3.select(this).style("fill", "aliceblue");
var point = d3.mouse(this)
, p = {mx: point[0], my: point[1] };
console.log(p.mx +"and "+ p.my);
})
.on("mouseout", function(){d3.select(this).style("fill", "white");})
.on("click", function(){
tmodal.style.display = "block";
})
.call(drag)
});
This element have drag functionality i created like this.
var drag = d3.behavior.drag().on('drag', function(d) {
dragMove(this)
})
function dragMove(me) {
var x = d3.event.x
var y = d3.event.y
d3.select(me).attr('transform', 'translate(' + x + ',' + y + ')')
}
Is there way to call Drag function and start dragging after button click and when we click in canvas we can paste it in the canvas.
If there any way to do this functionality please let me know.

Appending another element when a sibling element's transition ends

I have a bar chart, which I am using transitions to animate the heights of rect elements like so:
//Create a layer for each category of data that exists, as per dataPointLegend values
//e.g. DOM will render <g class="successful"><g>
layers = svg.selectAll('g.layer')
.data(stacked, function(d) {
return d.dataPointLegend;
})
.enter()
.append('g')
.attr('class', function(d) {
return d.dataPointLegend;
})
//transform below is used to shift the entire layer up by one pixel to allow
//x-axis to appear clearly, otherwise bars inside layer appear over the top.
.attr('transform', 'translate(0,-1)');
//Create a layer for each datapoint object
//DOM will render <g class="successful"><g></g><g>
barLayers = layers.selectAll('g.layer')
.data(function(d) {
return d.dataPointValues;
})
.enter()
.append('g');
//Create rect elements inside each of our data point layers
//DOM will render <g class="successful"><g><rect></rect></g></g>
barLayers
.append('rect')
.attr('x', function(d) {
return x(d.pointKey);
})
.attr('width', x.rangeBand())
.attr('y', height - margin.bottom - margin.top)
.attr('height', 0)
.transition()
.delay(function(d, i) {
return i * transitionDelayMs;
})
.duration(transitionDurationMs)
.attr('y', function(d) {
return y(d.y0 + d.pointValue);
})
.attr('height', function(d) {
return height - margin.bottom - margin.top - y(d.pointValue)
});
I then have a further selection used for appending text elements
//Render any point labels if present
//DOM will render <g><g><rect></rect><text></text></g></g>
if (width > miniChartWidth) {
barLayers
.append('text')
.text(function(d) {
return d.pointLabel
})
.attr('x', function(d) {
return x(d.pointKey) + x.rangeBand() / 2;
})
.attr('y', function(d) {
var textHeight = d3.select(this).node().getBoundingClientRect().height;
//Position the text so it appears below the top edge of the corresponding data bar
return y(d.y0 + d.pointValue) + textHeight;
})
.attr('class', 'data-value')
.attr('fill-opacity', 0)
.transition()
.delay(function(d, i) {
return i * transitionDelayMs + transitionDurationMs;
})
.duration(transitionDurationMs)
.attr('fill-opacity', 1);
}
This fades in the text elements nicely after all the rects have finished growing in height. What I wondered, was whether its possible to append a text element to the corresponding layer as each bar finishes its transition?
I have seen the answer on this SO - Show text only after transition is complete d3.js
Which looks to be along the lines of what I am after, I tried adding an .each('end',...) in my rect rendering cycle like so
.each('end', function(d){
barLayers
.append('text')
.text(function() {
return d.pointLabel
})
.attr('x', function() {
return x(d.pointKey) + x.rangeBand() / 2;
})
.attr('y', function() {
var textHeight = d3.select(this).node().getBoundingClientRect().height;
//Position the text so it appears below the top edge of the corresponding data bar
return y(d.y0 + d.pointValue) + textHeight;
})
.attr('class', 'data-value')
.attr('fill-opacity', 0)
.transition()
.delay(function(d, i) {
return i * transitionDelayMs + transitionDurationMs;
})
.duration(transitionDurationMs)
.attr('fill-opacity', 1);
});
But I end up with lots of text elements for each of my g that holds a single rect for each of my datapoints.
I feel like I'm close, but need some assistance from you wise people :)
Thanks
whateverTheSelectionIs
.each('end', function(d){
barLayers
.append('text')
.each runs separately for every element in your selection, and inside the each you're adding text elements to every barLayer (barLayers). So you're going to get a (barLayers.size() * selection.size()) number of text elements added overall. You need to add only one text element in the each to the right bar / g.
The below is a fudge that might work. It's tricky because the text you want to add is a sibling of the rects in the selection that calls the .each function..., d3.select(this.parentNode) should move you up to the parent of the rect, which would be the right barLayer.
whateverTheSelectionIs
.each('end', function(d,i){
d3.select(this.parentNode)
.append('text')

D3 drag error 'Cannot read property x of undefined'

I'm trying to get drag functionality to work on D3, and have copied the code directly from the developer's example.
However it seems the origin (what is being clicked) is not being passed correctly into the variable d, which leads to the error: 'Cannot read property 'x' of undefined'
The relevant code:
var drag = d3.behavior.drag()
.on("drag", function(d,i) {
d.x += d3.event.dx
d.y += d3.event.dy
d3.select(this).attr("transform", function(d,i){
return "translate(" + [ d.x,d.y ] + ")"
})
});
var svg = d3.select("body").append("svg")
.attr("width", 1000)
.attr("height", 300);
var group = svg.append("svg:g")
.attr("transform", "translate(10, 10)")
.attr("id", "group");
var rect1 = group.append("svg:rect")
.attr("rx", 6)
.attr("ry", 6)
.attr("x", 5/2)
.attr("y", 5/2)
.attr("id", "rect")
.attr("width", 250)
.attr("height", 125)
.style("fill", 'white')
.style("stroke", d3.scale.category20c())
.style('stroke-width', 5)
.call(drag);
Usually, in D3 you create elements out of some sort of datasets. In your case you have just one (perhaps, one day you'll want more than that). Here's how you can do it:
var data = [{x: 2.5, y: 2.5}], // here's a dataset that has one item in it
rects = group.selectAll('rect').data(data) // do a data join on 'rect' nodes
.enter().append('rect') // for all new items append new nodes with the following attributes:
.attr('x', function (d) { return d.x; })
.attr('y', function (d) { return d.y; })
... // other attributes here to modify
.call(drag);
As for the 'drag' event handler:
var drag = d3.behavior.drag()
.on('drag', function (d) {
d.x += d3.event.dx;
d.y += d3.event.dy;
d3.select(this)
.attr('transform', 'translate(' + d.x + ',' + d.y + ')');
});
Oleg's got it, I just wanted to mention one other thing you might do in your case.
Since you only have a single rect, you can bind data directly to it with .datum() and not bother with computing a join or having an enter selection:
var rect1 = svg.append('rect')
.datum([{x: 2.5, y: 2.5}])
.attr('x', function (d) { return d.x; })
.attr('y', function (d) { return d.y; })
//... other attributes here
.call(drag);

Resources