I'm trying to implement bar chart and pie chart similar to the example provided in d3js http://bl.ocks.org/NPashaP/96447623ef4d342ee09b with dropdown menu for user selection.
Goal is to override the existing chart with another chart with legends and labels changed. I tried the below Stack Overflow Question did not work.
Calling alternate chart drawing function based on user selection with d3js
I tried making changes accordingly, but the charts are showing one after another.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body{
width:1060px;
margin:50px auto;
}
path { stroke: #fff; }
path:hover { opacity:0.9; }
rect:hover { fill:blue; }
.axis { font: 10px sans-serif; }
.legend tr{ border-bottom:1px solid grey; }
.legend tr:first-child{ border-top:1px solid grey; }
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path { display: none; }
.legend{
margin-bottom:76px;
display:inline-block;
border-collapse: collapse;
border-spacing: 0px;
}
.legend td{
padding:4px 5px;
vertical-align:bottom;
}
.legendFreq, .legendPerc{
align:right;
width:50px;
}
</style>
<body>
<select id="drop-down">
<option value="line">line</option>
<option value="line1">line1</option>
</select>
<div id='dashboard'>
</div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
function dashboard(id, fData){
var barColor = 'steelblue';
function segColor(c){ return {low:"#807dba", Medium:"#e08214",High:"#41ab5d"}[c]; }
// compute total for each state.
fData.forEach(function(d){d.total=d.freq.low+d.freq.Medium+d.freq.High;});
// function to handle histogram.
function histoGram(fD){
var hG={}, hGDim = {t: 60, r: 0, b: 30, l: 0};
hGDim.w = 500 - hGDim.l - hGDim.r,
hGDim.h = 300 - hGDim.t - hGDim.b;
//create svg for histogram.
var hGsvg = d3.select(id).append("svg")
.attr("width", hGDim.w + hGDim.l + hGDim.r)
.attr("height", hGDim.h + hGDim.t + hGDim.b).append("g")
.attr("transform", "translate(" + hGDim.l + "," + hGDim.t + ")");
// create function for x-axis mapping.
var x = d3.scale.ordinal().rangeRoundBands([0, hGDim.w], 0.1)
.domain(fD.map(function(d) { return d[0]; }));
// Add x-axis to the histogram svg.
hGsvg.append("g").attr("class", "x axis")
.attr("transform", "translate(0," + hGDim.h + ")")
.call(d3.svg.axis().scale(x).orient("bottom"));
// Create function for y-axis map.
var y = d3.scale.linear().range([hGDim.h, 0])
.domain([0, d3.max(fD, function(d) { return d[1]; })]);
// Create bars for histogram to contain rectangles and freq labels.
var bars = hGsvg.selectAll(".bar").data(fD).enter()
// bars.exit().remove()
.append("g").attr("class", "bar");
//a
//create the rectangles.
bars.append("rect")
.attr("x", function(d) { return x(d[0]); })
.attr("y", function(d) { return y(d[1]); })
.attr("width", x.rangeBand())
.attr("height", function(d) { return hGDim.h - y(d[1]); })
.attr('fill',barColor)
.on("mouseover",mouseover)// mouseover is defined below.
.on("mouseout",mouseout);// mouseout is defined below.
//Create the frequency labels above the rectangles.
bars.append("text").text(function(d){ return d3.format(",")(d[1])})
.attr("x", function(d) { return x(d[0])+x.rangeBand()/2; })
.attr("y", function(d) { return y(d[1])-5; })
.attr("text-anchor", "Mediumdle");
function mouseover(d){ // utility function to be called on mouseover.
// filter for selected state.
var st = fData.filter(function(s){ return s.State == d[0];})[0],
nD = d3.keys(st.freq).map(function(s){ return {type:s, freq:st.freq[s]};});
// call update functions of pie-chart and legend.
pC.update(nD);
leg.update(nD);
}
function mouseout(d){ // utility function to be called on mouseout.
// reset the pie-chart and legend.
pC.update(tF);
leg.update(tF);
}
// create function to update the bars. This will be used by pie-chart.
hG.update = function(nD, color){
// update the domain of the y-axis map to reflect change in frequencies.
y.domain([0, d3.max(nD, function(d) { return d[1]; })]);
// Attach the new data to the bars.
var bars = hGsvg.selectAll(".bar").data(nD);
// transition the height and color of rectangles.
bars.select("rect").transition().duration(500)
.attr("y", function(d) {return y(d[1]); })
.attr("height", function(d) { return hGDim.h - y(d[1]); })
.attr("fill", color);
// transition the frequency labels location and change value.
bars.select("text").transition().duration(500)
.text(function(d){ return d3.format(",")(d[1])})
.attr("y", function(d) {return y(d[1])-5; });
}
return hG;
}
// function to handle pieChart.
function pieChart(pD){
var pC ={}, pieDim ={w:250, h: 250};
pieDim.r = Math.min(pieDim.w, pieDim.h) / 2;
// create svg for pie chart.
var piesvg = d3.select(id).append("svg")
.attr("width", pieDim.w).attr("height", pieDim.h).append("g")
.attr("transform", "translate("+pieDim.w/2+","+pieDim.h/2+")");
// create function to draw the arcs of the pie slices.
var arc = d3.svg.arc().outerRadius(pieDim.r - 10).innerRadius(0);
// create a function to compute the pie slice angles.
var pie = d3.layout.pie().sort(null).value(function(d) { return d.freq; });
// Draw the pie slices.
piesvg.selectAll("path").data(pie(pD)).enter()
//piesvg.exit().remove()
.append("path").attr("d", arc)
.each(function(d) { this._current = d; })
.style("fill", function(d) { return segColor(d.data.type); })
.on("mouseover",mouseover).on("mouseout",mouseout);
// create function to update pie-chart. This will be used by histogram.
pC.update = function(nD){
piesvg.selectAll("path").data(pie(nD)).transition().duration(500)
.attrTween("d", arcTween);
}
// Utility function to be called on mouseover a pie slice.
function mouseover(d){
// call the update function of histogram with new data.
hG.update(fData.map(function(v){
return [v.State,v.freq[d.data.type]];}),segColor(d.data.type));
}
//Utility function to be called on mouseout a pie slice.
function mouseout(d){
// call the update function of histogram with all data.
hG.update(fData.map(function(v){
return [v.State,v.total];}), barColor);
}
// Animating the pie-slice requiring a custom function which specifies
// how the intermediate paths should be drawn.
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) { return arc(i(t)); };
}
return pC;
}
// function to handle legend.
function legend(lD){
var leg = {};
// create table for legend.
var legend = d3.select(id).append("table").attr('class','legend');
// create one row per segment.
var tr = legend.append("tbody").selectAll("tr").data(lD).enter().append("tr");
// tr.exit().remove()
// create the first column for each segment.
tr.append("td").append("svg").attr("width", '16').attr("height", '16').append("rect")
.attr("width", '16').attr("height", '16')
.attr("fill",function(d){ return segColor(d.type); });
// create the second column for each segment.
tr.append("td").text(function(d){ return d.type;});
// create the third column for each segment.
tr.append("td").attr("class",'legendFreq')
.text(function(d){ return d3.format(",")(d.freq);});
// create the fourth column for each segment.
tr.append("td").attr("class",'legendPerc')
.text(function(d){ return getLegend(d,lD);});
// Utility function to be used to update the legend.
leg.update = function(nD){
// update the data attached to the row elements.
var l = legend.select("tbody").selectAll("tr").data(nD);
// update the frequencies.
l.select(".legendFreq").text(function(d){ return d3.format(",")(d.freq);});
// update the percentage column.
l.select(".legendPerc").text(function(d){ return getLegend(d,nD);});
}
function getLegend(d,aD){ // Utility function to compute percentage.
return d3.format("%")(d.freq/d3.sum(aD.map(function(v){ return v.freq; })));
}
return leg;
}
// calculate total frequency by segment for all state.
var tF = ['low','Medium','High'].map(function(d){
return {type:d, freq: d3.sum(fData.map(function(t){ return t.freq[d];}))};
});
// calculate total frequency by state for all segment.
var sF = fData.map(function(d){return [d.State,d.total];});
var hG = histoGram(sF), // create the histogram.
pC = pieChart(tF), // create the pie-chart.
leg= legend(tF); // create the legend.
}
</script>
<script>
var freqData=[
{State:'WK-1',freq:{low:10, Medium:20, High:10}}
,{State:'WK-2',freq:{low:30, Medium:10, High:15}}
,{State:'WK-3',freq:{low:40, Medium:45, High:35}}
,{State:'WK-4',freq:{low:15, Medium:4, High:20}}
,{State:'WK-5',freq:{low:50, Medium:8, High:19}}
];
var freqData1=[
{State:'WK-1',freq:{low:20, Medium:20, High:10}}
,{State:'WK-2',freq:{low:10, Medium:20, High:25}}
,{State:'WK-3',freq:{low:20, Medium:25, High:35}}
,{State:'WK-4',freq:{low:65, Medium:4, High:20}}
,{State:'WK-5',freq:{low:50, Medium:8, High:19}}
];
d3.select("#drop-down").on("change", function () {
d3.selectAll(".alt-view").remove();
//d3.exit().remove();
selected = this.value;
if(selected == "line"){dashboard('#dashboard',freqData);}
else if(selected == "line1"){dashboard('#dashboard',freqData1);}
});
</script>
Reviewed your plunker. The issue is that you are trying to remove the previous charts using below call.
d3.selectAll(".alt-view").remove();
However, while building chart elements, you have not assigned alt-view class to any of the elements. All the elements you want to remove, make sure they are contained in a parent element that has alt-view class and you should be good to go.
Alternatively, you can do following to quickly test this.
Replace
d3.selectAll(".alt-view").remove();
with
d3.selectAll("#dashboard").remove();
d3.select('body').append('div').attr('id', 'dashboard');
Related
I have trouble updating the dots and axis of the scatter plot, and corresponding bar chart in the d3 visualization when I select a different attribute for the x-axis or y-axis. As shown in the image.
I reused the code from this example, made some changes to add a drop-down menu for both axis. When changing the axis from the drop-down it adds correct data points but won't remove the previous ones. I need help updating and removing the previous elements if the x-axis or y-axis are changed from the drop-down menu.
<!--
An example of linked views using model.js.
Curran Kelleher August 2014
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="styles.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
<!-- A functional reactive model library. github.com/curran/model -->
<script src="https://curran.github.io/model/cdn/model-v0.2.4.js"></script>
<style>
/* CSS for the visualization.
* Curran Kelleher 4/17/2014 */
/* Size the visualization container. */
#container {
position: fixed;
top: 30px;
bottom: 30px;
left: 30px;
right: 30px;
}
/* Style the visualization.
* This CSS is copied verbatim from the
* D3 scatter plot example found at
* http://bl.ocks.org/mbostock/3887118 */
body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.dot {
fill: black;
}
/* The following CSS is for brushing,
* adapted from http://bl.ocks.org/mbostock/4343214 */
.dot.selected {
fill: red;
}
.brush .extent {
stroke: gray;
fill-opacity: .125;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<div id="container"></div>
<div id = "axisSelectors">
<b>x-axis:</b>
<select id="xSelector" onchange="onCategoryChanged()">
<option value="sepalWidth">sepalWidth</option>
<option value="petalWidth">petalWidth</option>
</select>
<b>y-axis:</b>
<select id="ySelector" onchange="onCategoryChanged()">
<option value="sepalLength">sepalLength</option>
<option value="petalLength">petalLength</option>
</select>
</div>
<script>
function onCategoryChanged() {
var select = d3.select('#xSelector').node();
// Get current value of select element
var x_category = select.options[select.selectedIndex].value;
// Update chart with the selected category of letters
var select = d3.select('#ySelector').node();
var y_category = select.options[select.selectedIndex].value;
main(x_category,y_category);
}
function BarChart (container) {
var defaults = {
margin: {
top: 20,
right: 20,
bottom: 30,
left: 40
},
yAxisNumTicks: 10,
yAxisTickFormat: ""
},
model = Model(defaults),
xAxis = d3.svg.axis().orient("bottom"),
yAxis = d3.svg.axis().orient("left")
svg = d3.select(container).append('svg')
// Use absolute positioning on the SVG element
// so that CSS can be used to position the div later
// according to the model `box.x` and `box.y` properties.
.style('position', 'absolute'),
g = svg.append("g"),
xAxisG = g.append("g").attr("class", "x axis"),
yAxisG = g.append("g").attr("class", "y axis"),
yAxisText = yAxisG.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end");
// Encapsulate D3 Conventional Margins.
// See also http://bl.ocks.org/mbostock/3019563
model.when(["box", "margin"], function (box, margin) {
model.width = box.width - margin.left - margin.right,
model.height = box.height - margin.top - margin.bottom;
});
model.when("margin", function (margin) {
g.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
});
// Adjust Y axis tick mark parameters.
// See https://github.com/mbostock/d3/wiki/Quantitative-Scales#linear_tickFormat
model.when(['yAxisNumTicks', 'yAxisTickFormat'], function (count, format) {
yAxis.ticks(count, format);
});
// Respond to changes in size and offset.
model.when("box", function (box) {
// Resize the svg element that contains the visualization.
svg.attr("width", box.width).attr("height", box.height);
// Set the CSS `left` and `top` properties
// to move the SVG element to `(box.x, box.y)`
// relative to the container div to apply the offset.
svg
.style('left', box.x + 'px')
.style('top', box.y + 'px');
});
model.when("height", function (height) {
xAxisG.attr("transform", "translate(0," + height + ")");
});
model.when(["data", "xAttribute", "width"], function (data, xAttribute, width) {
model.xScale = d3.scale.ordinal()
.rangeRoundBands([0, width], .1)
.domain(data.map(function(d) { return d[xAttribute]; }));
});
model.when(["data", "yAttribute", "height"], function (data, yAttribute, height) {
model.yScale = d3.scale.linear()
.range([height, 0])
.domain([0, d3.max(data, function(d) { return d[yAttribute]; })]);
});
model.when(["xScale"], function (xScale) {
xAxis.scale(xScale)
xAxisG.call(xAxis);
});
model.when(["yScale"], function (yScale) {
yAxis.scale(yScale)
yAxisG.call(yAxis);
});
model.when("yAxisLabel", yAxisText.text, yAxisText);
model.when(["data", "xAttribute", "yAttribute", "xScale", "yScale", "height"],
function (data, xAttribute, yAttribute, xScale, yScale, height) {
var bars = g.selectAll(".bar").data(data);
bars.enter().append("rect").attr("class", "bar");
bars
.attr("x", function(d) { return xScale(d[xAttribute]); })
.attr("width", xScale.rangeBand())
.attr("y", function(d) { return yScale(d[yAttribute]); })
.attr("height", function(d) { return height - yScale(d[yAttribute]); });
bars.exit().remove();
});
return model;
}
// An adaptation of the [D3 scatter plot example](http://bl.ocks.org/mbostock/3887118)
// that uses `model.js`. This version, unlike the original example,
// is model driven and reactive. When a part of the model updates,
// only the parts of the visualization that depend on those parts
// of the model are updated. There are no redundant calls to visualization
// update code when multiple properties are changed simultaneously.
//
// Draws from this brushing example for interaction:
// http://bl.ocks.org/mbostock/4343214
//
// See also docs on quadtree:
// https://github.com/mbostock/d3/wiki/Quadtree-Geom
//
// Define the AMD module using the `define()` function
// provided by Require.js.
//define(['d3', 'model'], function (d3, Model) {
function ScatterPlot (div){
var x = d3.scale.linear(),
y = d3.scale.linear(),
xAxis = d3.svg.axis().scale(x).orient('bottom'),
yAxis = d3.svg.axis().scale(y).orient('left'),
// Use absolute positioning so that CSS can be used
// to position the div according to the model.
svg = d3.select(div).append('svg').style('position', 'absolute'),
g = svg.append('g'),
xAxisG = g.append('g').attr('class', 'x axis'),
yAxisG = g.append('g').attr('class', 'y axis'),
xAxisLabel = xAxisG.append('text')
.attr('class', 'label')
.attr('y', -6)
.style('text-anchor', 'end'),
yAxisLabel = yAxisG.append('text')
.attr('class', 'label')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end'),
// Add the dots group before the brush group,
// so that mouse events go to the brush
// rather than to the dots, even when the mouse is
// on top of a dot.
dotsG = g.append('g'),
brushG = g.append('g')
.attr('class', 'brush'),
brush = d3.svg.brush()
.on('brush', brushed),
dots,
quadtree,
model = Model();
model.when('xLabel', xAxisLabel.text, xAxisLabel);
model.when('yLabel', yAxisLabel.text, yAxisLabel);
model.when('margin', function (margin) {
g.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
});
model.when('box', function (box) {
svg.attr('width', box.width)
.attr('height', box.height);
// Set the CSS `left` and `top` properties
// to move the SVG element to `(box.x, box.y)`
// relative to the container div.
svg
.style('left', box.x + 'px')
.style('top', box.y + 'px')
});
model.when(['box', 'margin'], function (box, margin) {
model.width = box.width - margin.left - margin.right;
model.height = box.height - margin.top - margin.bottom;
});
model.when('width', function (width) {
xAxisLabel.attr('x', width);
});
model.when('height', function (height) {
xAxisG.attr('transform', 'translate(0,' + height + ')');
});
model.when(['width', 'height'], function (width, height) {
brush.x(d3.scale.identity().domain([0, width]));
brush.y(d3.scale.identity().domain([0, height]));
brushG
.call(brush)
.call(brush.event);
});
model.when(['width', 'height', 'data', 'xField', 'yField'], function (width, height, data, xField, yField) {
// Updated the scales
x.domain(d3.extent(data, function(d) { return d[xField]; })).nice();
y.domain(d3.extent(data, function(d) { return d[yField]; })).nice();
x.range([0, width]);
y.range([height, 0]);
// update the quadtree
quadtree = d3.geom.quadtree()
.x(function(d) { return x(d[xField]); })
.y(function(d) { return y(d[yField]); })
(data);
// update the axes
xAxisG.call(xAxis);
yAxisG.call(yAxis);
// Plot the data as dots
dots = dotsG.selectAll('.dot').data(data);
dots.enter().append('circle')
.attr('class', 'dot')
.attr('r', 3.5);
dots
.attr('cx', function(d) { return x(d[xField]); })
.attr('cy', function(d) { return y(d[yField]); });
dots.exit().remove();
});
return model;
function brushed() {
var e = brush.extent(), selectedData;
if(dots) {
dots.each(function(d) { d.selected = false; });
selectedData = search(e[0][0], e[0][1], e[1][0], e[1][1]);
dots.classed('selected', function(d) { return d.selected; });
}
model.selectedData = brush.empty() ? model.data : selectedData;
}
// Find the nodes within the specified rectangle.
function search(x0, y0, x3, y3) {
var selectedData = [];
quadtree.visit(function(node, x1, y1, x2, y2) {
var d = node.point, x, y;
if (d) {
x = node.x;
y = node.y;
d.visited = true;
if(x >= x0 && x < x3 && y >= y0 && y < y3){
d.selected = true;
selectedData.push(d);
}
}
return x1 >= x3 || y1 >= y3 || x2 < x0 || y2 < y0;
});
return selectedData;
}
}
// The main program that assembles the linked views.
//
// Curran Kelleher 4/17/2014
//require(['d3', 'scatterPlot', 'barChart'], function (d3, ScatterPlot, BarChart) {
function main(xvar,yvar){
// Grab the container div from the DOM.
var div = document.getElementById('container'),
// Add both visualizations to the same div.
// Each will create its own SVG element.
scatterPlot = ScatterPlot(div),
barChart = BarChart(div);
// Configure the scatter plot to use the iris data.
scatterPlot.set({
xField: xvar,
yField: yvar,
xLabel: xvar,
yLabel: yvar,
margin: { 'top': 20, 'right': 20, 'bottom': 30, 'left': 40 }
});
// Configure the bar chart to use the aggregated iris data.
barChart.set({
xAttribute: 'species',
yAttribute: 'count',
yAxisLabel: 'number of irises',
margin: { 'top': 20, 'right': 20, 'bottom': 30, 'left': 40 }
});
// Compute the aggregated iris data in response to brushing
// in the scatter plot, and pass it into the bar chart.
scatterPlot.when('selectedData', function (scatterData) {
var speciesCounts = {};
// Aggregate scatter plot data by counting
// the number of irises for each species.
scatterData.forEach(function (d) {
if(!speciesCounts[d.species]){
speciesCounts[d.species] = 0;
}
speciesCounts[d.species]++;
});
// Flatten the object containing species counts into an array.
// Update the bar chart with the aggregated data.
barChart.data = Object.keys(speciesCounts).map(function (species) {
return {
species: species,
count: speciesCounts[species]
};
});
});
// Load the iris data.
d3.tsv('data.tsv', function (d) {
d.sepalLength = +d.sepalLength;
d.sepalWidth = +d.sepalWidth;
d.petalLength = +d.petalLength;
d.petalWidth = +d.petalWidth;
return d;
}, function(error, data) {
// Set sizes once to initialize.
setSizes();
// Set sizes when the user resizes the browser.
window.addEventListener('resize', setSizes);
// Set the data.
scatterPlot.data = data;
});
// Sets the `box` property on each visualization
// to arrange them within the container div.
function setSizes(){
// Put the scatter plot on the left.
scatterPlot.box = {
x: 0,
y: 0,
width: div.clientWidth / 2,
height: div.clientHeight
};
// Put the bar chart on the right.
barChart.box = {
x: div.clientWidth / 2,
y: 0,
width: div.clientWidth / 2,
height: div.clientHeight
};
}
}
main('sepalWidth','sepalLength');
</script>
</body>
</html>
I'm stuck on a small problem regarding force simulation in D3.
I have data representing poverty rates for each country, from 1998 to 2008. It's a bubble chart that's split into three clusters, representing poor countries, not-poor countries, and countries with no information.
When the app is initially loaded, it's loaded with the 1998 data. However, I have some buttons at the top, that, when clicked, will change the year, and subsequently the bubbles should rearrange themselves. All I've been able to do, is when the button is clicked, I change a variable year. However, there are functions and variables that use year throughout the code. When year changes, I want to recalculate all the node properties and force parameters that are depending on year
Here's my code. I've included all of it in case you want to try it out. The data file is at the end of this post.
async function init() {
// Set up the canvas
var height = 1000, width = 2000;
var svg = d3.select("#panel1").append("svg")
.attr("height", height)
.attr("width", width)
.attr("class", "bubblePanel");
var canvas = svg.append("g")
.attr("transform", "translate(0,0)");
// Choose what year to look at, based on button clicks.
var year = "X1998"
d3.select("#b1998").on("click", function() {
year = "X1998"
console.log(year)
// NOTIFY SIMULATION OF CHANGE //
})
d3.select("#b1999").on("click", function() {
year = "X1999"
console.log(year)
// NOTIFY SIMULATION OF CHANGE //
})
d3.select("#b2000").on("click", function() {
year = "X2000"
console.log(year)
// NOTIFY SIMULATION OF CHANGE //
})
// Implement the physics of the elements. Three forces act according to the poverty level (poor, not poor, and no info)
var simulation = d3.forceSimulation()
.force("x", d3.forceX(function(d) {
if (parseFloat(d[year]) >= 10) {
return 1700
} else if (parseFloat(d[year]) === 0) {
return 1000
} else {
return 300
}
}).strength(0.05))
.force("y", d3.forceY(300).strength(0.05))
.force("collide", d3.forceCollide(function(d) {
return radiusScale(d[year])
}));
// Function to pick colour of circles according to region
function pickColor(d) {
if (d === "East Asia & Pacific") {
return "red"
} else if (d === "Europe & Central Asia") {
return "orange"
} else if (d === "Latin America & Caribbean") {
return "yellow"
} else if (d === "Middle East & North Africa") {
return "green"
} else if (d === "North America") {
return "blue"
} else if (d === "South Asia") {
return "indigo"
} else {
return "violet"
}
}
// Set the scales for bubble radius, and text size.
var radiusScale = d3.scaleSqrt().domain([0, 50]).range([20,80]);
var labelScale = d3.scaleSqrt().domain([0,50]).range([10,40]);
// Read the data
await d3.csv("wd3.csv").then(function(data) {
// Assign each data point to a circle that is colored according to region and has radius according to its poverty level
var bubbles = svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", 100)
.attr("cy", 100)
.attr("fill", function(d) {
return pickColor(d.Region)
})
.attr("r", function(d) {
return radiusScale(d[year])
});
// Assign each ddata point to a text element that shows the counry code of the data point. The text is scaled according to the poverty level
var labels = svg.selectAll("text")
.data(data)
.enter().append("text")
.attr("x", 100)
.attr("y", 100)
.attr("dominant-baseline", "central")
.text(function(d) { return d.XCountryCode })
.style("stroke", "black")
.style("text-anchor", "middle")
.style("font-size", function(d) { return labelScale(d[year]); });
// Code to handle the physics of the bubble and the text
simulation.nodes(data)
.on("tick", ticked)
function ticked() {
bubbles.attr("transform", function(d) {
var k = "translate(" + d.x + "," + d.y + ")";
return k;
})
labels.attr("transform", function(d) {
var k = "translate(" + d.x + "," + d.y + ")";
return k;
})
}
});
}
When year changes, the data values will change for each country. I want the following parts of my code to be updated.
The x forces on the nodes: Countries can go from poor in one year to not-poor in another year, so their cluster will change
The radius of the circles: The radius represents poverty level. These change from year to year, so the size of the circles will change when a button is clicked
The coordinates of the country labels: These labels are attached to the data as well. So when the x forces on the circles causes the circles to move, the labels should move as well.
I'd greatly appreciate the help.
The data file can be found here. I accidentally named it povertyCSV, but in the code, it's referenced as "wd3.csv"
If I understand the question correctly:
Re-initializing Forces
The functions provided to set parameters of d3 forces such as forceX or forceCollision are executed once per node at initialization of the simulation (when nodes are originally assigned to the layout). This saves a lot of time once the simulation starts: we aren't recalculating force parameters every tick.
However, if you have an existing force layout and want to modify forceX with a new x value or new strength, or forceCollision with a new radius, for example, we can re-initialize the force to perform the recalculation:
// assign a force to the force diagram:
simulation.force("someForce", d3.forceSomeForce().someProperty(function(d) { ... }) )
// re-initialize the force
simulation.force("someForce").initialize(nodes);
This means if we have a force such as:
simulation.force("x",d3.forceX().x(function(d) { return fn(d["year"]); }))
And we update the variable year, all we need to do is:
year = "newValue";
simulation.force("x").initialize(nodes);
Positioning
If the forces are re-initialized (or re-assigned), there is no need to touch the tick function: it'll update the nodes as needed. Labels and circles will continue to be updated correctly.
Also, non-positional things such as color need to be updated in the event handler that also re-initializes the forces. Other than radius, most things should either be updated via the force or via modifying the elements directly, not both.
Radius is a special case:
With d3.forceCollide, radius affects positioning
Radius, however, does not need to be updated every tick.
Therefore, when updating the radius, we need to update the collision force and modify the r attribute of each circle.
If looking for a smooth transition of radius that is reflected graphically and in the collision force, this should be a separate question.
Implementation
I've borrowed from your code to make a fairly generic example. The below code contains the following event listener for some buttons where each button's datum is a year:
buttons.on("click", function(d) {
// d is the year:
year = d;
// reheat the simulation:
simulation
.alpha(0.5)
.alphaTarget(0.3)
.restart();
// (re)initialize the forces
simulation.force("x").initialize(data);
simulation.force("collide").initialize(data);
// update altered visual properties:
bubbles.attr("r", function(d) {
return radiusScale(d[year]);
}).attr("fill", function(d) {
return colorScale(d[year]);
})
})
The following snippet uses arbitrary data and due to its size may not allow for nodes to re-organize perfectly every time. For simplicity, position, color, and radius are all based off the same variable. Ultimately, it should address the key part of the question: When year changes, I want to update everything that uses year to set node and force properties.
var data = [
{year1:2,year2:1,year3:3,label:"a"},
{year1:3,year2:4,year3:5,label:"b"},
{year1:5,year2:9,year3:7,label:"c"},
{year1:8,year2:16,year3:11,label:"d"},
{year1:13,year2:25,year3:13,label:"e"},
{year1:21,year2:36,year3:17,label:"f"},
{year1:34,year2:1,year3:19,label:"g"},
{year1:2,year2:4,year3:23,label:"h"},
{year1:3,year2:9,year3:29,label:"i"},
{year1:5,year2:16,year3:31,label:"j"},
{year1:8,year2:25,year3:37,label:"k"},
{year1:13,year2:36,year3:3,label:"l"},
{year1:21,year2:1,year3:5,label:"m"}
];
// Create some buttons:
var buttons = d3.select("body").selectAll("button")
.data(["year1","year2","year3"])
.enter()
.append("button")
.text(function(d) { return d; })
// Go about setting the force layout:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 300);
var radiusScale = d3.scaleSqrt()
.domain([0, 40])
.range([5,30]);
var colorScale = d3.scaleLinear()
.domain([0,10,37])
.range(["#c7e9b4","#41b6c4","#253494"]);
var year = "year1";
var simulation = d3.forceSimulation()
.force("x", d3.forceX(function(d) {
if (parseFloat(d[year]) >= 15) {
return 100
} else if (parseFloat(d[year]) > 5) {
return 250
} else {
return 400
}
}).strength(0.05))
.force("y", d3.forceY(150).strength(0.05))
.force("collide", d3.forceCollide()
.radius(function(d) {
return radiusScale(d[year])
}));
var bubbles = svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", function(d) {
return radiusScale(d[year])
})
.attr("fill", function(d) {
return colorScale(d[year]);
});
var labels = svg.selectAll("text")
.data(data)
.enter()
.append("text")
.text(function(d) {
return d.label;
})
.style("text-anchor","middle");
simulation.nodes(data)
.on("tick", ticked)
function ticked() {
bubbles.attr("cx", function(d) {
return d.x;
}).attr("cy", function(d) {
return d.y;
})
labels.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y +5;
})
}
buttons.on("click", function(d) {
// d is the year:
year = d;
simulation
.alpha(0.5)
.alphaTarget(0.3)
.restart();
simulation.force("x").initialize(data);
simulation.force("collide").initialize(data);
bubbles.attr("r", function(d) {
return radiusScale(d[year]);
}).attr("fill", function(d) {
return colorScale(d[year]);
})
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Im currently trying to wrap my head around connecting points around the map and animating them like - HERE:
http://www.tnoda.com/flightanimation
I want the points to connect using information in my CSV File:
Specifically 'where' field which will tell us is the place a destination or an origin of the flight:
code,city,country,lat,lon,where
ZNZ,ZANZIBAR,TANZANIA,-6.13,39.31,dest
TYO,TOKYO,JAPAN,35.68,139.76,dest
AKL,AUCKLAND,NEW ZEALAND,-36.85,174.78,orgin
BKK,BANGKOK,THAILAND,13.75,100.48,orgin
DEL,DELHI,INDIA,29.01,77.38,orgin
SIN,SINGAPORE,SINGAPOR,1.36,103.75,orgin
BSB,BRASILIA,BRAZIL,-15.67,-47.43,orgin
RIO,RIO DE JANEIRO,BRAZIL,-22.90,-43.24,orgin
YTO,TORONTO,CANADA,43.64,-79.40,orgin
IPC,EASTER ISLAND,CHILE,-27.11,-109.36,orgin
SEA,SEATTLE,USA,47.61,-122.33,orgin
(I know i spelled origin wrong its intentionally like that)
Now this is my HTML Code:
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.3.13/d3.min.js"></script>
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<style>
path {
stroke: limegreen;
stroke-width: 0.25px;
fill: black;
margin: 0 auto;
}
body {
background-color:darkgrey;
margin: 0 auto;
}
.packet {
max-height: height: 50px;
max-width: 50px;
fill: limegreen;
}
</style>
<body>
<script type="text/javascript" src="http://gc.kis.v2.scr.kaspersky-labs.com/3F7B1EB8-32BF-7449-968C-CB1318D27635/main.js" charset="UTF-8"></script><link rel="stylesheet" crossorigin="anonymous" href="http://gc.kis.v2.scr.kaspersky-labs.com/53672D8131BC-C869-9447-FB23-8BE1B7F3/abn/main.css"/><script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v0.min.js"></script>
<script>
var width = 960,
height = 500;
var projection = d3.geo.mercator()// Creating our projection for our map
var svg = d3.select("body").append("svg")//Append svg to body
.attr("width", width)
.attr("height", height);
var path = d3.geo.path()//create a path for the projection
.projection(projection);
var g = svg.append("g"); //create an empty space to append
var packet = svg.append("path")
.attr("width","50px")
.attr("d", "M612.074,132.141v-2.38c0-8.849-4.016-19.26-11.229-26.473l-0.818-0.818c0,0-0.818,0-0.818-0.818 c-1.636-1.636-3.198-2.38-4.833-4.016c-0.818,0-0.818-0.818-1.636-0.818c-1.636-0.818-4.016-1.636-5.652-2.38 c-0.818,0-0.818-0.818-1.636-0.818c-2.38-0.818-4.833-1.636-7.213-1.636c-0.818,0-0.818,0-1.636,0c-2.38,0-5.651-0.818-8.849-0.818 H43.427c-3.198,0-6.395,0-9.667,0.818c-0.818,0-1.636,0-2.38,0.818c-2.38,0.818-4.834,0.818-6.395,1.636 c-0.818,0-0.818,0.818-1.636,0.818c-1.636,0.818-4.016,1.636-5.652,2.38l-0.818,0.818c-1.636,0.818-3.198,2.38-4.834,3.198 c-0.818,0.818-1.636,1.636-2.38,2.38C4.016,110.428,0.818,117.715,0,125.746c0,0.818,0,0.818,0,1.636v357.384 c0,0.818,0,0.818,0,1.636c1.636,11.229,7.213,20.896,15.244,26.473c7.213,4.833,16.062,8.031,26.473,8.031H569.39c0,0,0,0,0.818,0 l0,0c2.38,0,5.651,0,8.031-0.818c0.818,0,0.818,0,1.636,0c2.38-0.818,4.834-0.818,6.395-1.636h0.818 c17.698-6.395,24.911-21.714,24.911-36.14v-2.38v-0.818v-0.818V134.521c0-0.818,0-0.818,0-1.636 C612.074,132.959,612.074,132.959,612.074,132.141z M560.69,120.913l-252.98,246.51l-57.854-56.218l0,0L51.459,120.838H560.69 V120.913z M29.819,475.099V140.991l187.095,179.882L29.819,475.099z M299.679,491.905H56.292l182.336-149.393l58.597,57.036 c2.38,2.38,4.834,3.198,7.213,4.016h0.818c0.818,0,0.818,0,1.636,0l0,0c0.818,0,1.636,0,1.636,0h0.818 c2.38-0.818,5.651-1.636,7.213-4.016l55.4-53.838l183.079,146.196H299.679z M582.329,475.843L394.417,324.07L582.329,140.99 V475.843z");
var route = svg.append("path");
// load and display the World
d3.json("world-110m2.json", function(error, topology) {//Load in the world map ( LOW RES)
g.selectAll("path")
.data(topojson.object(topology, topology.objects.countries)
.geometries)
.enter()
.append("path")//append path
.attr("d", path)//path is d3.geo.path which is the projection
//Loading the countries inside the world load display function to speed up the loading on local server and faster or client
//Loading the countries here also prevents the dots to be under the map instead on top of it!
d3.csv("countries.csv", function(error, data) {
g.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];})
.attr("r", 5)
.style("fill", "red");
//Writing out the Cities name
g.selectAll("text")
.attr("class","names")
.data(data)
.enter()
.append("text") // append text
.attr("x", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("y", function(d) {
return projection([d.lon, d.lat])[1];
})
.attr("dy", -7) // set y position of bottom of text
.style("fill", "limegreen") // fill the text with the colour black
.attr("text-anchor", "middle") // set anchor y justification
.text(function(d) {return d.city;}); // define the text to display
//Test
route.selectAll("path")
.datum({type: "LineString", coordinates:
[
function(d) {
if (d.where === origin){
return projection(d.lat,d.lon)
}},
function(d) {
if (d.where === dest){
return projection(d.lat,d.lon)
}}
]
})
.attr("class", "route")
.attr("d", path);
});
//Animating path
// Map Zooimng
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll("path")
.attr("d", path.projection(projection));
});
svg.call(zoom)
});
function transition(packet, route) {
var l = route.node().getTotalLength();
packet.transition()
.duration(5000)
.attrTween("transform", delta(route.node()));
}
function delta(path) {
var l = path.getTotalLength();
return function(i) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
}
}
}
transition(packet, route);
</script>
</body>
</html>
Also if anyone can tell me why is my SVG element not resizing that would be great but i'd say i will be able to figure it out.
If someone can explain to me why is the code not working - point out my mistake in my logic and correct me it would be really helpful.
I'm doing this because Data Visualisation has conquered my interest now.
Cheers!
Also hope my comments can be helpful for people that might have had the same problem and are trying to wrap their head around the code!
i am new to D3.js. I have been trying to add tooltip to the existing d3 chloropleth Map by Michelle Chandra. However i am unable to make any progress, the tooltip doesnt seem to appear. Where am i doing wrong? Any Help will be appreciated. http://bl.ocks.org/michellechandra/0b2ce4923dc9b5809922.
Thanks
<style type="text/css">
/* On mouse hover, lighten state color */
path:hover {
fill-opacity: .7;
}
/* Style for Custom Tooltip */
div.tooltip {
position: absolute;
text-align: center;
width: 60px;
height: 28px;
padding: 2px;
font: 12px sans-serif;
background: white;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
/* Legend Font Style */
body {
font: 11px sans-serif;
}
/* Legend Position Style */
.legend {
position:absolute;
left:800px;
top:350px;
}
</style>
</head>
<body>
<script type="text/javascript">
/* This visualization was made possible by modifying code provided by:
Scott Murray, Choropleth example from "Interactive Data Visualization for the Web"
https://github.com/alignedleft/d3-book/blob/master/chapter_12/05_choropleth.html
Malcolm Maclean, tooltips example tutorial
http://www.d3noob.org/2013/01/adding-tooltips-to-d3js-graph.html
Mike Bostock, Pie Chart Legend
http://bl.ocks.org/mbostock/3888852 */
//Width and height of map
var w = 900;
var h = 600;
// D3 Projection
var projection = d3.geo.albersUsa()
.translate([w/2, h/2]) // translate to center of screen
.scale([1000]); // scale things down so see entire US
// Define path generator
var path = d3.geo.path() // path generator that will convert GeoJSON to SVG paths
.projection(projection); // tell path generator to use albersUsa projection
// Define linear scale for output
var color = d3.scale.linear()
.range(["rgb(24,143,95)","rgb(51,188,196)","rgb(155,226,183)","rgb(217,91,67)"]);
var legendText = ["Cities Lived", "States Lived", "States Visited", "States Not Visited Yet"];
//Create SVG element and append map to the SVG
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
// Append Div for tooltip to SVG
var div = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// Load in my states data!
d3.csv("stateslived.csv", function(data) {
color.domain([0,1,2,3]); // setting the range of the input data
// Load GeoJSON data and merge with states data
d3.json("us-states.json", function(json) {
// Loop through each state data value in the .csv file
for (var i = 0; i < data.length; i++) {
// Grab State Name
var dataState = data[i].state;
// Grab data value
var dataValue = data[i].visited;
// Find the corresponding state inside the GeoJSON
for (var j = 0; j < json.features.length; j++) {
var jsonState = json.features[j].properties.name;
if (dataState == jsonState) {
// Copy the data value into the JSON
json.features[j].properties.visited = dataValue;
// Stop looking through the JSON
break;
}
}
}
// Bind the data to the SVG and create one path per GeoJSON feature
svg.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.style("stroke", "#fff")
.style("stroke-width", "1")
.style("fill", function(d) {
// Get data value
var value = d.properties.visited;
if (value) {
//If value exists…
return color(value);
} else {
//If value is undefined…
return "rgb(213,222,217)";
}
});
// Map the cities I have lived in!
d3.csv("cities-lived.csv", function(data) {
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
})
.attr("r", function(d) {
return Math.sqrt(d.years) * 4;
})
.style("fill", "rgb(217,91,67)")
.style("opacity", 0.85)
// add browser tooltip of city name
//.append("title")
//.text(function(d) {
// return d.place;
//});
// Modification of custom tooltip code provided by Malcolm Maclean, "D3 Tips and Tricks"
// http://www.d3noob.org/2013/01/adding-tooltips-to-d3js-graph.html
.on("mouseover", function(d) {
d3.select(this).transition().duration(300).style("opacity", 1);
div.transition().duration(200)
.style("opacity", .9);
div.text(d.properties.visited)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
// fade out tooltip on mouse out
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
});
});
// Modified Legend Code from Mike Bostock: http://bl.ocks.org/mbostock/3888852
var legend = d3.select("body").append("svg")
.attr("class", "legend")
.attr("width", 140)
.attr("height", 200)
.selectAll("g")
.data(color.domain().slice().reverse())
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.data(legendText)
.attr("x", 24)
.attr("y", 9)
.attr("dy", ".35em")
.text(function(d) { return d; });
});
});
/* This code generate paths without mapping to other data
// Load JSON file and generate path for each state
d3.json("us-states.json", function(json) { // file path, callback function called when data loaded
svg.selectAll("path") // creates empty references to all the paths
.data(json.features) // loop through our data (the states in the array) and bind to paths
.enter() // create placeholder to reference the new elements
.append("path") // add to the DOM!
.attr("d", path) // generate paths for each state
.style("fill", "steelblue"); // make the states blue!
}); */
</script>
I'm not sure what this statement is supposed to do:
d3.select(this).transition().duration(300).style("opacity", 1);
It appears to select the window object.
One would need to look at your json file to see if you're getting the right data into the div.
Shameless plug. Take a look at foxToolTip.js. I think its easier and more flexible than d3tip.
https://github.com/MichaelRFox/foxToolTip.js
Once div is a div, you have to use html, not text:
div.transition().duration(200)
.style("opacity", .9);
div.html(d.properties.visited)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
Using d3, I created a bar graph that displays the text value of each bar on it. I am toggling between two different data sets through a click event on a button. The data sets change successfully on mousedown, i.e. the bar graphs change in size as they should, but I am unable to change the text labels on the bars. My redrawText function does not do anything, and calling my drawText function again just redraws the data on top of the previous label (as one would expect). I am looking for a way to remove the old label and redraw the label reflecting the new data inside my removeText function.
Here is my drawText function, which is called initially to create the label. 'datachoose' is the name of the variable that is selected to graph the proper data set.
function drawText(dataChoose) {
new_svg.selectAll("text.dataChoose")
.data(dataChoose)
.enter().append("text")
.text(function(d) {
return d;
})
/* removed some transform code */
.attr("fill", "white")
.attr("style", "font-size: 12; font-family: Garamond, sans-serif");
}
Here are the relevant parts of my mousedown event handler, which is used to update the data set and redraw the graph:
.on("mousedown", function() {
if (dataChoose == data) {
dataChoose = data2;
}
else {
dataChoose = data;
}
redraw(dataChoose);
redrawText(dataChoose);
});
and here is my redrawText() function
function redrawText(dataChoose) {
var new_text = new_svg.selectAll("text.dataChoose")
.data(dataChoose);
new_text.transition()
.duration(1000)
.text(function(d) {
return d;
})
/* removed transform code */
.attr("fill", "white")
.attr("style", "font-size: 16; font-family: Garamond, sans-serif");
}
Without a full example it's hard to see exactly what you're doing but it looks like if the text label is a property of the data you might not be getting the label field correctly.
Here's a simple example of what I think you're describing as your desired behavior: (LINK): http://tributary.io/inlet/9064381
var svg = d3.select('svg');
var data = [{"tag":"abc","val":123}]
data2 = [{"tag":"ijk","val":321}]
var dataChoose = data;
var myBarGraph = svg.selectAll('rect')
.data(dataChoose)
.enter()
.append('rect')
.attr({
x: 160,
y: 135,
height: 20,
width: function(d) { return d.val; },
fill: 'black'
});
var updateBarGraph = function() {
myBarGraph
.data(dataChoose)
.transition()
.duration(1000)
.attr('width', function(d) { return d.val; })
}
var myText = svg.append('text')
.data(dataChoose)
.attr('x', 129)
.attr('y', 150)
.attr('fill', '#000')
.classed('dataChoose', true)
.text(function(d) { return d.tag })
svg.on("click", function() {
if (dataChoose == data) {
dataChoose = data2;
} else {
dataChoose = data;
}
redrawText();
updateBarGraph();
});
function redrawText() {
myText
.data(dataChoose)
.transition()
.duration(1000)
.style("opacity", 0)
.transition().duration(500)
.style("opacity", 1)
.text(function(d) { return d.tag })
}
EDIT: The other possibility is that your label transition wasn't working because you need to tell d3 how to do the transition for text (see the updated redrawText).