I'm working on a d3.js application.
In this example I am trying to toggle the slices when the user clicks on the legend components. It will initially take the complete data as its source, but if there is a previous manipulated data source will use that as a base. I've tried to hook into the toggling functionality as the legend is manipulated. I would prefer to separate the functionality - but wasn't sure how else to know if the slice is to be active or not.
Its not working as expected though, especially when trying to handle multiple active/non active slices.
http://jsfiddle.net/Qh9X5/3282/
onLegendClick: function(dt, i){
//_toggle rectangle in legend
var completeData = jQuery.extend(true, [], methods.currentDataSet);
newDataSet = completeData;
if(methods.manipulatedData){
newDataSet = methods.manipulatedData;
}
d3.selectAll('rect')
.data([dt], function(d) {
return d.data.label;
})
.style("fill-opacity", function(d, j) {
var isActive = Math.abs(1-d3.select(this).style("fill-opacity"));
if(isActive){
newDataSet[j].total = completeData[j].total;
}else{
newDataSet[j].total = 0;
}
return isActive;
});
//animate slices
methods.animateSlices(newDataSet);
//stash manipulated data
methods.manipulatedData = newDataSet;
}
Here is the onLegendClick function.
I am toggling the opacity of the inner fill on the rectangle when the user clicks.
I've tried to modify the value of the data accordingly - although it is not handling multiple toggles.
http://jsfiddle.net/Qh9X5/3324/
Ideally if the user tries to deactivate all slices, I would want it to reset the chart and restore all the slices in the process. Would be keen to streamline the code and maybe separate the logic and presentation layer for the styling of the rectangles in the legend.
line 234
onLegendClick: function(dt, i){
//_toggle rectangle in legend
var completeData = jQuery.extend(true, [], methods.currentDataSet);
newDataSet = completeData;
if(methods.manipulatedData){
newDataSet = methods.manipulatedData;
}
d3.selectAll('rect')
.data([dt], function(d) {
return d.data.label;
})
.style("fill-opacity", function(d, j) {
var isActive = Math.abs(1-d3.select(this).style("fill-opacity"));
if(isActive){
newDataSet[j].total = completeData[j].total;
}else{
newDataSet[j].total = 0;
}
return isActive;
});
//animate slices
methods.animateSlices(newDataSet);
//stash manipulated data
methods.manipulatedData = newDataSet;
}
Related
I have D3 graph base on Multi-line graph 3 with v7: Legend, this sample contains few and shorts legends for the graph. In my sample I want to increase the length for legends and stack the data if is necessary, I want to avoid overlapping in the legends,
https://bl.ocks.org/d3noob/d4a9e3e45094e89808095a47da19808d
dataNest.forEach(function(d,i) {
svg.append("path")
.attr("class", "line")
.style("stroke", function() { // Add the colours dynamically
return d.color = color(d.key); })
.attr("d", priceline(d.value));
// Add the Legend
svg.append("text")
.attr("x", (legendSpace/2)+i*legendSpace) // space legend
.attr("y", height + (margin.bottom/2)+ 5)
.attr("class", "legend") // style the legend
.style("fill", function() { // Add the colours dynamically
return d.color = color(d.key); })
.text(d.key);
});
There are two possible solutions I can think of, shown in this JSFiddle.
First, if it is acceptable that the legend is not part of the svg, then realize the legend with a simple unordered list in a container next to the svg. This is probably the best when there is a varying number of legend entries and there are no restrictions considering the styling via css. The browser takes care of varying lengths and does an automatic line break.
Second, if the legend has to be a part of the svg, one can use the getBBox()-method that determines the coordinates of the smallest rectangle around an object inside an svg.
In a first step select all the legend entries that have been rendered and get the bounding boxes:
const bbox = svg.selectAll(".legend")
.nodes()
.map(legend_entry => legend_entry.getBBox());
With this array and the width of the svg, we can calculate the positions for each legend entry:
bbox.reduce((pos, box) => {
let left, right, line;
if (pos.length === 0) {
left = 0;
line = 1;
} else {
/* The legend entry starts where the last one ended. */
left = pos[pos.length - 1].right;
line = pos[pos.length - 1].line;
}
/* Cumulative width of legend entries. */
right = left + box.width;
/* If right end of legend entry is outside of svg, make a line break. */
if (right > svg_width) {
line = line + 1;
left = 0;
right = box.width;
}
pos.push({
left: left,
right: right,
line: line,
});
return pos;
}, []);
Margins and paddings have to be included manually in the calculation of the positions. Of course, one could obtain the maximum width of all legend entries and make them all the same width with center alignment as in the d3noob example.
In the JSFiddle, I realized this repositioning by first rendering a hidden legend and then an additional one that is visible. It is of course possible to use only one legend, but I would not to take any chances that the process of repositioning is visible in the rendered document.
I am displaying two Geojson files on leaflet. The two files are displaying fine on my map but I want to be able to have the selector working on both layers.
As you can see here, my selector only display on province : http://bl.ocks.org/renauld94/8493ca671ce8de63bfab9fafd3f3f574/363f40907203cc431de22e16987669b7bae13fe8
var ward = [];
var wardOverlay = L.d3SvgOverlay(function(sel, proj) {
var upd = sel.selectAll('path').data(ward);
upd.enter()
.append('path')
.attr('d', proj.pathFromGeojson)
.attr('stroke', 'red')
.attr('fill-opacity', '0.2');
upd.attr('stroke-width', 1 / proj.scale);
});
var province = [];
var provinceOverlay = L.d3SvgOverlay(function(sel, proj) {
var upd = sel.selectAll('path').data(province);
upd.enter()
.append('path')
.attr('d', proj.pathFromGeojson)
.attr('stroke', 'black')
.attr('fill-opacity', '0.1');
upd.attr('stroke-width', 1 / proj.scale);
});
L.control.layers({"Geo Tiles": tiles}, {"province": provinceOverlay}, {"ward": wardOverlay}).addTo(map);
d3.json("ward.geo.json", function(data) { ward = data.features; wardOverlay.addTo(map) });
d3.json("province.geo.json", function(data) { province = data.features; provinceOverlay.addTo(map) });
</script>
</body>
</html>
How I can have a selector for both layers?
Have a look at the creation of Control.Layers :
L.control.layers(<Object> baselayers?, <Object> overlays?, <Control.Layers options> options?)
Creates an attribution control with the given layers. Base layers will be switched with radio buttons, while overlays will be switched with checkboxes. [...]
It means that the second object contains the entries for each switchable layer. Try:
L.control.layers({"Geo Tiles": tiles}, {
"province": provinceOverlay,
"ward": wardOverlay
}).addTo(map);
I've setup an example to demonstrate the issue's I've encountered:
To be brief, I'm using d3 to render a map of the united states. I'm appending relevant data attributes to handle click events and identify which state was clicked.
On click events the following is preformed:
I'm grabbing the US County topojson file (which contains ALL US
Counties).
Removing irrelevant counties from the data and rendering them on the
map of the state that was clicked.
I can't seem to figure out what is going on behind the scenes that is causing some of the counties to be drawn while others are ignored.
When I log the data that is returned from the filtered list, I'm displaying the accurate number of counties, but they are only partially drawn on the map. Some states don't return any. Pennsylvania and Texas partially work.
I've checked the data and the comparison operations, but I'm thinking this may have to do with arcs properties being mismatched.
If I utilize the Counties JSON file to render the entire map of the united states they are all present.
If anyone can help shed some light on what might be happening that would be great.
svg {
fill:#cccccc;
height:100%;
width:100%;
}
.subunit{
outline:#000000;
stroke:#FFFFFF;
stroke-width: 1px;
}
.subunit:hover{
fill:#ffffff;
stroke:#FFFFFF;
stroke-width="10";
}
<body>
<script src="http://www.cleanandgreenfuels.org/jquery-1.11.3.min.js"></script>
<script src="http://www.cleanandgreenfuels.org/d3.v3.min.js"></script>
<script src="http://www.cleanandgreenfuels.org/topojson.v1.min.js"></script>
<script>
var width = window.innerWidth;
height = window.innerHeight;
var projection = d3.geo.albers()
.scale(1500)
.translate([width / 2, height / 2]);
//d3.geo.transverseMercator()
//.rotate([77 + 45 / 60, -39 - 20 / 60]);
//.rotate([77+45/60,-40-10/60])
//.scale(500)
//.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width+"px")
.attr("height", height+"px");
d3.json("http://www.cleanandgreenfuels.org/usstates2.json.php", function(error, us, init) {
//svg.append("path")
// .datum(topojson.feature(us, us.objects.counties))
//.attr("d", path);
function init(){
$( document ).ready(function() {
$('.subunit').on('click',function(){
var stateid = $(this).attr("data-stateid");
function clearStates(stateid){
d3.json("http://www.cleanandgreenfuels.org/uscounties2.json.php", function(error, us) {
console.log(us);
console.log("DATA CLICKED ID "+stateid);
test = jQuery.grep(us.objects.counties.geometries, function(n){
return (n.properties.stateid == stateid);
});
us.objects.counties.geometries = test;
console.log(test.length);
console.log(us.objects.counties.geometries.length);
var test = topojson.feature(us, us.objects.counties).features;
console.log(test);
console.log(test.length);
svg.selectAll(".subunit")
.data(test)
.enter().append("path")
.attr("class", function(d) { return "subunit"; })
.attr("d", path)
.attr("data-countyid", function(r){ return r.id; });
});
}
clearStates(stateid);
});
});
}
svg.selectAll(".subunit")
.data(topojson.feature(us, us.objects.us).features)
.enter().append("path")
.attr("class", function(d) { return "subunit"; })
.attr("d", path)
.attr("data-stateid", function(r){ return r.id; });
init();
});
</script>
</body>
It appears as if I was attempting to utilize some outdated features, using topojson.mesh and .datum() to add the new data has resolved this issue, but has introduced a new error.
Now it appears as if the polygons that are rendered must be in sequence to be drawn properly this way.
I think the data going in should be cleaned up to optimize the way d3 is designed to function, but I'd still like to know more about how it is rendering this information that is obtained from the dataset.
function clearStates(stateid){
d3.json("http://www.cleanandgreenfuels.org/uscounties2.json.php", function(error, us) {
console.log(us);
console.log("DATA CLICKED ID "+stateid);
test = jQuery.grep(us.objects.counties.geometries, function(n){
return (n.properties.stateid == stateid);
});
us.objects.counties.geometries = test;
console.log(test.length);
console.log(us.objects.counties.geometries.length);
**var test = topojson.mesh(us, us.objects.counties);**
console.log(test);
console.log(test.length);
**svg.append("path")
.datum(test)
.attr("class", function(d) { return "subunit"; })
.attr("d", path)
.attr("data-countyid", function(r){ return r.id; });**
});
}
In the d3.js example, the goal is to change the X & Y scales to reveal a newly added circle which is added offscreen.
Currently, if you were to change the zoom scale, then click on the button to add a new circle, the zoom level suddenly resets to 1. Now dragging the view will cause the zoom scale to become correct again, which is the same zoom scale right before adding the circle.
However this problem goes away if you change redrawWithTransition() on line 123 to redraw() which removes the transitions.
Everything works fine if you add the circle without first zooming.
Why do we have to drag the view again to get the correct zoom scale back? How can we avoid having to do this additional drag and still use transitions in redrawWithTransition()? Thank you!!
Jsfiddle: http://jsfiddle.net/NgWmU/
Because panning again after adding the circle solves the problem, I tried calling redraw() at the end but theres no difference! Is the panning/zooming triggering something else in addition to redraw()?
Jsfiddle: http://jsfiddle.net/NgWmU/2/
First the user zooms/pans around and ends up with a view like in figure A. Next the new circle is added and we should get figure C, but instead we get figure B where the user's current translate/zoom is changed significantly.
Another case
If the user pan/zoom to a view similar to the top figure, adding the new circle should reposition the existing circles slightly, but instead the entire view is reset.
Was thinking about something like:
// Copy existing domains
var xMinOld = xMin,
xMaxOld = xMax,
yMinOld = yMin,
yMaxOld = yMax;
// Update domains
var yMin = d3.min(data.map( function(d) { return d; } )),
yMax = d3.max(data.map( function(d) { return d; } )),
xMin = d3.min(data.map( function(d) { return d; } )),
xMax = d3.max(data.map( function(d) { return d; } ));
if(x.domain()[0] > newData[0] || x.domain()[1] < newData[0]) {
if(x.domain()[0] > newData[0]) {
x.domain([xMin, xMaxOld]);
} else {
x.domain([xMinOld, xMax]);
}
zoom.x(x);
}
if(y.domain()[0] > newData[0] || y.domain()[1] < newData[0]) {
if(y.domain()[0] > newData[0]) {
y.domain([yMin, yMaxOld]);
} else {
y.domain([yMinOld, yMax]);
}
zoom.y(y);
}
But eventually the view will still be reset because we pass the updated scales x and y into zoom...
I am trying to modify the following code found on one of the sites to display both the original and updated data in the graph. I want the updated data be in different color and still show the original data and see the change. Can anyone point me the error.
<title>d3 example</title>
<style>
.original{
fill: rgb(7, 130, 180);
}
.updated{
fill: rgb(7,200,200);
}
</style>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript" src="http://d3js.org/d3.v2.min.js"></script>
<script type="text/javascript">
// Suppose there is currently one div with id "d3TutoGraphContainer" in the DOM
// We append a 600x300 empty SVG container in the div
var chart = d3.select("#d3TutoGraphContainer").append("svg").attr("width", "600").attr("height", "300");
// Create the bar chart which consists of ten SVG rectangles, one for each piece of data
var rects = chart.selectAll('rect').data([1 ,4, 5, 6, 24, 8, 12, 1, 1, 20])
.enter().append('rect')
.attr("stroke", "none")
//.attr("fill", "rgb(7, 130, 180)")
.attr('class','original')
.attr("x", 0)
.attr("y", function(d, i) { return 25 * i; } )
.attr("width", function(d) { return 20 * d; } )
.attr("height", "20");
// Transition on click managed by jQuery
rects.on('click', function() {
// Generate randomly a data set with 10 elements
var newData = [1,2,3,4];
//for (var i=0; i<10; i+=1) { newData.push(Math.floor(24 * Math.random())); }
var newRects = d3.selectAll('rects.original');
newRects.data(newData)
.transition().duration(2000).delay(200)
.attr("width", function(d) { return d * 20; } )
//.attr("fill", newColor);
.attr('class','updated');
});
</script>
I want to know if I can get control of the original data using d3.selectAll('rects.original')
If you select "rects.original" and bind data to it, you create a join with an Update, Exit and Enter selection. Im not sure i fully understand what you are trying to achieve, but if you want new data to be drawn independently of the old rects and data, you have to create a new selection for it:
var newRects = chart.selectAll("rect.new")
.data(newData)
.enter()
(...)
and draw it.
Beware that overlapping in SVG means that underlying Elements will not be displayed anymore.
Im not sure what you mean by "getting control over the original Data", it is still bound to the selection you bound it to. If you want to modify it, you have to modify the data, rebind it, and then apply a transition on the update selection.