I have a d3 layout that is intended to produce 3 charts:
Custom item layout w/ transitions
Pie chart for sales by category (with transitions at some point)
Bar chart for top 5 performing items w/ transitions.
1 & 2 work OK but when I add the third chart, I see some strange behavior. The intention is to create a bar chart where the width of each bar is tied to the sales metric for the top N items determined like so:
data = data.filter(function(d){ return d.date === someDate});
var cf = crossfilter(data);
var salesDimension = cf.dimension(function(d){ return d.sales; });
topData = salesDimension.top(5);
The problem is that instead of drawing the bar chart, my code somehow overwrites the position of 5 items in chart 1 and moves them back to the origin. If I change 5 to 10 above then 10 items are overwritten and so on.
I double checked the scope of my variables and even tried changing the names of everything in my drawTopItems() which made no difference. I suspect that I am doing something incorrectly when it comes to selecting the svg element or applying classes to the svg group elements that I want to modify but I can't for the life of me see what. Can anyone tell me what I might be doing wrong?
Here is my issue in a fiddle: https://jsfiddle.net/Sledge/4eggpd5e/12/.
Here is my javascript code:
var item_width = 40, item_height = 60;
var margin = {top: 50, right: 50, bottom: 75, left: 40},
width = 700 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scaleLinear().range([0, width]);
var y = d3.scaleLinear().range([0, height]);
var colorScale = d3.scaleLinear().domain([500,3000]).range(["white","#4169e1"]);
// Pie Chart parameters
var pieWidth = 300, pieHeight = 300;
var outerRadius = Math.min(pieWidth, pieHeight) / 2,
innerRadius = outerRadius * .50;
var pieColor = d3.scaleOrdinal(['#42b9f4','#3791f2','#374ff1','#25b22e','#107222']); // custom color scale
var legendRectSize = 18; // NEW
var legendSpacing = 4; // NEW
// Top Item Parameters
var topItemMargin = {top:25, right:25, bottom: 25, left: 25};
var topItemWidth = 300 - topItemMargin.left - topItemMargin.right,
topItemHeight = 300 - topItemMargin.top - topItemMargin.bottom;
var topItemXScale = d3.scaleLinear().range([0, topItemWidth]);
var barHeight = 20, barSpacing = 5;
/* SVG */
var svgItemLayout = d3.select("#item_layout")
.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 + ")");
var svgPieChart = d3.select("#pie_chart")
.append("svg")
.attr("width", pieWidth)
.attr("height", pieHeight)
.append("g")
.attr("transform", "translate(" + outerRadius + "," + outerRadius + ")") ;
var svgTopItems = d3.select("#top_items")
.append("svg")
.attr("width", topItemWidth)
.attr("height", topItemHeight)
.append("g")
.attr("transform", "translate(" + topItemMargin.left + "," + topItemMargin.top + ")");
/* DRAW FUNCTIONS */
// a single function to draw
function drawItemLayout(data, someDate){
data.forEach(function(d) {
d.x_pos = +d.x_pos;
d.y_pos = +d.y_pos;
d.sales = +d.sales;
});
// pre-filter data
data = data.filter(function(d){ return d.date === someDate});
var x_offset = 5, y_offset = 5;
x.domain(d3.extent(data, function(d) { return d.x_pos; })); // set the x domain
y.domain(d3.extent(data, function(d) { return d.y_pos; })); // set the y domain
// create an update selection with a key function
var g_sel = svgItemLayout.selectAll("g")
.data(data, function(d){
return d.item_name;
});
// get rid of those leaving the update
g_sel.exit().remove();
// our entering g
var g_ent = g_sel.enter()
.append("g");
// add our rects to our g
g_ent.append("rect")
.attr("class", "dot") // wonder if I really need this class?
.attr("width", item_width)
.attr("height", item_height)
.attr("rx", 3)
.attr("ry", 3)
.style("fill", function(d){ return colorScale(d.sales); }) // color factor variable
.style("fill-opacity", 0.5);
// add our text to our g
g_ent.append("text")
.attr("font-size", 10)
.attr("text-anchor", "middle")
.attr("fill", "black")
.attr("dx", item_width/2)
.attr("dy", item_height/2)
.text(function(d){ return d.item_name; });
// UPDATE + ENTER selection
g_sel = g_ent.merge(g_sel);
// move them into position with transition
g_sel
.transition()
.duration(1200)
.delay(function(d, i) { return i *40; })
.attr("transform", function(d){
return "translate(" + (x(d.x_pos) + x_offset) + "," + (y(d.y_pos) + y_offset) + ")";
});
}
function drawPieChart(data, someDate) {
data.forEach(function(d) {
d.x_pos = +d.x_pos;
d.y_pos = +d.y_pos;
d.sales = +d.sales;
});
// pre-filter data
data = data.filter(function(d){ return d.date === someDate});
var cf = crossfilter(data);
var salesDimension = cf.dimension(function(d){ return d.sales; });
var categoryDimension = cf.dimension(function(d){ return d.category; });
var categoryGroup = categoryDimension.group();
function reduceInitial(p, v) {
return {
sales : 0,
count : 0
};
}
function reduceAdd(p, v) {
p.sales = p.sales + v.sales;
p.count = p.count + 1;
return p;
}
function reduceRemove(p, v) {
p.sales = p.sales - v.sales;
p.count = p.count - 1;
return p;
}
categoryAggregated = categoryGroup.reduce(reduceAdd, reduceRemove, reduceInitial).all();
var arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var pie = d3.pie()
.value(function(d) { return d.value.sales; })
.sort(null);
var path = svgPieChart.selectAll('path')
.data(pie(categoryAggregated))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) { return pieColor(i);});
// Add a legend:
var legend = svgPieChart.selectAll('.legend')
.data(pieColor.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * pieColor.domain().length / 2;
var horz = -3 * legendRectSize;
var vert = i * height - offset;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', pieColor)
.style('stroke', pieColor);
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function(d) { return categoryAggregated[d].key; }); // returns text based on data index
}
function drawTopItems (data, someDate) {
data.forEach(function(d) {
d.x_pos = +d.x_pos;
d.y_pos = +d.y_pos;
d.sales = +d.sales;
});
// pre-filter data
data = data.filter(function(d){ return d.date === someDate});
var cf = crossfilter(data);
var salesDimension = cf.dimension(function(d){ return d.sales; });
topData = salesDimension.top(5);
topItemXScale.domain(d3.extent(topData, function(d) { return d.sales; })); // set the x domain
var f_sel = svgTopItems.selectAll("g")
.data(topData,function(d){ return d.item_name; }).enter();
f_sel.exit().remove();
var f_ent = f_sel.enter().append("g");
f_ent.append("rect")
.attr("class", "dot") // wonder if I really need this class?
.attr("width", function(d){ return d.sales })
.attr("height", barHeight)
.style("fill","#351eff") // color factor variable
.style("fill-opacity", 0.75);
// add our text to our g
f_ent.append("text")
.attr("font-size", 10)
.attr("text-anchor", "left")
.attr("fill", "black")
.attr("dx", item_width/2)
.attr("dy", item_height/2)
.text(function(d){ return d.item_name});
// UPDATE + ENTER selection
f_sel = f_ent.merge(f_sel);
f_sel.transition()
.duration(1200)
.delay(function(d, i) { return i *40; })
.attr("transform", function(d, i){
return "translate( 0, "+ i*25 +")" + ")";
});
}
/* MAIN */
var data = d3.csvParse( d3.select("pre#data").text());
drawItemLayout(data, '1-20-2017');
drawPieChart(data, '1-20-2017');
drawTopItems(data, '1-20-2017');
/* UPDATE DATA */
function updateData(date) {
//d3.csv("http://localhost:8080/udacity_test_vis_1/output_fixture_data.csv", function(data) {
var data = d3.csvParse( d3.select("pre#data").text());
drawItemLayout (data, date);
drawPieChart(data, date);
drawTopItems(data, date);
}
/* GET SELECTION */
$("#select_params").change(function(){
var date = $("#select_params :selected").val();
console.log(date);
updateData(date);
})
Just three problems:
You are repeating the enter() function:
var f_sel = svgTopItems.selectAll("g")
.data(topData,function(d){ return d.item_name; }).enter();
//this is an enter selection...
var f_ent = f_sel.enter().append("g");
//and you have enter() again here
So, remove the first enter: f_sel should be just the data-binding selection.
Move your merged selection to before appending the rectangles
Your translate has an extra parenthesis:
return "translate( 0, "+ i*25 +")" + ")";
With that problems corrected, this is your updated fiddle: https://jsfiddle.net/utf5hva2/
Related
I have made a violin plot in D3.js with the following code:
<script src="https://d3js.org/d3.v4.js"></script>`
<div id="power"></div>
<script>
var margin = {top: 120, right: 100, bottom: 80, left: 100},
width = 2600 - margin.left - margin.right,
height = 620 - margin.top - margin.bottom;
var svg = d3.select("#power")
.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 + ")");
// Read the data and compute summary statistics for each
d3.csv("static/csv/violinsummary.csv", function (data) {
// Show the X scale
var x = d3.scaleBand()
.range([0, width])
.domain(["2017-09", "2017-10", "2018-02", "2018-03"])
.paddingInner(0)
.paddingOuter(.5);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Show the Y scale
var y = d3.scaleLinear()
.domain([80, 105])
.range([height, 0]);
svg.append("g").call(d3.axisLeft(y));
// Features of density estimate
var kde = kernelDensityEstimator(kernelEpanechnikov(.2), y.ticks(50));
// Compute the binning for each group of the dataset
var sumstat = d3.nest()
.key(function (d) {
return d.DATE;
})
.rollup(function (d) { // For each key..
input = d.map(function (g) {
return g.Power;
});
density = kde(input); // And compute the binning on it.
return (density);
})
.entries(data);
var maxNum = 0;
for (i in sumstat) {
allBins = sumstat[i].value;
kdeValues = allBins.map(function (a) {
return a[1]
});
biggest = d3.max(kdeValues);
if (biggest > maxNum) {
maxNum = biggest
}
}
// The maximum width of a violin must be x.bandwidth = the width dedicated to a group
var xNum = d3.scaleLinear()
.range([0, x.bandwidth()])
.domain([-maxNum, maxNum]);
svg
.selectAll("myViolin")
.data(sumstat)
.enter() // So now we are working group per group
.append("g")
.attr("transform", function (d) {
return ("translate(" + x(d.key) + " ,0)")
}) // Translation on the right to be at the group position
.append("path")
.datum(function (d) {
return (d.value)
}) // So now we are working density per density
.style("opacity", .7)
.style("fill", "#317fc8")
.attr("d", d3.area()
.x0(function (d) {
return (xNum(-d[1]))
})
.x1(function (d) {
return (xNum(d[1]))
})
.y(function (d) {
return (y(d[0]))
})
.curve(d3.curveCatmullRom));
});
function kernelDensityEstimator(kernel, X) {
return function (V) {
return X.map(function (x) {
return [x, d3.mean(V, function (v) {
return kernel(x - v);
})];
});
}
}
function kernelEpanechnikov(k) {
return function (v) {
return Math.abs(v /= k) <= 1 ? 0.75 * (1 - v * v) / k : 0;
};
}
</script>
Data (violinsummary.csv):
Power,DATE
89.29,2017-09
89.9,2017-09
91.69,2017-09
89.23,2017-09
91.54,2017-09
88.49,2017-09
89.15,2017-09
90.85,2017-09
89.59,2017-09
93.38,2017-10
92.41,2017-10
90.65,2017-10
91.07,2017-10
90.13,2017-10
91.73,2017-10
91.09,2017-10
93.21,2017-10
91.62,2017-10
89.58,2017-10
90.59,2017-10
92.57,2017-10
89.99,2017-10
90.59,2017-10
88.12,2017-10
91.3,2017-10
89.59,2018-02
91.9,2018-02
87.83,2018-02
90.36,2018-02
91.38,2018-02
91.56,2018-02
91.89,2018-02
90.95,2018-02
90.15,2018-02
90.24,2018-02
94.04,2018-02
85.4,2018-02
88.47,2018-02
92.3,2018-02
92.46,2018-02
92.26,2018-02
88.78,2018-02
90.13,2018-03
89.95,2018-03
92.98,2018-03
91.94,2018-03
90.29,2018-03
91.2,2018-03
94.22,2018-03
90.71,2018-03
93.03,2018-03
91.89,2018-03
I am trying to make a tooltip for each violin that shows the median and mean upon hover. I cannot figure out how to make the tooltip show up.
I know I need to do something like this with mouseover and mouseout but I'm not sure...
var tooltip = d3.select('#power')
.append('div')
.attr('class', 'tooltip')
.style("opacity", 0);
Any tips/guidance would be very appreciated.
You can implement the tooltip functionality by following two steps.
Step 1:
Initialize the tooltip container which already you did I guess.
var tooltip = svg.append("g")
.attr("class", "tooltip")
.style("display", "none");
tooltip.append("rect")
.attr("width", 30)
.attr("height", 20)
.attr("fill", "white")
.style("opacity", 0.5);
tooltip.append("text")
.attr("x", 15)
.attr("dy", "1.2em")
.style("text-anchor", "middle")
.attr("font-size", "12px")
.attr("font-weight", "bold");
Step 2:
Change the visibility property of the tooltip in the mouseover, mouseout event of the element. In your case, it's myViolin
.on("mouseover", function() {
tooltip.style("display", null);
})
.on("mouseout", function() {
tooltip.style("display", "none");
})
.on("mousemove", function(d) {
var xPosition = d3.mouse(this)[0] - 15;
var yPosition = d3.mouse(this)[1] - 25;
tooltip.attr("transform", "translate(" + xPosition + "," + yPosition + ")");
tooltip.select("text").text(d.y);
});
Here is the implementation of tooltip jsFiddle
Hope it helps :)
I'm trying to get my data to show up in my graph, however I get an error that says that my data is "NaN" after I converted the Year and Miles column from strings to integers.
I'm guessing that it's something with my x_scale & y_scale...?
<!DOCTYPE html>
<html lang="en">
<head>
<description>
<!--charts - avg vehicle trip per mile, source: http://nhts.ornl.gov/2009/pub/stt.pdf-->
</description>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<link rel="stylesheet" href="/bootstrap-3.3.7-dist/css/bootstrap.min.css">
<script type="text/javascript" src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script type="text/javascript">
// global variables
var dataset;
d3.csv("avgVehicleTripLengthMiles.csv", function (error, data) {
if (error) {
console.log(error);
} else {
console.log(data);
}
// once loaded, data is copied to dataset because js is asynchronous
dataset = data;
createScatterplot();
});
/*
function typeConv() {
// type conversion from string to integer
var typeConv = dataset.forEach(function (d) {
d["Year"] = +d["Year"];
d["Miles"] = +d["Miles"];
return d;
});
}
*/
function createScatterplot() {
// TEST
var typeConv = dataset.forEach(function (d) {
d["Year"] = +d["Year"];
d["Miles"] = +d["Miles"];
return d;
});
var title = d3.select("body")
.append("h4")
.text("Avg. Vehicle Trip Length per Mile");
// dimensions of canvas
var padding = 30;
var margin = {
top: 20,
right: 40,
bottom: 20,
left: 40
},
w = 800 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
// create svg canvas
var svg_canvas = d3.select("body")
.append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom);
// create scale for axis
var x_scale = d3.scaleLinear().domain([1969, 2009]).range([padding, w - padding * 2]);
var y_scale =
d3.scaleLinear().domain([0, 20]).range([h - padding, padding]);
// r_scale created specifically for circles' radius to be mapped unto axes
var r_scale =
d3.scaleLinear().domain([0, d3.max(dataset, function (d) {
return d[1];
})]).range([0, 20]);
// define axis & ticks // .ticks(5) to x_axis and .ticks(1) to y_axis
var x_axis = d3.axisBottom().scale(x_scale);
var y_axis = d3.axisLeft().scale(y_scale);
// create group, "g" element, to create x_axis and y_axis
var x_group = svg_canvas.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(x_axis);
var y_group = svg_canvas.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + padding + ",0)")
.call(y_axis);
// create circles
svg_canvas.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function (d) {
return x_scale(d[0]);
})
.attr("cy", function (d) {
console.log(d); // TEST
return y_scale(d[1]);
})
.attr("cr", function (d) {
return r_scale(d[1]);
});
}
</script>
</body>
</html>
EDIT (new answer):
There are several issues, and I'll try to step through them one by one.
In order to test, I had to make up my own data. My test CSV file looked like this (so your final answer might change slightly if your file is different)
Year,Miles
2006,5.0
2007,7.2
2008,19.3
As was pointed out by #altocumulus in the comments above, your .attr() calls are referencing non-existant indexes, which might be part of the trouble.
The radius attribute for circles is r, not cr
I simplified the code by not calling a function for r, but rather doing a static value. You may want to play further with this.
The significantly changed portion of code is
svg_canvas.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function (d) {
return x_scale(d["Year"]);
})
.attr("cy", function (d) {
return y_scale(d["Miles"]);
})
.attr("r", function (d) {
return 5;
//return r_scale(d[1]);
});
You still have an issue with the x-axis acting like numbers, and not dates, making 2006 look like 2,006, for example. I haven't solved that issue here.
Lastly, I feel like you're complicating your code for no reason by trying to handle margin AND padding via the D3 code, when these end up meaning similar things in the Javascript context. I suggest accomplishing most of the margin/padding via CSS, which would simplify your code. Another example of an unnecessary complication is in the previous answer, below.
FIDDLE
OLD (incomplete, incorrect) ANSWER:
The return value of Array.forEach() is undefined, so it can't be assigned.
dataset.forEach(function (d) {
//d["Year"] = +d["Year"];
d["Miles"] = +d["Miles"];
// NOT NEEDED: return d;
});
If you need to keep your converted array separate, use Array.map().
// global variables
var dataset;
d3.csv("avgVehicleTripLengthMiles.csv", function (error, data) {
if (error) {
console.log(error);
} else {
console.log(data);
}
// once loaded, data is copied to dataset because js is asynchronous
dataset = data;
createScatterplot();
});
function createScatterplot() {
// TEST
var typeConv = dataset.forEach(function (d) {
// d["Year"] = +d["Year"];
d["Miles"] = +d["Miles"];
// return d;
});
var title = d3.select("body")
.append("h4")
.text("Avg. Vehicle Trip Length per Mile");
// dimensions of canvas
var padding = 30;
var margin = {
top: 20,
right: 40,
bottom: 20,
left: 40
},
w = 800 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
// create svg canvas
var svg_canvas = d3.select("body")
.append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom);
// create scale for axis
var x_scale = d3.scaleLinear().domain([1965, 2009]).range([padding, w - padding * 2]);
var y_scale =
d3.scaleLinear().domain([0, 20]).range([h - padding, padding]);
// r_scale created specifically for circles' radius to be mapped unto axes
var r_scale =
d3.scaleLinear().domain([0, d3.max(dataset, function (d) {
return d[1];
})]).range([0, 20]);
// define axis & ticks // .ticks(5) to x_axis and .ticks(1) to y_axis
var x_axis = d3.axisBottom().scale(x_scale);
var y_axis = d3.axisLeft().scale(y_scale);
// create group, "g" element, to create x_axis and y_axis
var x_group = svg_canvas.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(x_axis);
var y_group = svg_canvas.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + padding + ",0)")
.call(y_axis);
// create & color circles
svg_canvas.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function (d) {
return x_scale(d["Year"]);
})
.attr("cy", function (d) {
return y_scale(d["Miles"]);
})
.attr("r", function (d) {
return 5;
})
.style("fill", function (d) {
if (d["Trip Purpose"] === "All Purposes") {
return "pink";
} else if (d["Trip Purpose"] === "To or From Work") {
return "red";
} else if (d["Trip Purpose"] === "Shopping") {
return "blue";
} else if (d["Trip Purpose"] === "Other Family/Personal Errands") {
return "green";
} else if (d["Trip Purpose"] === "Social and Recreational") {
return "gray";
};
});
// create text label for x-axis
svg_canvas.append("text")
.attr("x", w / 2)
.attr("y", h + margin.top + 20)
.style("text-anchor", "middle")
.text("Year");
// create text label for y-axis
svg_canvas.append("text")
.attr("transform", "rotate(-90)")
.attr("x", (0 - margin.left / 2))
.attr("y", (h/2))
.style("text-anchor", "middle")
.text("Miles");
Can someone help me implementing a spiral chart similar to the one below using d3.js?
I've just got the basic spiral plot (a simple one) as of now but not been able to append bars to the plot based on the timeline as shown in the image. I'm trying out a few things (if you see the commented code).
Here's my fiddle, and my code:
var width = 400,
height = 430,
axes = 12,
tick_axis = 9,
start = 0,
end = 2.25;
var theta = function(r) {
return 2 * Math.PI * r;
};
var angle = d3.scale.linear()
.domain([0, axes]).range([0, 360])
var r = d3.min([width, height]) / 2 - 40;
var r2 = r;
var radius = d3.scale.linear()
.domain([start, end])
.range([0, r]);
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 8) + ")");
var points = d3.range(start, end + 0.001, (end - start) / 1000);
var spiral = d3.svg.line.radial()
.interpolate("cardinal")
.angle(theta)
.radius(radius);
var path = svg.selectAll(".spiral")
.data([points])
.enter().append("path")
.attr("class", "spiral")
.attr("d", spiral)
var z = d3.scale.category20();
var circles = svg.selectAll('.circle')
.data(points);
/* circles.enter().append('circle')
.attr('r', 5)
.attr('transform', function(d) { return 'translate(' + d + ')'})
.style('fill', function(d) { return z(d); });
*/
var circle = svg.append("circle")
.attr("r", 13)
.attr("transform", "translate(" + points[0] + ")");
var movingCircle = circle.transition().duration(4000)
.attrTween('transform', translateAlongPath(path.node()))
// .attr('cx', function(d) { return radius(d) * Math.cos(theta(d))})
// .attr('cy', function(d) { return radius(d) * Math.sin(theta(d))})
function translateAlongPath(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
//console.log(p)
return "translate(" + p.x + "," + p.y + ")";
};
};
}
function pathXY(path) {
var l = path.getTotalLength();
var start = 0;
/* for(i=start; i<l; i++) {
var point = path.getPointAtLength(i);
svg.append('rect').transition().duration(400).attr('transform', 'translate(' + point.x +','+point.y+')')
.attr('width', 10).attr('height', 30).style('fill', z);
}*/
}
pathXY(path.node());
/*var test = translateAlongPath(path.node())()();
//console.log(test)
var bars = svg.selectAll('.bar')
.data(points).enter().append('rect').transition().duration(2000)
// .attrTween('transform', translateAlongPath(path.node()))
.attr('class', 'bar')
.attr('width', 10)
.attr('height', 20)
.style('fill', function(d) { return z(d)});
*/
var rect = svg.append('rect').attr('width', 10).attr('height', 10);
rect.transition().duration(3400)
.attrTween('transform', translateAlongPath(path.node()));
It'd be great to have a few similar examples (i.e. spiral timeline plot).
Thanks.
Glad you came back and updated your question, because this is an interesting one. Here's a running minimal implementation. I've commented it ok, so let me know if you have any questions...
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
var width = 500,
height = 500,
start = 0,
end = 2.25,
numSpirals = 4;
var theta = function(r) {
return numSpirals * Math.PI * r;
};
var r = d3.min([width, height]) / 2 - 40;
var radius = d3.scaleLinear()
.domain([start, end])
.range([40, r]);
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// create the spiral, borrowed from http://bl.ocks.org/syntagmatic/3543186
var points = d3.range(start, end + 0.001, (end - start) / 1000);
var spiral = d3.radialLine()
.curve(d3.curveCardinal)
.angle(theta)
.radius(radius);
var path = svg.append("path")
.datum(points)
.attr("id", "spiral")
.attr("d", spiral)
.style("fill", "none")
.style("stroke", "steelblue");
// fudge some data, 2 years of data starting today
var spiralLength = path.node().getTotalLength(),
N = 730,
barWidth = (spiralLength / N) - 1;
var someData = [];
for (var i = 0; i < N; i++) {
var currentDate = new Date();
currentDate.setDate(currentDate.getDate() + i);
someData.push({
date: currentDate,
value: Math.random()
});
}
// here's our time scale that'll run along the spiral
var timeScale = d3.scaleTime()
.domain(d3.extent(someData, function(d){
return d.date;
}))
.range([0, spiralLength]);
// yScale for the bar height
var yScale = d3.scaleLinear()
.domain([0, d3.max(someData, function(d){
return d.value;
})])
.range([0, (r / numSpirals) - 30]);
// append our rects
svg.selectAll("rect")
.data(someData)
.enter()
.append("rect")
.attr("x", function(d,i){
// placement calculations
var linePer = timeScale(d.date),
posOnLine = path.node().getPointAtLength(linePer),
angleOnLine = path.node().getPointAtLength(linePer - barWidth);
d.linePer = linePer; // % distance are on the spiral
d.x = posOnLine.x; // x postion on the spiral
d.y = posOnLine.y; // y position on the spiral
d.a = (Math.atan2(angleOnLine.y, angleOnLine.x) * 180 / Math.PI) - 90; //angle at the spiral position
return d.x;
})
.attr("y", function(d){
return d.y;
})
.attr("width", function(d){
return barWidth;
})
.attr("height", function(d){
return yScale(d.value);
})
.style("fill", "steelblue")
.style("stroke", "none")
.attr("transform", function(d){
return "rotate(" + d.a + "," + d.x + "," + d.y + ")"; // rotate the bar
});
// add date labels
var tF = d3.timeFormat("%b %Y"),
firstInMonth = {};
svg.selectAll("text")
.data(someData)
.enter()
.append("text")
.attr("dy", 10)
.style("text-anchor", "start")
.style("font", "10px arial")
.append("textPath")
// only add for the first of each month
.filter(function(d){
var sd = tF(d.date);
if (!firstInMonth[sd]){
firstInMonth[sd] = 1;
return true;
}
return false;
})
.text(function(d){
return tF(d.date);
})
// place text along spiral
.attr("xlink:href", "#spiral")
.style("fill", "grey")
.attr("startOffset", function(d){
return ((d.linePer / spiralLength) * 100) + "%";
})
</script>
</body>
</html>
I am trying to create a corrologram using a set of data using D3.js.
I used R to create the correlation matrix but for visualization I want to use D3js and create a chart which shows the correlation matrix as in the picture. Can anyone guide me on this please.
Interesting problem so I took a whack at it. Using the mtcars dataset and given an R calculated correlation matrix, output in a CSV format using:
write.csv(cor(mtcars), file="data.csv")
Which creates:
"","mpg","cyl","disp","hp","drat","wt","qsec","vs","am","gear","carb"
"mpg",1,-0.852161959426613,-0.847551379262479,-0.776168371826586,0.681171907806749,-0.867659376517228,0.418684033921778,0.664038919127593,0.599832429454648,0.480284757338842,-0.550925073902459
"cyl",-0.852161959426613,1,0.902032872146999,0.83244745272182,-0.69993811382877,0.782495794463241,-0.591242073768869,-0.810811796083005,-0.522607046900675,-0.492686599389471,0.526988293749643
You can replicate your plot with d3:
d3.csv("data.csv", function(error, rows) {
// read in the CSV file and put the data in a d3 format or an array of objects
var data = [];
rows.forEach(function(d) {
var x = d[""]; // x represent the column name
delete d[""];
for (prop in d) {
var y = prop, // y is this row name
value = d[prop]; // correlation value
data.push({
x: x,
y: y,
value: +value
});
}
});
// standard d3 plot setup
var margin = {
top: 25,
right: 80,
bottom: 25,
left: 25
},
width = 500 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom,
domain = d3.set(data.map(function(d) { // our domain is just the column names
return d.x
})).values(),
num = Math.sqrt(data.length), // how many rows and columns
color = d3.scale.linear() // our color scale from red to white to blue
.domain([-1, 0, 1])
.range(["#B22222", "#fff", "#000080"]);
// set-up x and y scale
var x = d3.scale
.ordinal()
.rangePoints([0, width])
.domain(domain),
y = d3.scale
.ordinal()
.rangePoints([0, height])
.domain(domain),
xSpace = x.range()[1] - x.range()[0], // this is the space of each grid space
ySpace = y.range()[1] - y.range()[0];
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 + ")");
// bind our data for each grid space
var cor = svg.selectAll(".cor")
.data(data)
.enter()
.append("g")
.attr("class", "cor")
.attr("transform", function(d) {
return "translate(" + x(d.x) + "," + y(d.y) + ")";
});
// outer rectangle on each grid space
cor.append("rect")
.attr("width", xSpace)
.attr("height", ySpace)
.attr("x", -xSpace / 2)
.attr("y", -ySpace / 2)
// filter out below the diagonal
cor.filter(function(d){
var ypos = domain.indexOf(d.y);
var xpos = domain.indexOf(d.x);
for (var i = (ypos + 1); i < num; i++){
if (i === xpos) return false;
}
return true;
})
// append a text
.append("text")
.attr("y", 5)
.text(function(d) {
if (d.x === d.y) {
return d.x;
} else {
return d.value.toFixed(2);
}
})
// color it
.style("fill", function(d){
if (d.value === 1) {
return "#000";
} else {
return color(d.value);
}
});
// filter above the diagonal
cor.filter(function(d){
var ypos = domain.indexOf(d.y);
var xpos = domain.indexOf(d.x);
for (var i = (ypos + 1); i < num; i++){
if (i === xpos) return true;
}
return false;
})
// add a circle
.append("circle")
.attr("r", function(d){
return (width / (num * 2)) * (Math.abs(d.value) + 0.1);
})
.style("fill", function(d){
if (d.value === 1) {
return "#000";
} else {
return color(d.value);
}
});
// build the "yAxis" color scale
// its a series of rects colored correctly
// to produce a smooth gradient
var aS = d3.scale
.linear()
.range([-margin.top + 5, height + margin.bottom - 5])
.domain([1, -1]);
var yA = d3.svg.axis()
.orient("right")
.scale(aS)
.tickPadding(7);
var aG = svg.append("g")
.attr("class", "y axis")
.call(yA)
.attr("transform", "translate(" + (width + margin.right / 2) + " ,0)")
var iR = d3.range(-1, 1.01, 0.01);
var h = height / iR.length + 3;
iR.forEach(function(d){
aG.append('rect')
.style('fill',color(d))
.style('stroke-width', 0)
.style('stoke', 'none')
.attr('height', h)
.attr('width', 10)
.attr('x', 0)
.attr('y', aS(d))
});
});
Here's the result:
Full working code.
We can use d3 v4 here's the updated code with d3 changes' log.
I want to display two charts in one page.The Pie chart gets displayed but the Grouped and Stacked bar chart is not displaying . I tried to change the Id name in but no luck :( . Will appreciate if anyone helps me with the code correction .
/* Display Matrix Chart ,Pie chart, Grouped and Stacked bar chart in one page */
<apex:page showHeader="false">
<apex:includeScript value="{!URLFOR($Resource.jquery1)}"/>
<apex:includeScript value="{!URLFOR($Resource.D3)}"/>
<apex:includeScript value="{!URLFOR($Resource.nvD3)}"/>
<div id="body" height="50%" width="100px" ></div> // Id for Pie chart
<div id="body1" height="30%" width="90px"></div> // Id for Stacked and Grouped bar chart
<script>
// Matrix Chart starts here
var drawChart = function(divId,matrixReportId) {
$.ajax('/services/data/v29.0/analytics/reports/'+matrixReportId,
{
beforeSend: function(xhr) {
xhr.setRequestHeader('Authorization', 'Bearer {!$Api.Session_ID}');
},
success: function(response) {
console.log(response);
var chart = nv.models.multiBarChart();
var chartData = [];
document.getElementById(divId).innerHTML = '';
$.each(response.groupingsDown.groupings, function(di, de) {
var values = [];
chartData.push({"key":de.label, "values": values});
$.each(response.groupingsAcross.groupings, function(ai, ae) {
values.push({"x": ae.label, "y": response.factMap[de.key+"!"+ae.key].aggregates[0].value});
});
});
d3.select('#'+divId).datum(chartData).transition().duration(100).call(chart);
window.setTimeout(function(){
drawChart(divId,matrixReportId);
}, 5000);
}
}
);
};
$(document).ready(function(){
drawChart('chart','00O90000005SSHv');
});
// Pie Chart Starts here
var width =250 ,
height = 450,
radius = Math.min(width, height) / 2;
var data = [{"age":"<5","population":2704659},{"age":"14-17","population":2159981},
{"age":"14-17","population":2159981},{"age":"18-24","population":3853788},
{"age":"25-44","population":14106543},{"age":"45-64","population":8819342},
{"age":">65","population":612463}];
var color = d3.scale.ordinal()
.range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]);
var arc = d3.svg.arc().outerRadius(radius - 10).innerRadius(0);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.population; });
var svg = d3.select("#body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var g = svg.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("fill", function(d) { return color(d.data.age); });
g.append("text")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return d.data.age; });
// Grouped and Stacked Bar Chart starts here
var n = 2, // number of layers
m = 10, // number of samples per layer
stack = d3.layout.stack(),
layers = stack(d3.range(n).map(function() { return bumpLayer(m, .1); })),
yGroupMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y; }); }),
yStackMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y0 + d.y; }); });
var margin = {top: 40, right: 10, bottom: 20, left: 10},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.domain(d3.range(m))
.rangeRoundBands([0, width], .08);
var y = d3.scale.linear()
.domain([0, yStackMax])
.range([height, 0]);
var color = d3.scale.linear()
.domain([0, n - 1])
.range(["#aad", "#556"]);
var xAxis = d3.svg.axis()
.scale(x)
.tickSize(0)
.tickPadding(6)
.orient("bottom");
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 + ")");
var layer = svg.selectAll(".layer")
.data(layers)
.enter().append("g")
.attr("class", "layer")
.style("fill", function(d, i) { return color(i); });
var rect = layer.selectAll("rect")
.data(function(d) { return d; })
.enter().append("rect")
.attr("x", function(d) { return x(d.x); })
.attr("y", height)
.attr("width", x.rangeBand())
.attr("height", 0);
rect.transition()
.delay(function(d, i) { return i * 10; })
.attr("y", function(d) { return y(d.y0 + d.y); })
.attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); });
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
d3.selectAll("input").on("change", change);
var timeout = setTimeout(function() {
d3.select("input[value=\"grouped\"]").property("checked", true).each(change);
}, 2000);
function change() {
clearTimeout(timeout);
if (this.value === "grouped") transitionGrouped();
else transitionStacked();
}
function transitionGrouped() {
y.domain([0, yGroupMax]);
rect.transition()
.duration(500)
.delay(function(d, i) { return i * 10; })
.attr("x", function(d, i, j) { return x(d.x) + x.rangeBand() / n * j; })
.attr("width", x.rangeBand() / n)
.transition()
.attr("y", function(d) { return y(d.y); })
.attr("height", function(d) { return height - y(d.y); });
}
function transitionStacked() {
y.domain([0, yStackMax]);
rect.transition()
.duration(500)
.delay(function(d, i) { return i * 10; })
.attr("y", function(d) { return y(d.y0 + d.y); })
.attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); })
.transition()
.attr("x", function(d) { return x(d.x); })
.attr("width", x.rangeBand());
}
// Inspired by Lee Byron's test data generator.
function bumpLayer(n, o) {
function bump(a) {
var x = 1/(.1 + Math.random()),
y = 2*Math.random()-.5,
z = 10/(.1 + Math.random());
for (var i = 0; i < n; i++) {
var w = (i/n- y) * z;
a[i] += x * Math.exp(-w * w);
}
}
var a=[],i;
for (i = 0; i < n; ++i) a[i] = o + o * Math.random();
for (i = 0; i < 5; ++i) bump(a);
return a.map(function(d, i) { return {x: i, y: Math.max(0, d)}; });
}
</script>
<svg id="chart" height="50%" width="500px" ></svg> // Id for Matrix chart
</apex:page>
Thanks in advance
first, as we discussed here (d3 Donut chart does not render), you should position your
<div id="body1" height="30%" width="90px"></div>
above the script.
second, you are missing the # in your second svg-declaration to correctly select the div by its id, it has to be
var svg = d3.select("#body1").append("svg")
you could also think about naming the second svg differently (eg svg2) so you don't override your first svg-variable (in case you want to do something with it later).