Dropdown menu in plotly.js to switch between charts - javascript

I'm trying to create a way to automatically visualise many different types of data on an online dashboard. I'm using plotly.js (built on top of d3) to make these visualisations. For now, next to the default chart, I want to create a drop down box that allows you to switch to other chart types, but I can't get it to work and haven't found examples of something similar done in plotly. Any help would be great!
My code is below which queries the server for the data, and then plots it in a histogram. I can get individual charts to work, just not with the drop down box. I'm not even sure if what I've done by making the drop down object a function is allowed, and if I can use it to change the data to be plotted.
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<title>Simple Bar Chart</title>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script> </script>
<style>
.bar {
fill: steelblue;
}
.bar:hover {
fill: brown;
}
</style>
</head>
<body>
<div id="chart" style="width:90%;height:600px;"></div>
<!-- For plotly code to bind to -->
<div id="drop-down"></div>
<script>
// set the dimensions and margins of the graph
var margin = { top: 20, right: 20, bottom: 80, left: 40 };
var width = 960 - margin.left - margin.right;
var height = 500 - margin.top - margin.bottom;
// append the svg object to the body of the page
// append a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// sends asynchronous request to the url
var HttpClient = function() {
this.get = function(aUrl, aCallback) {
var anHttpRequest = new XMLHttpRequest();
anHttpRequest.onreadystatechange = function() {
if (anHttpRequest.readyState == 4 && anHttpRequest.status == 200) {
aCallback(anHttpRequest.responseText);
}
}
anHttpRequest.open("GET", aUrl, true);
anHttpRequest.send(null);
}
};
var client = new HttpClient();
//hard coded URL for now, will accept from UI later
myURL = "https://neel-dot-village-test.appspot.com/_ah/api/searchApi/v1/fetchChartData?chartSpecs=%7B%22axis1%22%3A+%22name%22%2C+%22axis2%22%3A%22cumulativeNumbers.totalBudget%22%7D&topicType=%2Ftype%2Ftata%2Fproject";
client.get(myURL, function(response) {
var jresp = JSON.parse(response); //get response as JS object
plotHist(JSON.parse(jresp.json));
});
var chooseChart = function(data){
buttons: [{
method: plotHist,
args: data,
label: 'Histogram'
}, {
method: plotBar,
args: data,
label: 'Bar Chart'
}]
};
var plotHist = function(data) {
var plotdata = [{
x: data.y.values,
type: 'histogram',
marker: {
//color: 'rgba(100,250,100,0.7)'
},
}];
var layout = {
xaxis: {
title: data.y.label,
rangeslider: {} }, //does not automatically adjust bin sizes though
yaxis: { title: "Count" },
updatemenus: chooseChart(data),
autobinx: true
};
Plotly.newPlot('chart', plotdata, layout);
};
var plotBar = function(data) { //using plotly (built on d3)
var plotdata = [{
x: data.x.values,
y: data.y.values,
type: 'bar'
}];
var layout = {
xaxis: { title: data.x.label },
yaxis: { title: data.y.label },
updatemenus: chooseChart(data)
};
Plotly.newPlot('chart', plotdata, layout);
};
</script>
</body>

Related

Bar chart to take account of negative values

I have the following data in a csv file called BarData.csv:
Fruit,dt,amount
Apple,12/28/2016,-1256
Apple,12/29/2016,-500
Apple,12/30/2016,3694
Apple,12/31/2016,5586
Apple,1/1/2017,4558
Apple,1/2/2017,6696
Apple,1/3/2017,7757
Apple,1/4/2017,8528
Apple,1/5/2017,5543
Apple,1/6/2017,3363
Apple,1/7/2017,5464
Pear,12/25/2017,250
Pear,12/26/2017,669
Pear,12/27/2017,441
Pear,12/28/2017,159
Pear,12/29/2017,357
Pear,12/30/2017,775
Pear,12/31/2017,669
The following html, css, and javascript is in one .html file:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>BAR SINGLE FUNCTION</title>
<script src="http://d3js.org/d3.v3.js"></script>
<style type="text/css">
#radioDiv {
top: 45px;
font-family: verdana;
font-size: 8px;
width: 455px;
}
#TOPbarChart {
position: absolute;
top: 50px;
left: 30px;
width: 750px;
height: 195px;
}
.axis--y path,
.axis--x path {
display: none;
}
.axis--x line,
.axis--y line {
stroke: black;
fill: none;
stroke-width: 2px
}
.yAxis text,
.xAxis text {
font: 7pt Verdana;
stroke: none;
fill: black;
}
.title,
.titleX {
font-family: Verdana;
font-size: 10px;
}
</style>
</head>
<body>
<div id="radioDiv">
<label>
<input id="radioFrt" type="radio" name="frt" value="Apple" class="radioB" checked> APPLE
</label>
<label>
<input type="radio" name="frt" value="Pear" class="radioB"> PEAR
</label>
</div>
<div id="TOPbarChart"></div>
<script type="text/javascript">
var currentFruit = "Apple";
var currentColr = "#00a5b6";
var barDataCSV_Dly = "BarData.csv";
//
//
// radio button
document.getElementById("radioFrt").checked = true;
d3.selectAll('input[name="frt"]').on("change", function change() {
currentFruit = this.value;
TOPbarChart(currentFruit, currentColr);
});
//FORMATS
var parseDate = d3.time.format("%m/%d/%Y").parse;
//
// BASIC SIZING
//
function barChartBasics() {
var margin = {
top: 25,
right: 35,
bottom: 25,
left: 70
},
width = 550 - margin.left - margin.right,
height = 155 - margin.top - margin.bottom,
colorBar = d3.scale.category20(),
barPaddingFine = 1,
barPaddingThick = 2;
return {
margin: margin,
width: width,
height: height,
colorBar: colorBar,
barPaddingFine: barPaddingFine,
barPaddingThick: barPaddingThick
};
}
// create svg element
var basics = barChartBasics();
var svg = d3.select("#TOPbarChart")
.append("svg")
.attr({
"width": basics.width + basics.margin.left + basics.margin.right,
"height": basics.height + basics.margin.top + basics.margin.bottom,
id: "svgTOPbarChart"
});
// create svg group
var plot = svg
.append("g")
.attr({
"transform": "translate(" + basics.margin.left + "," + basics.margin.top + ")",
id: "svgPlotTOPbarChart"
});
var axisPadding = 2;
var leftAxisGroup = svg
.append('g')
.attr({
transform: 'translate(' + (basics.margin.left - axisPadding) + ',' + (basics.margin.top) + ')',
'class': "yAxis axis--y",
id: "yAxisGTOPbarChart"
});
var bottomAxisGroup = svg
.append('g')
.attr({
'class': "xAxis axis--x",
id: "xAxisGTOPbarChart"
});
var titleTxt = svg.append("text")
.attr({
x: basics.margin.left + 12,
y: 20,
'class': "title",
'text-anchor': "start"
})
// create scales with ranges
var xScale = d3.time.scale().range([0, basics.width]);
var yScale = d3.scale.linear().range([basics.height, 0]);
function TOPbarChart(
frt, colorChosen) {
// get the data
d3.csv(barDataCSV_Dly, function(rows) {
TOPbarData = rows.map(function(d) {
return {
"Fruit": d.Fruit,
"dt": parseDate(d.dt),
"amount": +d.amount
};
}).filter(function(row) {
if (row['Fruit'] == frt) {
return true;
}
});
// create domains for the scales
xScale.domain(d3.extent(TOPbarData, function(d) {
return d.dt;
}));
var amounts = TOPbarData.map(function(d) {
return d.amount;
});
var yMax = d3.max(amounts);
var yMin = d3.min(amounts);
var yMinFinal = 0;
if (yMin < 0) {
yMinFinal = yMin;
}
yScale.domain([yMinFinal, yMax]);
// introduce the bars
// var plot = d3.select("#svgPlotTOPbarChart")
var sel = plot.selectAll("rect")
.data(TOPbarData);
sel.enter()
.append("rect")
.attr({
x: function(d, i) {
return xScale(d.dt);
},
y: function(d) {
return yScale(d.amount);
},
width: (basics.width / TOPbarData.length - basics.barPaddingFine),
height: function(d) {
return basics.height - yScale(d.amount);
},
fill: colorChosen,
'class': "bar"
});
// this little function will create a small ripple affect during transition
var dlyRipple = function(d, i) {
return i * 100;
};
sel
.transition()
.duration(dlyRipple) //1000
.attr({
x: function(d, i) {
return xScale(d.dt);
},
y: function(d) {
return yScale(d.amount);
},
width: (basics.width / TOPbarData.length - basics.barPaddingFine),
height: function(d) {
return basics.height - yScale(d.amount);
},
fill: colorChosen
});
sel.exit().remove();
// add/transition y axis - with ticks and tick markers
var axisY = d3.svg.axis()
.orient('left')
.scale(yScale)
.tickFormat(d3.format("s")) // use abbreviations, e.g. 5M for 5 Million
.outerTickSize(0);
leftAxisGroup.transition().duration(1000).call(axisY);
// add/transition x axis - with ticks and tick markers
var axisX = d3.svg.axis()
.orient('bottom')
.scale(xScale);
bottomAxisGroup
.attr({
transform: 'translate(' + (basics.margin.left + ((basics.width / TOPbarData.length) / 2)) + ',' + (basics.margin.top + basics.height) + ')',
})
.transition().duration(1000).call(axisX.ticks(5));
titleTxt.text("Daily: last " + TOPbarData.length + " days");
// console.log(TOPbarData.length)
});
}
//
//
//
//
TOPbarChart(currentFruit, currentColr);
//
//
//
//
</script>
</body>
</html>
When all the data is positive everything is pretty much ok - but when some of the data is negative we can see the result in this plunker demo:
http://plnkr.co/edit/1hudJYkRq2MnuIlwxXZi?p=preview
How do I amend the code so that:
- the negative bars are shown?
- the base of the positive bars moves vertically up when negative numbers are included?
- the vertical movement is also included in the transition?
Above is more than 1 question but help on any would be appreciated.
The key is to play with the y and height attributes of the bars to position them correctly.
For y, change it to:
y: function(d) {
return yScale(Math.max(0, d.amount));
},
And for the height, change it to:
height: function(d) {
return Math.abs(yScale(d.amount) - yScale(0));
},
You can then style the negative bars to make them a different color.
Check the updated Plunkr - http://plnkr.co/edit/q7dQsPW0PiPuwFTy8gLN?p=preview
Edit:
For the coloring part, you can achieve it with a 1 liner if you want to reduce lines and want more simplicity.
Instead of:
fill: function(d) {
var col = colorChosen
if (d.amount < 0) {
col = "#FF0000";
}
return col;
},
});
You can do:
fill: function(d) {
return d.amount < 0 ? "#FF0000" : colorChosen;
},

Switch for making background map from Mapbox visible/invisible in D3

I'm building a Voronoi map inspired by http://chriszetter.com/blog/2014/06/15/building-a-voronoi-map-with-d3-and-leaflet/. I'd like to make an option to turn off the background map as the location of the data may not be relevant in all my use cases. Furthermore, it would be great if the visualization could work offline this way. After toggling the switch, the entire background would be white. The Voronoi overlap would be the same. How do I do that? Here's the code (zip-file contains the csv-files): https://www.dropbox.com/s/i8vtfh8mkxazfr0/voronoi-maps-master.zip?dl=0
EDIT: There's two layer variables in the original code as I was trying to split the visualization into two. However, that attempt was unsuccessful and only mapLayer is used. This may not have been clear in the original question.
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link href="base.css" rel="stylesheet" />
<link href='https://api.tiles.mapbox.com/mapbox.js/v1.6.3/mapbox.css' rel='stylesheet' />
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id='map'>
</div>
<div id='loading'>
</div>
<!-- <div id='selected'>
<h1>...</h1>
</div> -->
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min.js"></script>
<script src="https://api.tiles.mapbox.com/mapbox.js/v1.6.3/mapbox.js"></script>
<!-- <script src="/voronoi-map/lib/voronoi_map.js"></script> -->
<script type="text/javascript" src="voronoi_map.js"></script>
<script>
map = L.mapbox.map('map', 'zetter.i73ka9hn') // <- dur ikke!
.fitBounds([[59.355596 , -9.052734], [49.894634 , 3.515625]]);
url = 'supermarkets.csv';
initialSelection = d3.set(['Tesco', 'Sainsburys']);
voronoiMap(map, url, initialSelection);
</script>
</body>
</html>
voronoi_map.js
voronoiMap = function(map, url, initialSelections) {
var pointTypes = d3.map(),
points = [],
lastSelectedPoint;
var voronoi = d3.geom.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
var selectPoint = function() {
d3.selectAll('.selected').classed('selected', false);
var cell = d3.select(this),
point = cell.datum();
lastSelectedPoint = point;
cell.classed('selected', true);
d3.select('#selected h1')
.html('')
.append('a')
.text( /*point.name*/ "8 interactions from this cell")
/* .attr('href', point.url)
.attr('target', '_blank') */
}
var drawWithLoading = function(e){
d3.select('#loading').classed('visible', true);
if (e && e.type == 'viewreset') {
d3.select('#overlay').remove();
}
setTimeout(function(){
draw();
d3.select('#loading').classed('visible', false);
}, 0);
}
var draw = function() {
d3.select('#overlay').remove();
var bounds = map.getBounds(),
topLeft = map.latLngToLayerPoint(bounds.getNorthWest()),
bottomRight = map.latLngToLayerPoint(bounds.getSouthEast()),
existing = d3.set(),
drawLimit = bounds.pad(0.4);
filteredPoints = points.filter(function(d) {
var latlng = new L.LatLng(d.latitude, d.longitude);
if (!drawLimit.contains(latlng)) { return false };
var point = map.latLngToLayerPoint(latlng);
key = point.toString();
if (existing.has(key)) { return false };
existing.add(key);
d.x = point.x;
d.y = point.y;
return true;
});
voronoi(filteredPoints).forEach(function(d) { d.point.cell = d; });
var svg = d3.select(map.getPanes().overlayPane).append("svg")
.attr('id', 'overlay')
.attr("class", "leaflet-zoom-hide")
.style("width", map.getSize().x + 'px')
.style("height", map.getSize().y + 'px')
.style("margin-left", topLeft.x + "px")
.style("margin-top", topLeft.y + "px");
var g = svg.append("g")
.attr("transform", "translate(" + (-topLeft.x) + "," + (-topLeft.y) + ")");
var svgPoints = g.attr("class", "points")
.selectAll("g")
.data(filteredPoints)
.enter().append("g")
.attr("class", "point");
var buildPathFromPoint = function(point) {
return "M" + point.cell.join("L") + "Z";
}
svgPoints.append("path")
.attr("class", "point-cell")
.attr("d", buildPathFromPoint)
//.style('fill', function(d) { return '#' + d.color } )
.on('click', selectPoint)
.classed("selected", function(d) { return lastSelectedPoint == d} );
/* svgPoints.append("circle")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.style('fill', function(d) { return '#' + d.color } )
.attr("r", 2); */
}
/* function interactionGradient() {
color =
if
return
} */
var mapLayer = {
onAdd: function(map) {
map.on('viewreset moveend', drawWithLoading);
drawWithLoading();
}
};
var voronoiLayer = {
onAdd: function(map) {
map.on('viewreset moveend', drawWithLoading);
drawWithLoading();
}
};
map.on('ready', function() {
d3.csv(url, function(csv) {
points = csv;
points.forEach(function(point) {
pointTypes.set(point.type, {type: point.type, color: point.color});
})
map.addLayer(mapLayer);
})
});
}
You need to load the tileLayer seperately so you can create a reference to it which you can then in turn use to create a layercontrol which can easily enable/disable layers:
L.mapbox.accessToken = 'pk.eyJ1IjoicGF1bC12ZXJob2V2ZW4iLCJhIjoiZ1BtMWhPSSJ9.wQGnLyl1DiuIQuS0U_PtiQ';
// Create the tileLayer.
var tileLayer = L.mapbox.tileLayer('examples.map-i86nkdio');
var map = L.mapbox.map('mapbox', null, { // Do not add as parameter
'center': [0, 0],
'zoom': 1,
// Add here so it still gets added to the map initially
// You could skip this so it won't be added and you can
// turn it on via the layercontrol
'layers': [tileLayer]
});
// Create layer control
var layerControl = L.control.layers(null, {
'Tilelayer': tileLayer // Add tile layer to overlays
}).addTo(map);
Here's a working example on Plunker: http://plnkr.co/edit/5re3o6qnyCwAqXNYXrkP?p=preview
L.mapbox.tileLayer reference: https://www.mapbox.com/mapbox.js/api/v2.1.5/l-mapbox-tilelayer/
L.control.layers reference: http://leafletjs.com/reference.html#control-layers
Edit because of the comments:
You're already working with separate layers, the tilelayer (background) gets added upon map initalization L.mapbox.map('map', 'zetter.i73ka9hn') that in fact calls: L.mapbox.tileLayer('zetter.i73ka9hn').addTo(map). You'll need to do it that way because you'll need a reference to the layer so you can add it to L.control.layers like shown above. Your voronoi layer gets added in the voronoiMap method in the ready handler of the map: map.addLayer(mapLayer);
Thus as you can see they are already separated. Now if you also want to be able to toggle the voronoi layer in your layer control you'll need to add it to the layer control:
map.on('ready', function() {
d3.csv(url, function(csv) {
points = csv;
points.forEach(function(point) {
pointTypes.set(point.type, {
type: point.type,
color: point.color
});
});
map.addLayer(mapLayer);
layerControl.addOverlay(mapLayer, 'Voronoi'); // Here
})
});
But that in itself is not enough in your case because your layer doesn't have a onRemove method as prescribed by the ILayer interface:
http://leafletjs.com/reference.html#ilayer
Now if we add a onRemove method to your layer like this:
var mapLayer = {
onAdd: function(map) {
map.on('viewreset moveend', drawWithLoading);
drawWithLoading();
},
onRemove: function (map) {
d3.select('#overlay').remove();
}
};
It works: http://plnkr.co/edit/3z3pCAo0gGuA7xqnqiqb?p=preview (note i've commented out the ready handler because the map is ready before the function call so it wouldn't fire and changed some colors to make things more clear.) Hope this helps.

How to center d3.js map after changing projection on the fly?

I am trying to create a map where the user can select a new projection and it'll be applied on the fly.
here is what I did (you can find a demo here)
<!DOCTYPE html>
<html>
<head>
<meta content="utf-8">
<style type="text/css">
svg {
border: 1px solid #ccc;
}
path {
fill: #ccc;
stroke: #fff;
stroke-width: .5px;
}
path:hover {
fill: #00AEEC;
}
#projectionList {
width: 200px;
height: 500px;
float: left;
border: 1px solid grey;
}
</style>
</head>
<body>
<div id="mainContainer">
<div id="projectionList">
</div>
<div id="graphContainer">
</div>
</div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script type="text/javascript">
// d3.js graph
var width = 900,
height = 500;
var path = d3.geo.path();
var svg = d3.select("#graphContainer").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("tunisia.json", function(error, topology) {
console.log(topology);
//console.clear();
var featureCollection = topojson.feature(topology, topology.objects.governorates);
var bounds = d3.geo.bounds(featureCollection);
var centerX = d3.sum(bounds, function(d) {return d[0];}) / 2,
centerY = d3.sum(bounds, function(d) {return d[1];}) / 2;
var projection = d3.geo.mercator()
.scale(3000)
.center([centerX, centerY]);
path.projection(projection);
svg.selectAll("path")
.data(featureCollection.features)
.enter().append("path")
.attr("d", path);
});
//end of d3.js graph
var selectProjection = {projection_list: []};
// add the list of supported projection
/*schema of objects:
{
id: "unique_int",
projection_name: "a name",
projection_function: "a function name",
projection_description: "a description"
}
*/
//add first projection
selectProjection.projection_list.push({
id: 1,
projection_name: "azimuthalEqualArea", // to be updated with a more descriptive name
projection_function: "azimuthalEqualArea",
projection_description: "azimuthalEqualArea" // to be updated with more detailed description
});
// add second projection
selectProjection.projection_list.push({
id: 2,
projection_name: "conicConformal", // to be updated with a more descriptive name
projection_function: "conicConformal",
projection_description: "conicConformal" // to be updated with more detailed description
});
// add thrid projection
selectProjection.projection_list.push({
id: 3,
projection_name: "equirectangular", // to be updated with a more descriptive name
projection_function: "equirectangular",
projection_description: "equirectangular" // to be updated with more detailed description
});
selectProjection.projection_list.push({
id: 4,
projection_name: "Mercator", // to be updated with a more descriptive name
projection_function: "mercator",
projection_description: "mercator" // to be updated with more detailed description
});
// the above storage method is used temporary in order to leverage a demo, in production mode, there will be a database table to hold these information
function set_projection(option) {
proj = option.value
projection = eval("d3.geo."+proj+"().scale(500);");
path = d3.geo.path()
.projection(projection);
svg.selectAll("path").transition()
.duration(1000)
.attr("d", path);
/*var link = "https://github.com/mbostock/d3/wiki/Geo-Projections#wiki-" + proj
d3.selectAll("div#current_proj_info").html("Current projection: "+option.text+"");*/
}
function apply_projection(e){
// applying a new projection
set_projection(e.target[e.target.selectedIndex]);
}
var newSelect = document.createElement("select");
newSelect.id = "selectProjection";
newSelect.onchange = apply_projection;
for (var i = selectProjection.projection_list.length - 1; i >= 0; i--) {
console.log(selectProjection.projection_list[i].projection_name);
newSelect[newSelect.length] = new Option(selectProjection.projection_list[i].projection_name, selectProjection.projection_list[i].projection_function, false, false);
};
console.log("newSelect content");
console.log(newSelect);
document.getElementById('projectionList').appendChild(newSelect);
</script>
</body>
</html>
the issue is that when I try to scale graph after applying the new projection, it gets outside of the visible area of the div.
Could you please check what's the issue.

bootstrap layout with nvd3.js

I recently started to look into d3.js. Though im pretty new in html/css/javascript; my current goal is to build a simple (static) dashboard using nvd3.js (for easier graphs) and bootstrap as layout component.
I started off with nvd3.js example "pie.html" and try to arrange the graphs in one line using bootstraps grid system. I got it working with d3 only using the d3noob.org example. Sadly, its not working with nvd3.
I pasted the full code below:
<!DOCTYPE html>
<meta charset="utf-8">
<link href="nvd3/src/nv.d3.css" rel="stylesheet" type="text/css">
<link href="bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen">
<style>
body {
overflow-y:scroll;
}
text {
font: 12px sans-serif;
}
</style>
<div class="row">
<div class="span5" id="area1"></div>
<div class="span2"> This is some random text which should be in the middle of these two charts </div>
<div class="span5" id="area2"></div>
</div>
<body class='with-3d-shadow with-transitions'>
<!-- load the libraries -->
<script src="nvd3/lib/d3.v3.js"></script>
<script src="nvd3/nv.d3.js"></script>
<script src="nvd3/src/models/pie.js"></script>
<script src="nvd3/src/utils.js"></script>
<script src="http://code.jquery.com/jquery.js"></script>
<script src="bootstrap/js/bootstrap.min.js"></script>
<script>
var testdata = [
{
key: "One",
y: 5
},
{
key: "Two",
y: 2
},
{
key: "Three",
y: 9
},
{
key: "Four",
y: 7
},
{
key: "Five",
y: 4
},
{
key: "Six",
y: 3
}
];
nv.addGraph(function() {
var width = nv.utils.windowSize().width - 40,
height = nv.utils.windowSize().height / 2 - 40;
var chart = nv.models.pie()
.values(function(d) { return d })
.width(width)
.height(height);
d3.select("#area1").append("svg")
.datum([testdata])
.transition().duration(1200)
.attr('width', width)
.attr('height', height)
.call(chart);
return chart;
});
nv.addGraph(function() {
var width = nv.utils.windowSize().width - 40,
height = nv.utils.windowSize().height / 2 - 40;
var chart = nv.models.pie()
.values(function(d) { return d })
.width(width)
.height(height)
.donut(true);
d3.select("#area2").append("svg")
.datum([testdata])
.transition().duration(1200)
.attr('width', width)
.attr('height', height)
.call(chart);
return chart;
});
</script>
</body>
What it looks like:
The d3+bootstrap example: https://gist.github.com/d3noob/6a3b59149cf3ebdb3fc4
What it should look like:
class "spanX" is outdated in Bootstrap 3.x
Using the class "col-xx-x" is making it work.

writing a Javascript function outside of scope to fire inside the scope

I use the D3 visualization library for a lot of projects and find myself copying and pasting a lot of boilerplate code for each one. Most projects, for example, start out like this:
var margin = {top: 20, right: 10, bottom: 30, left: 60},
width = 960,
height = 500;
var svg = d3.select(container_id).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
After this sort of code, every project diverges. Part of the joy of D3 is that you do some specialized, creative coding for each new project.
I want to write a lightweight wrapper for the boilerplate code so that I can skip to the fun part each time, and in so doing I realized I don't quite understand how to properly make a complex, reusable Javascript object. Here's what I started with:
var d3mill = function() {
var margin = {top: 20, right: 10, bottom: 30, left: 60},
width = 960,
height = 500;
var svg = d3.select(container_id).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
return {
svg: function() { return svg; },
call: function(f) { f(); }
};
};
I think I want to be able to do this:
var d3m = d3mill();
var test = function() {
console.log(svg);
};
d3.call(test);
I thought (wishfully) that passing the function through call() would cause the function to fire inside the closure of the d3mill instance, thus making svg be defined.
It would be a huge waste of time to expose every variable in the closure to the outside world in the manner of the svg() function above. What's the right way to allow outside functions to operate here?
If you change you code to this:
return {
svg: function() { return svg; },
call: function(f) { f.call(this); }
};
then it should correctly set the context within test to be d3m.
Within that function you should then be able to access this.svg() to get the SVG object, but you will not be able to access the "private" lexically scoped variable svg directly, i.e.:
var d3m = d3mill();
var test = function() {
console.log(this.svg()); // OK
console.log(svg); // not OK - undefined variable
};
d3m.call(test);
You could also just pass the svg parameter to f when it's called:
return {
svg: function() { return svg; },
call: function(f) { return f.call(this, svg); } // also added "return", just in case
};
with usage:
var d3m = d3mill();
var test = function(svg) {
console.log(svg); // now OK - it's a parameter
};
d3m.call(test);
You could also use as a constructor function.
var D3Mill = (function() {
var defaults = {
margin: { top: 20, right: 10, bottom: 30, left: 60 },
width: 960,
height: 500
};
function num(i, def) {
return ("number" === typeof i) ? i : def;
}
function D3Mill(container_id, opts) {
opts = opts || {};
// Use opts.xxx or default.xxx if no opts provided
// Expose all values as this.xxx
var margin = this.margin = (opts.margin || defaults.margin);
var width = this.width = num(opts.width, defaults.width);
var height = this.height = num(opts.height, defaults.height);
this.svg = d3.select(container_id).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
}
D3Mill.prototype.perform = function(f) { return f.call(this); };
return D3Mill;
}());
var d3m = new D3Mill("my_container_id");
// or
var opts = {
width: 1,
height: 1,
margin: { ... }
};
var d3m = new D3Mill("my_container_id", opts);
var test = function() {
console.log(this.svg, this.margin, this.width, this.height);
};
d3m.perform(test);
the following will also give you access to the variables you want inside test.
var d3mill = function() {
this.margin = {top: 20, right: 10, bottom: 30, left: 60},
width = 960,
height = 500;
this.svg = d3.select(container_id).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
};
var d3m = new d3mill();
var test = function() {
console.log(this.svg);
};
test.call(d3m);

Categories