Sunburst partition data overwritten by second sunburst on same page - javascript

Posting both question & answer here to save somebody else the same trouble later...
When I create two sunburst charts using d3.layout.partition, the first sunburst's slice proportions are overwritten by the second sunburst's slice proportions upon resize of the slices.
The two charts pass different .value accessor functions into the partition layout, e.g.
d3.layout.partition()
.sort(null)
.value(function(d) { return 1; });
vs.
d3.layout.partition()
.sort(null)
.value(function(d) { return d.size; });
And they generate their own list of nodes that are not shared between the two sunbursts. However, if I re-call the d3.svg.arc generator to resize to larger radius (but not change overall proportions), the slice angles are suddenly overwritten.
See the example here: http://bl.ocks.org/explunit/ab8cf15534f7fec5ac6d

The problem is that while partition.nodes() seems to generate a new data structure (e.g if you give it some .key functions, it writes the extra properties (e.g. .x, .y, .dx, dy) to the underlying data and does not make a copy of the data. Thus if the data structure is shared between the two charts, these .x, .y, .dx, dy properties will bleed through to the other graphs.
This seems like a bug to me, but in reading this old GitHub issue it seems to be treated as "by design". Perhaps it will be reconsidered in future versions.
One workaround is to use something like Lodash/Underscore cloneDeep or Angular's copy to make each chart have it's own copy of the data.
makeSunburst(null, _.cloneDeep(root), countAccessorFn);
makeSunburst(null, _.cloneDeep(root), sizeAccessorFn);
See example here: http://bl.ocks.org/explunit/e9efb830439247eea1be

An alternative to copying the whole dataset for each chart would be to simply recompute the partition before re-rendering.
Instead of having makeSunburst() be a function of the accessor, make it a function of the partition. Pass a different partition function to each chart:
// create separate partition variables
var countPartition = d3.layout.partition().sort(null).value(countAccessorFn);
var sizePartition = d3.layout.partition().sort(null).value(sizeAccessorFn);
// make the charts as a function of partition
charts.push(makeSunburst(root, countPartition));
charts.push(makeSunburst(root, sizePartition));
Then before applying the transition, simply update the nodes variable to reflect the associated partition:
addToRadius: function(radiusChange) {
radius += radiusChange;
ringRadiusScale.range([0, radius]);
// update the data before re-rendering each chart
nodes = partition.nodes(dataRoot);
path.transition().attr('d', arc);
}
Now when you update each chart, it is using the correct partition.
Here's an updated example.

Related

Reusable d3 charts with optional functionality

I encapsulated d3 charts into function as suggested best practice from creator in blog Towards Reusable Charts. Is it possible to create optional functionalities on top of this chart, so calling specific function would trigger it, otherwise it would be omitted.
Working JSFiddle example (base working example from Rob Moore's blog)
In JS line 56 I added a function which I'd like to create and then conditionally call in line 67.
My current way of doing it, is creating a boolean and setting it to false and then calling function with argument true. Problem of doing it this way is that the code gets too many conditionals and edge cases.
P.S. this question is not meant to be a discussion how to correctly apply axis to the chart. This is just an example.
I think it's better to add additional functionalities after the chart is drawn
var runningChart = barChart().barPadding(2).fillColor('coral');
d3.select('#runningHistory')
.datum(milesRun)
.call(runningChart);
runningChart.x_axis(); // additional functionality
So that the original chart container can be saved in a variable and it can be used to append other functionalities. For example
function barChart() {
var charContainer;
function chart(selection){
charContainer = d3.select(this).append('svg')
.attr('height', height)
.attr('width', width);
}
chart.x_axis = function() {
// Add scales to axis
var x_axis = d3.axisBottom()
.scale(widthScale);
charContainer.append('g').call(x_axis);
return chart;
}
}
If there is any need to add additional functionality before the chart is drawn, then all the functionalities can be saved in a Javascript object and drawn like in this example. https://jsfiddle.net/10f7hdae/

D3 parcoords axis scale not updating correctly

I'm using the d3 parcoords library. When I modify data plotted in a parallel coordinates chart, and reload the chart, the scale of the axis isn't refreshed.
I tried calling .autoscale(), but if I call it before .render(), it has no effect, and if I call it after .render(), then no polylines are drawn anymore.
$("#example").html(""); //to make sure "ghost" axes are removed
parcoords = d3.parcoords()("#example")
.data(parsedData)
.createAxes()
.brushMode("1D-axes")
.reorderable()//;
.detectDimensions()
.dimensions(dimensionObj)
.autoscale() //no visible effect if placed here
.render()
.updateAxes();
Not sure if this is related (although the issue started at the same time), I started specifying an order for the dimensions. To do this, instead of using an array containing the axes (dimensionArray), I use a JS object dimensionObj containing numbered "index" keys, as follows:
//dimensionArray = ["axis1", "axis2", "axis3", "axis4"]; //orderless array
//Default axes to display with predefined ordering
dimensionObj = {
"axis1": {index:0},
"axis2": {index:1},
"axis3": {index:2},
"axis4": {index:3}
}
For illustration purposes, the following screenshots show how on the top image, the scales are properly set, but on the second (updated) chart, some new polylines are going to 0 on the 1st and 3rd axis, but the scale isn't updated so lines go out of range.
Is there a simple way to refresh the axis scales when reloading a chart? Is it possible that using the JS object in .dimensions() is creating some conflicts with the underlying scale function?
Found what was causing this behaviour: an if statement in d3.parcoords.js pc.autoscale function which was only resetting scales if the yscale was not previously defined. Essentially I edited the if statement from original:
d3.keys(__.dimensions).forEach(function(k) {
if (!__.dimensions[k].yscale){
__.dimensions[k].yscale = defaultScales[__.dimensions[k].type](k);
}
});
to this (of course the if statement could be dropped altogether, I simply kept it in this form in case I need to revert to original version later for any reason):
d3.keys(__.dimensions).forEach(function(k) {
if (!__.dimensions[k].yscale || __.dimensions[k].yscale){
__.dimensions[k].yscale = defaultScales[__.dimensions[k].type](k);
}
});

Zoomable sunburst visualization comes out messed up

I am reading a spreadsheet using sheetsee.js (and tabletop.js) and trying to create a zoomable sunburst with labels visualization. However, it is only creating one level and the text is rotated either +90 or -90. The URL to my html page is http://www.wyzpubs.com/dataviz/sheetsee/dita_users_sb.html
Can someone tell me what could be causing this? I think the way I am creating the JSON with size information is OK and it is exactly like in metmajer's zoomable sunburst with labels.
Thanks,
Jayaram
The bug is here in sheetsee.sunburst.js
var partition = d3.layout.partition()
.value(function(d) { return d.size; });
Your data has nothing like size(in the json) thus everything gets collapsed.
It should have been some value to decide the arc length size
I did something like this(but you can change it to some biz logic of yours):
var partition = d3.layout.partition()
.value(function(d) { return d.parent.children.length; });
Working code here
Hope this helps!

d3.v3 scatterplot with all circles the same radius

Every example I have found shows all of the scatter plot points to be of random radii. Is it possible to have them all the same size? If I try to statically set the radius all of the circles will be very small (I'm assuming the default radius). However, if I use Math.random() as in most examples there are circles large and small. I want all the circles to be large. Is there a way to do that? Here's the code snippet forming the graph data using Math.random() (this works fine for some reason):
function scatterData(xData, yData)
{
var data = [];
for (var i = 0; i < seismoNames.length; i++)
{
data.push({
key: seismoNames[i],
values: []
});
var xVals=""+xData[i];
xVals=xVals.split(",");
var yVals=""+yData[i];
yVals=yVals.split(",");
for (var j = 0; j < xVals.length; j++)
{
data[i].values.push({
x: xVals[j],
y: yVals[j],
size: Math.random()
});
}
}
return data;
}
Math.random() spits out values between 0 and 1 such as 0.164259538891095 and 0.9842195005008699. I have tried putting these as static values in the 'size' attribute, but no matter what the circles are always really small. Is there something I'm missing?
Update: The NVD3 API has changed, and now uses pointSize, pointSizeDomain, etc. instead of just size. The rest of the logic for exploring the current API without complete documentation still applies.
For NVD3 charts, the idea is that all adjustments you make can be done by calling methods on the chart function itself (or its public components) before calling that function to draw the chart in a specific container element.
For example, in the example you linked too, the chart function was initialized like this:
var chart = nv.models.scatterChart()
.showDistX(true)
.showDistY(true)
.color(d3.scale.category10().range());
chart.xAxis.tickFormat(d3.format('.02f'));
chart.yAxis.tickFormat(d3.format('.02f'));
The .showDistX() and .showDistY() turn on the tick-mark distribution in the axes; .color() sets the series of colours you want to use for the different categories. The next too lines access the default axis objects within the chart and set the number format to be a two-digit decimal. You can play around with these options by clicking on the scatterplot option from the "Live Code" page.
Unfortunately, the makers of the NVD3 charts don't have a complete documentation available yet describing all the other options you can set for each chart. However, you can use the javascript itself to let you find out what methods are available.
Inspecting a NVD3.js chart object to determine options
Open up a web page that loads the d3 and nvd3 library. The live code page on their website works fine. Then open up your developer's console command line (this will depend on your browser, search your help pages if you don't know how yet). Now, create a new nvd3 scatter chart function in memory:
var testChart = nv.models.scatterChart();
On my (Chrome) console, the console will then print out the entire contents of the function you just created. It is interesting, but very long and difficult to interpret at a glance. And most of the code is encapsulated so you can't change it easily. You want to know which properties you can change. So run this code in the next line of your console:
for (keyname in testChart){console.log(keyname + " (" + typeof(testChart[keyname]) + ")");}
The console should now print out neatly the names of all the methods and objects that you can access from that chart function. Some of these will have their own methods and objects you can access; discover what they are by running the same routine, but replacing the testChart with testChart.propertyName, like this:
for (keyname in testChart.xAxis){console.log(keyname + " (" + typeof(testChart.xAxis[keyname]) + ")");}
Back to your problem. The little routine I suggested above doesn't sort the property names in any order, but skimming through the list you should see three options that relate to size (which was the data variable that the examples were using to set radius)
size (function)
sizeDomain (function)
sizeRange (function)
Domain and range are terms used by D3 scales, so that gives me a hint about what they do. Since you don't want to scale the dots, let's start by looking at just the size property. If you type the following in the console:
testChart.size
It should print back the code for that function. It's not terribly informative for what we're interested in, but it does show me that NVD3 follows D3's getter/setter format: if you call .property(value) you set the property to that value, but if you call .property() without any parameters, it will return back the current value of that property.
So to find out what the size property is by default, call the size() method with no parameters:
testChart.size()
It should print out function (d) { return d.size || 1}, which tells us that the default value is a function that looks for a size property in the data, and if it doesn't exist returns the constant 1. More generally, it tells us that the value set by the size method determines how the chart gets the size value from the data. The default should give a constant size if your data has no d.size property, but for good measure you should call chart.size(1); in your initialization code to tell the chart function not to bother trying to determine size from the data and just use a constant value.
Going back to the live code scatterplot can test that out. Edit the code to add in the size call, like this:
var chart = nv.models.scatterChart()
.showDistX(true)
.showDistY(true)
.color(d3.scale.category10().range())
.size(1);
chart.xAxis.tickFormat(d3.format('.02f'));
chart.yAxis.tickFormat(d3.format('.02f'));
Adding that extra call successfully sets all the dots to the same size -- but that size is definitely not 1 pixel, so clearly there is some scaling going on.
First guess for getting bigger dots would be to change chart.size(1) to chart.size(100). Nothing changes, however. The default scale is clearly calculating it's domain based on the data and then outputting to a standard range of sizes. This is why you couldn't get big circles by setting the size value of every data element to 0.99, even if that would create a big circle when some of the data was 0.01 and some was 0.99. Clearly, if you want to change the output size, you're going to have to set the .sizeRange() property on the chart, too.
Calling testChart.sizeRange() in the console to find out the default isn't very informative: the default value is null (nonexistent). So I just made a guess that, same as the D3 linear scale .range() function, the expected input is a two-element array consisting of the max and min values. Since we want a constant, the max and min will be the same. So in the live code I change:
.size(1);
to
.size(1).sizeRange([50,50]);
Now something's happening! But the dots are still pretty small: definitely not 50 pixels in radius, it looks closer to 50 square pixels in area. Having size computed based on the area makes sense when sizing from the data, but that means that to set a constant size you'll need to figure out the approximate area you want: values up to 200 look alright on the example, but the value you choose will depend on the size of your graph and how close your data points are to each other.
--ABR
P.S. I added the NVD3.js tag to your question; be sure to use it as your main tag in the future when asking questions about the NVD3 chart functions.
The radius is measured in pixels. If you set it to a value less than one, yes, you will have a very small circle. Most of the examples that use random numbers also use a scaling factor.
If you want all the circles to have a constant radius you don't need to set the value in the data, just set it when you add the radius attribute.
Not sure which tutorials you were looking at, but start here: https://github.com/mbostock/d3/wiki/Tutorials
The example "Three little circles" does a good step-by-step of the different things you can do with circles:
http://mbostock.github.io/d3/tutorial/circle.html

second d3.js triggered from first doesn't fully iterate over X axis

Folks -
I'm now trying to trigger a second chart based on which series in the first chart is clicked on.
Based on which is chosen, one of two data sets are sent to the function:
.on("mouseup", function(d) {return d.myCat == 0 ? updateData(yesXYZData) : updateData(nonXYZData)})
This part works, but I'm getting one big stack in the target div, not the iteration I am expecting.
function updateData(whichDataSet) {...
I've tried putting the updateData() function into the window.onload function, duping or reusing various elements (since the domain and range for the X axis are the same, I expect to reuse).
[Note- I have taken Lars Kothoff's advice regarding numbers in the data object. Also, I will create a better data structure later, using crossfilter.js and native d3.js data manipulation- for now I need a working prototype demonstrating functionality.]
here is the gist:
https://gist.github.com/RCL1/6906892
Thanks in advance!
-RL
line 242 of the gist. I needed to use a non-filtered version of the data to calculate x axis (totalAll.map).

Categories

Resources