Programmatically control brush and pass relevant information - javascript

this is an extension of this question from yesterday. I am attempting to draw and create multiple brushes. The bruteforce method i am using does ok, but i need additional functionality to programmatically control each of these newly created brushes,
createBrush(){
//Brushes
var brushGroups = []
for(var i=0; i<this.uniqueCats.length; i++){
//BRUSH CONTAINER
brushGroups[i] = d3.select('#brushContainer')
.append('g')
.attr('id',`${this.uniqueCats[i]}brush`)
.attr('transform', `translate(${this.margin.left},0)`)
.attr('class','brush');
//BRUSH
var brushID = this.uniqueCats[i]
this[`${this.uniqueCats[i]}Brush`] = d3.brushX()
.extent([[0, (i*135)+38], [this.width,(i*135)+170]])
.on('end',(i)=>{
console.log('hi')
this.updateBubbles(i)
})
//CALL BRUSH from the brush container
brushGroups[i].call(this[`${this.uniqueCats[i]}Brush`])
}
}
As you can see above I 1) create a brush container 2) create a brush, 3) call that brush from the container. The main issue i am having it passing relevant information to the on for this.updateBubbles(). The hope was for each brush to pass in specific information to updatebubbles() that would allow me to understand which brush was being activated.
From there I decided to use the more elegant d3 approach with enter() but, this method fails to invoke a brush at all,
createBrushv2(){var that =this
var brushContainer = d3.select('#brushContainer')
.selectAll('g')
.data(this.uniqueCats)
var brushContainerEnter = brushContainer
.enter()
.append('g')
.attr('id', d=> `${d}brush`)
brushContainer = brushContainerEnter.merge(brushContainer)
.attr('transform', `translate(${this.margin.left},0)`)
.attr('class','brush')
.each( function(d,i){
console.log(i)
d3.brushX()
.extent([[0,(i*135)+38], [that.width,(i*135)+170]])
.on('end',that.updateBubbles(d))
})
}
The updateBubbles() function is whats evoked from each brush, so this function needs to be able to understand which brush is selected.

There are several issues in your code, which makes your question too broad. Therefore, I'll only deal with the issue of the brush not being invoked.
Before anything else, congratulations for moving from approach #1 (for loop) to approach #2 (D3 selections). In a D3 code, it's almost a very bad idea using loops to append anything.
That being said, unlike your first snippet, you're not actually calling the brush in the second one.
Therefore, this...
d3.brushX()
.extent([[0,(i*135)+38], [that.width,(i*135)+170]])
.on('end',that.updateBubbles(d))
Should be:
d3.brushX()
.extent([[0,(i*135)+38], [that.width,(i*135)+170]])
.on('end', that.updateBubbles)(d3.select(this))
Even better, you should use a selection.call, just like in your first approach. Also, have in mind that that.updateBubbles(d) will invoke the function immediately, and that's not what you want.
Here is a very basic snippet as a demo:
const data = d3.range(5);
const svg = d3.select("svg");
createBrushv2();
function createBrushv2() {
var brushContainer = svg.selectAll(".brush")
.data(data)
var brushContainerEnter = brushContainer.enter()
.append('g');
brushContainer = brushContainerEnter.merge(brushContainer)
.attr('transform', function(d) {
return "translate(0," + (d * 25) + ")"
})
.attr('class', 'brush')
.each(function(d) {
d3.brushX()
.extent([
[0, 0],
[300, 20]
])
.on('end', updateBubbles)(d3.select(this))
})
.each(function() {
d3.brushX().move(d3.select(this), [0, 300])
})
};
function updateBubbles(brush) {
console.log(brush)
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>

Related

Bubble Map with leaflet and D3.js [problem] : bubbles overlapping

I have a basic map here, with dummy data. Basically a bubble map.
The problem is I have multiple dots (ex:20) with exact same GPS coordinates.
The following image is my csv with dummy data, color blue highlight overlapping dots in this basic example. Thats because many compagny have the same city gps coordinates.
Here is a fiddle with the code I'm working on :
https://jsfiddle.net/MathiasLauber/bckg8es4/45/
Many research later, I found that d3.js add this force simulation fonction, that avoid dots from colliding.
// Avoiding bubbles overlapping
var simulationforce = d3.forceSimulation(data)
.force('x', d3.forceX().x(d => xScale(d.longitude)))
.force('y', d3.forceY().y(d => yScale(d.latitude)))
.force('collide', d3.forceCollide().radius(function(d) {
return d.radius + 10
}))
simulationforce
.nodes(cities)
.on("tick", function(d){
node
.attr("cx", function(d) { return projection.latLngToLayerPoint([d.latitude, d.longitude]).x; })
.attr("cy", function(d) {return projection.latLngToLayerPoint([d.latitude, d.longitude]).y; })
});
The problem is I can't make force layout work and my dots are still on top of each other. (lines: 188-200 in the fiddle).
If you have any tips, suggestions, or if you notice basic errors in my code, just let me know =D
Bunch of code close to what i'm trying to achieve
https://d3-graph-gallery.com/graph/circularpacking_group.html
https://jsbin.com/taqewaw/edit?html,output
There are 3 problems:
For positioning the circles near their original position, the x and y initial positions need to be specified in the data passed to simulation.nodes() call.
When doing a force simulation, you need to provide the selection to be simulated in the on tick callback (see node in the on('tick') callback function).
The simulation needs to use the previous d.x and d.y values as calculated by the simulation
Relevant code snippets below
// 1. Add x and y (cx, cy) to each row (circle) in data
const citiesWithCenter = cities.map(c => ({
...c,
x: projection.latLngToLayerPoint([c.latitude, c.longitude]).x,
y: projection.latLngToLayerPoint([c.latitude, c.longitude]).y,
}))
// citiesWithCenter will be passed to selectAll('circle').data()
// 2. node selection you forgot
const node = selection
.selectAll('circle')
.data(citiesWithcenter)
.enter()
.append('circle')
...
// let used in simulation
simulationforce.nodes(citiesWithcenter).on('tick', function (d) {
node
.attr('cx', function (d) {
// 3. use previously computed x value
// on the first tick run, the values in citiesWithCenter is used
return d.x
})
.attr('cy', function (d) {
// 3. use previously computed y value
// on the first tick run, the values in citiesWithCenter is used
return d.y
})
})
Full working demo here: https://jsfiddle.net/b2Lhfuw5/

Datum-Data difference in map behavior in d3

I'm pretty new to d3js and trying to understand the difference between using data and datum to attach data to elements. I've done a fair bit of reading the material online and I think I theoretically understand what's going on but I still lack an intuitive understanding. Specifically, I have a case where I'm creating a map using topojson. I'm using d3js v7.
In the first instance, I have the following code to create the map within a div (assume height, width, projection etc. setup correctly):
var svg = d3.select("div#map").append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + 15 + "," + 0 + ")");
var path = d3.geoPath()
.projection(projection);
var mapGroup = svg.append("g");
d3.json("json/world-110m.json").then(function(world){
console.log(topojson.feature(world, world.objects.land))
mapGroup.append("path")
.datum(topojson.feature(world, world.objects.land))
.attr("class", "land")
.attr("d", path);
});
The console log for the topojson feature looks like this:
And the map comes out fine (with styling specified in a css file):
But if I change datum to data, the map disappears. I'm trying to improve my understanding of how this is working and I'm struggling a little bit after having read what I can find online. Can someone explain the difference between data and datum as used in this case and why one works and the other doesn't?
Thanks for your help!
There are several differences between data() and datum(), but for the scope of your question the main difference is that data() accepts only 3 things:
An array;
A function;
Nothing (in that case, it's a getter);
As you can see, topojson.feature(world, world.objects.land) is an object. Thus, all you'd need to use data() here (again, not the idiomatic D3, I'm just addressing your specific question) is wrapping it with an array:
.data([topojson.feature(world, world.objects.land)])
Here is your code using data():
var svg = d3.select("div#map").append("svg")
.attr("width", 500)
.attr("height", 300)
.attr("transform", "translate(" + 15 + "," + 0 + ")");
var path = d3.geoPath();
var mapGroup = svg.append("g");
d3.json("https://raw.githubusercontent.com/d3/d3.github.com/master/world-110m.v1.json").then(function(world) {
const projection = d3.geoEqualEarth()
.fitExtent([
[0, 0],
[500, 300]
], topojson.feature(world, world.objects.land));
path.projection(projection);
mapGroup.append("path")
.data([topojson.feature(world, world.objects.land)])
.attr("class", "land")
.attr("d", path);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/topojson#3"></script>
<div id="map"></div>

Code doesn't work when upgrading from v3 to v5

when I upgrade d3.js from v3.5 to 5.9.2 (the latest actually), my graph display correctly, but the legend items doesn't display. In my code, the data exist, but I have only <div class="legend"></div> without inner <div data-id="DATA">DATA</div>. I don't know if the code below is correct for v5. Thank you for your help.
d3.select(".segLegend")
.insert("div", ".chart")
.attr("class", "legend")
.selectAll("div")
.data(names)
.sort()
.enter()
.append("div")
.attr("data-id", function(id) {
return id;
})
.each(function(id) {
d3.select(this)
.append("span")
.style(
"background-color",
$scope.openGraphModal.chart.color(id)
);
d3.select(this)
.append("span")
.html(id);
if (id !== findSupplier.commodity.supplier.name) {
$(this).toggleClass('c3-legend-item-hidden');
}
})
The DOM with V3
The DOM with V5
At a first and quick look this is a strange problem, hard to identify, because in a D3 code we don't use the sort method at the position you did: at that position, that sort is worthless. Have a look (using v3):
const data = [1,3,5,4,2];
const divs = d3.select("body")
.selectAll(null)
.data(data)
.sort()
.enter()
.append("div")
.html(Number)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
As you can see, nothing is sorted. The sort method should be used after appending the real DOM elements (again, v3):
const data = [1, 3, 5, 4, 2];
const divs = d3.select("body")
.selectAll(null)
.data(data)
.enter()
.append("div")
.sort()
.html(Number)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
Finally, sort at this correct position works with v5 as well:
const data = [1, 3, 5, 4, 2];
const divs = d3.select("body")
.selectAll(null)
.data(data)
.enter()
.append("div")
.sort()
.html(Number)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Your problem
When you put sort in that strange (and useless) position in the enter selection in a code that uses D3 v4/5, not only it doesn't sort anything but, the most important, it avoids the DOM elements being appended:
const data = [1, 3, 5, 4, 2];
const divs = d3.select("body")
.selectAll(null)
.data(data)
.sort()
.enter()
.append("div")
.html(Number)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
The snippet remains blank.
The solution is moving the sort to the correct position, not only because of the problem you're facing but also because, where it is right now, it's useless in v3 as well.

d3js rescale redraw barchart without new data

I have a simple (o.k. not so simple) barchart which shows the electric power consumption of one consumer (C1). I add the consumption of another consumer (C2) as line. The max consumption of C2 if higher then the max consumption of C1 so I have to rescale. I have solved this problem but not as beautiful I wanted to.
I calculate the new yMax, set the domain, rescale the axis (beautiful) remove all 'rect' and redraw (not beautiful). Is there a possibility to say: hey bars, I have a new scale, go down with a beautiful animation :)
Here the rescale method:
var rescale = function () {
//in this function the new _maxYValue is set
renderLineView();
var data = _data;
y.domain([_minYValue, _maxYValue]);
_svg.select(".y.axis")
.transition().duration(1500).ease("sin-in-out")
.call(yAxis());
_svg.selectAll("rect").remove();
var barWidth = getBarWidth(data.length);
var bars = d3.select("#layer_1").selectAll(".bar").data(data, function (d) {
return d.xValue;
});
bars.enter().append("rect")
.attr("class", "daybarincomplete")
.attr("x", function (d, i) {
return x(d.xValue) + 4;
})
.attr("width", barWidth)
.attr("y", function (d) {
return Math.min(y(0), y(d.value));
})
.attr("height", function (d) {
return Math.abs(y(d.value) - y(0));
});
}
Here is the jsfiddle: http://jsfiddle.net/axman/v4qc7/5/
thx in advance
©a-x-i
Use the .transition() call on bars, to determine the behaviour you want when the data changes (e.g. Bar heights change). You'd chain the .attr() function after it to set bar height etc.
To deal with data points that disappear between refreshes (e.g. You had 10 bars originally but now only have 9), chain the .exit().remove() functions to bars.
With both of the above, you can additionally chain something like .duration(200).ease('linear') to make it look all pretty.
pretty much what #ninjaPixel said. There's a easy to follow example here
http://examples.oreilly.com/0636920026938/chapter_09/05_transition.html

D3: move circle between two different g elements in force layout

I have two g elements each containing circles. Circles are organized using force.layout. The g elements are transitioning.
You can see here: demo. Reduced code:
var dots = svg.selectAll(".dots")
.data(data_groups)
.enter().append("g")
.attr("class", "dots")
.attr("id", function (d) {
return d.name;
})
...
.each(addCircles);
dots.transition()
.duration(30000)
.ease("linear")
.attr("transform", function (d, i) {
return "translate(" + (150 + i * 100) + ", " + 450 + ")";
});
function addCircles(d) {
d3.select(this).selectAll('circle')
.data(data_circles.filter(function (D) {
return D.name == d.name
}))
.enter()
.append('circle')
.attr("class", "dot")
.attr("id", function (d) {
return d.id;
})
...
.call(forcing);
}
function forcing(E) {
function move_towards(alpha) {
...
}
var force = d3.layout.force()
.nodes(E.data())
.gravity(-0.01)
.charge(-1.9)
.friction(0.9)
.on("tick", function (e) {
...
});
force.start();
}
I need to move circle (for example id=1) from the first g element to the second one using transition.
Any suggestions are appreciated.
It can be done.
What I did was:
1) Use jquery to append the point to the target group
2) Use a transformation (no transition) to move the point back to its original location
3) Transition the point to its new location
The jQuery was used for the appendTo method. It can be removed and replaced with some pure Javascript stuff, but it's quite convenient.
I've got a partially working fiddle here. The green points work right, but something is going wrong with the blue ones. Not sure why.
In my view, transitions work on a single element. If an element changes its position in the DOM tree, from below one g to another, I can't think of a way to make that as one smooth transition because it's basically a binary split: Now there's an element under one g, now it's gone but there's another one somewhere else.
What I'd do in order to achieve what I think you want to do: Group everything under the same ´g´, assign color and translation individually, then change color and translation for that single element you want to change.
But don't take that as a reliable statement that you can't do it the way you originally wanted.

Categories