In my d3js app, when the user hovers over a particular circle, I have it enlarge. That is no problem. At the same time, I want to select "all the others" and make them smaller. What is a good query to grab "all the other circles"?
You can use selection.filter or the lesser known functional form of the commonly used selection.select depending on your needs.
If you bind your DOM elements to data using key functions, which is the recommended way, then you can filter on a selection's key: http://jsfiddle.net/9TmXs/
.on('click', function (d) {
// The clicked element returns to its original size
d3.select(this).transition() // ...
var circles = d3.selectAll('svg circle');
// All other elements resize randomly.
circles.filter(function (x) { return d.id != x.id; })
.transition() // ...
});
Another general approach is comparing the DOM elements themselves: http://jsfiddle.net/FDt8S/
.on('click', function (d) {
// The clicked element returns to its original size
d3.select(this).transition() // ..
var self = this;
var circles = d3.selectAll('svg circle');
// All other elements resize randomly.
circles.filter(function (x) { return self != this; })
.transition()
// ...
});
Related
I want to filter data in the table based on the age and height at the same time using 2 range sliders.
I have implemented 2 range sliders (Age and Height) using d3.slider.js and a dc.dataTable. I want to use these 2 range sliders at the same time, but it seems that they are not working properly.
Also, under the table, there is the text "49 selected out of 49 records". The numbers are not changing while using the sliders.
Code:
var dataTable = dc.dataTable("table#list");
var dispatch = d3.dispatch('load','filter');
d3.json('data.json',function(json){
dispatch.load(json)
});
dispatch.on('load',function(json) {
var formatNumber = d3.format( ",d");
var facts = crossfilter(json);
var dimensionAge = facts.dimension(function(d) {
return +d.age;
});
var accessorAge = function(d) {
return d.age;
};
var dimensionHeight = facts.dimension(function(d) {
return +d.height;
});
var accessorHeight = function(d) {
return d.height;
};
var range = d3.extent(json, accessorAge);
var range2 = d3.extent(json, accessorHeight);
var all = facts.groupAll();
d3.select("div#slider3")
.call(d3.slider().axis(true).min(range[0]).max(range[1]).value(range)
.on("slide", function(evt,value) {
dispatch.filter(value);
d3.select("#slider3textmin").text(Math.floor(value[0]));
d3.select("#slider3textmax").text(Math.floor(value[1]))
}))
d3.select("div#slider4")
.call(d3.slider().axis(true).min(range2[0]).max(range2[1]).value(range2)
.on("slide", function(evt,value) {
dispatch.filter(value);
d3.select("#slider4textmin").text(Math.floor(value[0]));
d3.select("#slider4textmax").text(Math.floor(value[1]))
}))
FieldNames = [
"",
"Age",
"Weight",
"Height",
"Eye Color",
"Hair Color",
"Race",
"Sex",
"Annual Income"
];
d3.select("tr#FieldNames").selectAll("th")
.data(FieldNames)
.enter()
.append("th")
.append("text")
.text(function(d){
return d;
});
dataTable
.dimension(dimensionAge)
.group(function(d) {
return d.sex;
})
.columns([
function(d) {return "";},
function(d) {return d.age;},
function(d) {return d.weight;},
function(d) {return d.height;},
function(d) {return d.eyeColor;},
function(d) {return d.hairColor;},
function(d) {return d.race;},
function(d) {return d.sex;},
function(d) {return formatNumber(d.annualIncome);}
]);
dispatch.on('filter',function(value){
dataTable.replaceFilter(dc.filters.RangedFilter(value[0], value[1]));
dataTable.redraw();
})
dc.dataCount(".dc-data-count")
.dimension(facts)
.group(all);
dc.renderAll();
});
Link to the website
Plunker
Original response on the dc.js users group.
Nice use of d3.slider.js - I haven't seen that used with dc.js before.
At a quick glance, I see two problems here. First, you're using one
dispatch for both sliders, so both sliders are filtering the age,
since that's the dimension of the table. You'd probably want to create
another dimension for filtering by height, and you don't really need
to attach that to a chart.
Second, instead of just redrawing the chart with dataTable.redraw(),
you probably want to call dataTable.redrawGroup() so that all charts
in its chart group get redrawn, including the dataCount.
Specifically:
you'll need two filter events in your dispatch
var dispatch = d3.dispatch('load','filterAge','filterHeight');
the age slider will call filterAge
dispatch.filterAge(value);
and the height slider will call filterHeight
dispatch.filterHeight(value);
the current filter event handler will now handle filterAge and it will call redrawGroup
dispatch.on('filterAge',function(value){
dataTable.replaceFilter(dc.filters.RangedFilter(value[0], value[1]));
dataTable.redrawGroup();
})
we add another filterHeight handler which directly filters dimensionHeight and also redraws the chart group
dispatch.on('filterHeight',function(value){
dimensionHeight.filter([value[0], value[1]]);
dataTable.redrawGroup();
})
Reset All will also have to clear dimensionHeight. (Since this dimension isn't used by any chart, dc.filterAll() won't find it.)
Reset All
Fork of your plunker.
this for reset all, the 49 selected out of 49 records already change correcly
replace this
Reset All
to this
Reset All
add this after dispatch on load
dispatch.on('load',function(json) {
//your code
})
function sololo(){
//table
dispatch.filterAge([0,100]);
dispatch.filterHeight([0,100]);
//text slider
d3.select("#slider4textmin").text(0)
d3.select("#slider4textmax").text(0)
d3.select("#slider3textmin").text(0);
d3.select("#slider3textmax").text(0)
//slider
d3.select('#slider3').select('#handle-one').style('left','0%')
d3.select('#slider3').select('#handle-two') .style('right','0%')
d3.select('#slider3').select('div').style('left','0%').style('right','0%')
d3.select('#slider4').select('#handle-one').style('left','0%')
d3.select('#slider4').select('#handle-two') .style('right','0%')
d3.select('#slider4').select('div').style('left','0%').style('right','0%')
}
I am trying to design an association miminap using d3.js. My goal is to position different items according to data and associate them using a drag function.
I try to calculate distance to different elements and if distance is lower than total radius, I consider this condition as a dragover. However drag function keeps selecting the node I drag instead of the node I drag it to.
var drag = d3.behavior.drag()
.on('dragstart', function() {})
.on('drag', function(d) {
nodes=d3.select(this.parentNode).selectAll("circle")[0];
nodes.pop(this);
var minDist=1000;
for (i=0;i<nodes.length;i++) {
var currentNode=d3.select(nodes[i]);
var r1=parseInt(this.style.r);
var r2=parseInt(currentNode.style("r"));
var dx=d3.event.x-parseInt(currentNode.style("cx"));
var dy=d3.event.y-parseInt(currentNode.style("cy"));
var dist=Math.sqrt(dx*dx+dy*dy);
if(dist<(r1+r2)) {
d.con=currentNode.attr("id");
currentNode.style("fill","red");
console.log(currentNode[0][0]);
}
}
d3.select(this)
.style("cx",d3.event.x)
.style("cy",d3.event.y);
})
Why does my futile attempt to remove the node I drag by entering nodes.pop(this) does not work ?
I have added an editable codepen version :
https://codepen.io/TeaCult/pen/ezJVyz?editors=1111
Thank you for reading.
Hei,
I'm updating a bar chart when user presses a button. That works fine with the .transition- property. However, if I do that on text, it replaces the text immediately. Instead what I'd like to happen is that it would count from the old to the new number (while the label moves with the bar). So as an example: a bar is updated from value 1453 to 1102. Instead of replacing 1453 immediately when the user clicks it should count up from 1102 to 1453 over the specified transition time.
Can I achieve that? Is there any d3 function for that?
I uploaded a quick example of text interpolation on bl.ocks. The relevant parts are the custom interpolator:
function interpolateText(a, b) {
var regex = /^(\d+), (\d+)$/;
var matchA = regex.exec(a);
var matchB = regex.exec(b);
if (matchA && matchB) {
var x = d3.interpolateRound(+matchA[1], +matchB[1]);
var y = d3.interpolateRound(+matchA[2], +matchB[2]);
return function(t) {
var result = [x(t), y(t)].join(", ");
return result;
};
}
}
d3.interpolators.push(interpolateText);
And using d3.transition.tween:
.on("dragend", function(d, i) {
var prev = [d.x, d.y].join(", ");
d.x = d.origin[0];
d.y = d.origin[1];
var next = [d.x, d.y].join(", ");
var selection = d3.select(this)
.transition()
.duration(1000)
.call(draw);
selection
.select("text")
.tween("textTween", function() {
var i = d3.interpolate(prev, next);
return function(t) {
this.textContent = i(t);
}
});
});
In my case, I am listening for a drag start/end but you can hook it up to a button press very easily.
The reason the above code works is because .tween will get the same animation "ticks" that the standard interpolators use. This causes the inner t parameter to match the progress of the animation and when you set this.textContent it will update the inner value of the DOM element.
The example I use is interpolating between two points which is fairly trivial but if all you want is to update text containing exactly one number it is even easier.
Introducing
I'm using Sequences Sunburst of d3.js for visualization of data.
I want to add a transition between two datasets (triggered by a button). I would like each arc to animate to display the new data.
Something like this: (1)Sunburst_Exemple, but without changing the accessor.
Research
In the (1)Sunburst_Example, the value accessor is modified. But I want to change the data, not the function that defines how to reach the data.
So, I searched a way for redefining data into path.
I was inspired by (2)Sunburst_Exemple, using each() method to store old values and attrTween() for transitions. But nothing is changing and I have the following error message:
Maximum call stack size exceeded . Maybe caused by the fact I have a method and I'm not in a global scope.
(2) link : _http://ninjapixel.io/StackOverflow/doughnutTransition.html
Then I have tried (3)Zoomable_Sunburst example, but nothing it happens in my case... .
(3) link : _http://bl.ocks.org/mbostock/4348373
My Example
Here is my example : JSFIDDLE
Problem is :
colors are lost
transition is not happening
I think I don't understand how transitioning is really working, I could have missed something that could help me in this case.
Any help ?
-
Listener of button call click method that redefined nodes and path.
/*CHANGING DATA*/
function click(d){
d3.select("#container").selectAll("path").remove();
var nodes = partition.nodes(d)
.filter(function(d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
}) ;
var path = vis.data([d]).selectAll("path")
.data(nodes)
.enter().append("svg:path")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d) { return colors[d.name]; })
.style("opacity", 1)
.on("mouseover", mouseover)
.each(stash)
.transition()
.duration(750)
.attrTween("d", arcTween);
;
// Get total size of the tree = value of root node from partition.
totalSize = path.node().__data__.value;
}
_
// Stash the old values for transition.
function stash(d) {
d.x0 = d.x;
d.dx0 = d.dx;
}
// Interpolate the arcs in data space.
function arcTween(a) {
var i = d3.interpolate({x: a.x0, dx: a.dx0}, a);
return function(t) {
var b = i(t);
a.x0 = b.x;
a.dx0 = b.dx;
return arc(b);
};
}
Data Characteristics :
the root node is the same for the two datasets.
the structure is globally the same, only the values are changing.
Some fields disappear but we can add them with value equal to 0.
Observed Behavior
I'm using d3.js, and I'm in a situation where I'd like to update some data based on a drag event, and redraw everything after the dragend event. The draggable items also have some click behavior.
Draggable items can only move along the x-axis. When an item is dragged, and the cursor is directly above the draggable item on dragend/mouseup, the item must be clicked twice after it is re-drawn for the click event to fire. When an item is dragged, but dragend/mouseup does not occur directly above the item, the click event fires as expected (on the first try) after the redraw.
Desired Behavior
I'd like the click event to always fire on the first click after dragging, regardless of where the cursor is.
If I replace the click event on the draggable items with a mouseup event, everything works as expected, but click is the event I'd really like to handle.
A Demonstration
Here is a self-contained example: http://jsfiddle.net/RRCyq/2/
And here is the relevant javascript code:
var data, click_count,did_drag;
// this is the data I'd like to render
data = [
{x : 100, y : 150},
{x : 200, y : 250}
];
// these are some elements I'm using for debugging
click_count = d3.select('#click-count');
did_drag = d3.select('#did-drag');
function draw() {
var drag_behavior,dragged = false;
// clear all circles from the svg element
d3.select('#test').selectAll('circle')
.remove();
drag_behavior = d3.behavior.drag()
.origin(Object)
.on("drag", function(d) {
// indicate that dragging has occurred
dragged = true;
// update the data
d.x = d3.event.x;
// update the display
d3.select(this).attr('cx',d.x);
}).on('dragend',function() {
// data has been updated. redraw.
if(dragged) { draw(); }
});
d3.select('#test').selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx',function(d) { return d.x; })
.attr('cy',function(d) { return d.y; })
.attr('r',20)
.on('click',function() {
did_drag.text(dragged.toString());
if(!dragged) {
// increment the click counter
click_count.text(parseInt(click_count.text()) + 1);
}
}).call(drag_behavior);
}
draw();
A little late to the party, buuuut...
The documentations suggests that you use d3.event.defaultPrevented in your click event to know whether or not the element was just dragged. If you combine that with your drag and dragend events, a much cleaner approach is to call the exact function you want when necessary (see when and how flashRect is called):
http://jsfiddle.net/langdonx/fE5gN/
var container,
rect,
dragBehavior,
wasDragged = false;
container = d3.select('svg')
.append('g');
rect = container.append('rect')
.attr('width', 100)
.attr('height', 100);
dragBehavior = d3.behavior.drag()
.on('dragstart', onDragStart)
.on('drag', onDrag)
.on('dragend', onDragEnd);
container
.call(dragBehavior)
.on('click', onClick);
function flashRect() {
rect.attr('fill', 'red').transition().attr('fill', 'black');
}
function onDragStart() {
console.log('onDragStart');
}
function onDrag() {
console.log('onDrag');
var x = (d3.event.sourceEvent.pageX - 50);
container.attr('transform', 'translate(' + x + ')');
wasDragged = true;
}
function onDragEnd() {
if (wasDragged === true) {
console.log('onDragEnd');
// always do this on drag end
flashRect();
}
wasDragged = false;
}
function onClick(d) {
if (d3.event.defaultPrevented === false) {
console.log('onClick');
// only do this on click if we didn't just finish dragging
flashRect();
}
}
I didn't like the global variable, so I made a revision to use data: http://jsfiddle.net/langdonx/fE5gN/1/
After observing that the click required before my svg circles would start responding to click events again could happen anywhere in the document, I settled on a hack whereby I simulate a click event on the document (thanks to https://stackoverflow.com/a/2706236/1015178) after the drag ends. It's ugly, but it works.
Here's the function to simulate an event (again, thanks to https://stackoverflow.com/a/2706236/1015178)
function eventFire(el, etype){
if (el.fireEvent) {
(el.fireEvent('on' + etype));
} else {
var evObj = document.createEvent('Events');
evObj.initEvent(etype, true, false);
el.dispatchEvent(evObj);
}
}
And here's the updated drag behavior:
drag_behavior = d3.behavior.drag()
.origin(Object)
.on("drag", function(d) {
// indicate that dragging has occurred
dragged = true;
// update the data
d.x = d3.event.x;
// update the display
d3.select(this).attr('cx',d.x);
}).on('dragend',function() {
// data has been updated. redraw.
if(dragged) { draw(); }
// simulate a click anywhere, so the svg circles
// will start responding to click events again
eventFire(document,'click');
});
Here's the full working example of my hackish "fix":
http://jsfiddle.net/RRCyq/3/