I want to make a visual that shows ordinal data (ratings). There are 12 rating dimensions, and each rating will have its own dedicated line appended to a circle. The polar orientation of the line designates a category (i.e. lines pointing to 1 o'clock = category 1, 2 o'clock = category 2, and so forth). The length of the line indicates the ratings value (short = bad, long = good). The result should resemble a snow flake or a sun burst.
The name is stored in a string. The ratings for each company are stored in an array. Here are two slices of my data variable:
{'fmc':'fmc1', 'ratings':[10,10,10,10,10,10,10,10,10,10,10,10]},
{'fmc':'fmc2', 'ratings':[8,10,10,5,10,10,10,10,10,7,10,5]},
I have the grid-system placement for the companies functioning, but there seems to be an issue with the way I'm aligning the lines about the circle. Relevant code:
var rotationDegree = d3.scalePoint().domain([0,12]).range([0, 2*Math.PI - Math.PI/6]);
fmcG.append('line')
.data([10,10,10,10,10,10,10,10,10,10,10,10])
.attr("x1", r)
.attr("y1", r)
.attr("x2", function(d,i) { return length(10) * Math.cos(rotationDegree(i) - Math.PI/2) + (width/2); })
.attr("y2", function(d,i) { return length(10) * Math.sin(rotationDegree(i) - Math.PI/2) + (height/2); })
.style("stroke", function(d) { return "#003366" });
It would seem that I have the trig mapped out correctly, but in implementation I am proven wrong: the lines are not being appended about the circle like a snow flake / sun burst / clock.
Snippet:
var margins = {top:20, bottom:300, left:30, right:100};
var height = 600;
var width = 900;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");
var data = [
//{'fmc':'fmc1', 'ratings':[{'r1':10,'r2':10,'r3':10,'r4':10,'r5':10}]}
{'fmc':'fmc1', 'ratings':[10,10,10,10,10,10,10,10,10,10,10,10]},
{'fmc':'fmc2', 'ratings':[8,10,10,5,10,10,10,10,10,7,10,5]},
{'fmc':'fmc3', 'ratings':[10,10,10,10,10,10,10,10,10,10,10,10]},
];
var r = 30;
var length = d3.scaleLinear().domain([0, 10]).range([0, 50]);
var rotationDegree = d3.scalePoint().domain([0,12]).range([0, 2*Math.PI - Math.PI/6]);
var columns = 5;
var spacing = 220;
var vSpacing = 250;
var fmcG = graphGroup.selectAll('.fmc')
.data(data)
.enter()
.append('g')
.attr('class', 'fmc')
.attr('id', (d,i) => 'fmc' + i)
.attr('transform', (d,k) => {
var horSpace = (k % columns) * spacing;
var vertSpace = ~~((k / columns)) * vSpacing;
return "translate("+horSpace+","+vertSpace+")";
});
fmcG.append('circle')
.attr('cx',100)
.attr('cy',100)
.attr('r', r)
.style('fill','none')
.style('stroke','#003366');
fmcG.append('text')
.attr('x',100)
.attr('y',105)
.style('text-anchor','middle')
.text(function(d) {return d.fmc});
fmcG.append('line')
//.data(function(d) {return d.ratings}) why doesnt it workk??????
.data([10,10,10,10,10,10,10,10,10,10,10,10])
.attr("x1", r)
.attr("y1", r)
.attr("x2", function(d,i) { return length(10) * Math.cos(rotationDegree(i) - Math.PI/2) + (width/2); })
.attr("y2", function(d,i) { return length(10) * Math.sin(rotationDegree(i) - Math.PI/2) + (height/2); })
.style("stroke", function(d) { return "#003366" });
<script src="https://d3js.org/d3.v5.min.js"></script>
Question
How can I take an 12-item array and append lines about the circle in 30 degree increments (360 divided by 12) while using the value of each item in the array to determine the line's length?
The main issue is that, right now, you're appending a single line. For appending as many lines as data points you have to set up a proper enter selection:
fmcG.selectAll(null)
.data(function(d) {
return d.ratings
})
.enter()
.append('line')
//etc...
And that, by the way, is the reason your data is not working (as you ask in your comment "why doesnt it workk??????")
Other issues:
A point scale needs to have a discrete domain, for instance d3.range(12)
For whatever reason you're moving the circles 100px right and down. I'm moving the lines by the same amount.
Here is the snippet with those changes:
var margins = {
top: 20,
bottom: 300,
left: 30,
right: 100
};
var height = 600;
var width = 900;
var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate(" + margins.left + "," + margins.top + ")");
var data = [
//{'fmc':'fmc1', 'ratings':[{'r1':10,'r2':10,'r3':10,'r4':10,'r5':10}]}
{
'fmc': 'fmc1',
'ratings': [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10]
},
{
'fmc': 'fmc2',
'ratings': [8, 10, 10, 5, 10, 10, 10, 10, 10, 7, 10, 5]
},
{
'fmc': 'fmc3',
'ratings': [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10]
},
];
var r = 30;
var length = d3.scaleLinear().domain([0, 10]).range([0, 50]);
var rotationDegree = d3.scalePoint().domain(d3.range(12)).range([0, 2 * Math.PI]);
var columns = 5;
var spacing = 220;
var vSpacing = 250;
var fmcG = graphGroup.selectAll('.fmc')
.data(data)
.enter()
.append('g')
.attr('class', 'fmc')
.attr('id', (d, i) => 'fmc' + i)
.attr('transform', (d, k) => {
var horSpace = (k % columns) * spacing;
var vertSpace = ~~((k / columns)) * vSpacing;
return "translate(" + horSpace + "," + vertSpace + ")";
});
fmcG.append('circle')
.attr('cx', 100)
.attr('cy', 100)
.attr('r', r)
.style('fill', 'none')
.style('stroke', '#003366');
fmcG.append('text')
.attr('x', 100)
.attr('y', 105)
.style('text-anchor', 'middle')
.text(function(d) {
return d.fmc
});
fmcG.selectAll(null)
.data(function(d) {
return d.ratings
})
.enter()
.append('line')
.attr("x1", 100)
.attr("y1", 100)
.attr("x2", function(d, i) {
return 100 + length(d) * Math.cos(rotationDegree(i));
})
.attr("y2", function(d, i) {
return 100 + length(d) * Math.sin(rotationDegree(i));
})
.style("stroke", function(d) {
return "#003366"
});
<script src="https://d3js.org/d3.v5.min.js"></script>
Related
I'm trying to create a grid of squares using D3. My code works, but I'd like to add some padding around each of the squares so that the grid does not look so "tight".
Here is my code:
class Matrix {
constructor(parentElement, data1, data2, data3) {
this.parentElement = parentElement;
this.data1 = data1;
this.data2 = data2;
this.data3 = data3;
this.cellHeight = 40;
this.cellWidth = 40;
this.initializeVisual()
}
initializeVisual() {
let vis = this;
vis.margin = {top: 20, right: 20, bottom: 20, left: 40};
vis.width = document.getElementById(vis.parentElement).getBoundingClientRect().width - vis.margin.left - vis.margin.right;
vis.height = 800 - vis.margin.top - vis.margin.bottom;
// init drawing area
vis.svg = d3.select('#' + vis.parentElement).append('svg')
.attr('width', vis.width + vis.margin.left + vis.margin.right)
.attr('height', vis.height + vis.margin.top + vis.margin.bottom)
.append('g')
.attr('transform', `translate (${vis.margin.left}, ${vis.margin.top})`);
// Draws the initial grid
let squaresPerRow = 16
let scale = d3.scaleLinear()
.domain([0, squaresPerRow -1])
.range([0, this.cellWidth * squaresPerRow])
vis.squares = vis.svg.selectAll('.squares')
.data(d3.range(256))
.enter()
.append('rect')
.attr('x', (d, i) => {
let n = i % squaresPerRow
return scale(n)
})
.attr('y', (d, i) => {
let n = Math.floor(i / 16)
return scale(n)
})
.attr('width', this.cellWidth)
.attr('height', this.cellHeight)
.attr('fill', 'lightgrey');
}
The resulting grid looks like this:
How can I add the padding?
Thanks!
Personally I'd change the linear scale for a point scale, but here's a solution using most of your code as it is.
First, get rid of that minus one in the domain, that's a cumbersome way for creating the padding:
.domain([0, squaresPerRow -1])
Then, after you set the padding (here named padding), you can translate the x and y positions by half of it, and subtracting the rectangles' width and height by that padding.
Here's a demo, change the variable padding for different spaces:
const svg = d3.select("div")
.append('svg')
.attr('width', 400)
.attr('height', 400);
let squaresPerRow = 8,
cellWidth = 40,
cellHeight = 40,
padding = 14;
let scale = d3.scaleLinear()
.domain([0, squaresPerRow])
.range([0, cellWidth * squaresPerRow])
const squares = svg.selectAll('.squares')
.data(d3.range(squaresPerRow ** 2))
.enter()
.append('rect')
.attr('x', (d, i) => {
let n = i % squaresPerRow
return scale(n) + padding / 2
})
.attr('y', (d, i) => {
let n = Math.floor(i / squaresPerRow)
return scale(n) + padding / 2
})
.attr('width', cellWidth - padding)
.attr('height', cellHeight - padding)
.attr('fill', 'lightgrey');
<script src="https://d3js.org/d3.v7.min.js"></script>
<div></div>
I have been looking to create a moving gauge and struggled to find an off the shelf solution. I then stumbled across the gauge posted in the below link. Currently it runs random numbers in the chart. I would like it to change value based on an array of numerical values (not %ages) that I have, over a timeframe that I specify. The numbers are currently a simple Excel column.
So the gauge would go through the thousand or so numbers I have across, say, a minute.
I'll be frank, I'm a novice at coding and have limited experience in NetLogo and R. Hence why I'm here asking for pointers.
Any advice would be greatly appreciated. Thank you.
https://codepen.io/leomarquine/pen/xGzMjZ
var size = 150,
thickness = 60;
var color = d3.scale.linear()
.domain([0, 50, 100])
.range(['#db2828', '#fbbd08', '#21ba45']);
// .domain([0, 17, 33, 50, 67, 83, 100])
// .range(['#db4639', '#db7f29', '#d1bf1f', '#92c51b', '#48ba17', '#12ab24', '#0f9f59']);
var arc = d3.svg.arc()
.innerRadius(size - thickness)
.outerRadius(size)
.startAngle(-Math.PI / 2);
var svg = d3.select('#chart').append('svg')
.attr('width', size * 2)
.attr('height', size + 20)
.attr('class', 'gauge');
var chart = svg.append('g')
.attr('transform', 'translate(' + size + ',' + size + ')')
var background = chart.append('path')
.datum({
endAngle: Math.PI / 2
})
.attr('class', 'background')
.attr('d', arc);
var foreground = chart.append('path')
.datum({
endAngle: -Math.PI / 2
})
.style('fill', '#db2828')
.attr('d', arc);
var value = svg.append('g')
.attr('transform', 'translate(' + size + ',' + (size * .9) + ')')
.append('text')
.text(0)
.attr('text-anchor', 'middle')
.attr('class', 'value');
var scale = svg.append('g')
.attr('transform', 'translate(' + size + ',' + (size + 15) + ')')
.attr('class', 'scale');
scale.append('text')
.text(100)
.attr('text-anchor', 'middle')
.attr('x', (size - thickness / 2));
scale.append('text')
.text(0)
.attr('text-anchor', 'middle')
.attr('x', -(size - thickness / 2));
setInterval(function() {
update(Math.random() * 100);
}, 1500);
function update(v) {
v = d3.format('.1f')(v);
foreground.transition()
.duration(750)
.style('fill', function() {
return color(v);
})
.call(arcTween, v);
value.transition()
.duration(750)
.call(textTween, v);
}
function arcTween(transition, v) {
var newAngle = v / 100 * Math.PI - Math.PI / 2;
transition.attrTween('d', function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return arc(d);
};
});
}
function textTween(transition, v) {
transition.tween('text', function() {
var interpolate = d3.interpolate(this.innerHTML, v),
split = (v + '').split('.'),
round = (split.length > 1) ? Math.pow(10, split[1].length) : 1;
return function(t) {
this.innerHTML = d3.format('.1f')(Math.round(interpolate(t) * round) / round) + '<tspan>%</tspan>';
};
});
}
So, recently I've started to try using D3 library. I've successfully made a working Chord Diagram, here's the pen.
var parentSelector = '#con1'
var width = '100vh'
var height = '100vh'
var cssid = 'chord1'
var cssclass = 'svgChord'
var labels = ['a', 'b', 'c', 'd', 'e']
var data = [[11, 21, 31, 41, 51], [12, 22, 32, 42, 52], [13, 23, 33, 43, 53], [14, 24, 34, 44, 54], [15, 25, 35, 45, 55]]
var colors = ['#ddffdd', '#fd8b43', '#00922c', '#ffffff', '#F0d064']
var getWidth = function () { return document.querySelector('#' + cssid + '.' + cssclass).clientWidth }
var getHeight = function () { return document.querySelector('#' + cssid + '.' + cssclass).clientHeight }
var svg = d3.select(parentSelector)
.append('svg:svg')
.attr('width', width)
.attr('height', height)
.attr('class', cssclass)
.attr('id', cssid)
.attr('viewBox', '0,0,' + getWidth() + ',' + getHeight())
var outerRadius = Math.min(getWidth(), getHeight()) * 0.5 - 40
var innerRadius = outerRadius - 30
var colorsMap = d3.scaleOrdinal().range(colors)
var arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
var chord = d3.chord()
.padAngle(0.05)
.sortSubgroups(d3.descending)
var ribbon = d3.ribbon()
.radius(innerRadius)
var g = svg.append('g')
.attr('transform', 'translate(' + getWidth() / 2 + ',' + getHeight() / 2 + ')')
.datum(chord(data))
var groups = g.append('g')
.attr('class', 'groups')
var group = groups.selectAll('g')
.data(function (chords) { return chords.groups })
.enter()
.append('g')
.attr('class', 'group')
var arcSvg = group.append('path')
.attr('class', 'arc')
.attr('d', arc)
.style('fill', function (d) { return colorsMap(d.index) })
.style('stroke', function (d) { return d3.rgb(colorsMap(d.index)).darker() })
group.append('text')
// .attr('x', function (d) { return arc.centroid()[d.index][0] }) // doesn't work
// .attr('y', function (d) { return arc.centroid()[d.index][1] }) // doesn't work
.attr('class', 'group-label')
.text(function (d) { return labels[d.index] })
var ribbons = g.append('g')
.attr('class', 'ribbons')
.selectAll('path')
.data(function (d) { return d })
.enter()
.append('path')
.attr('d', ribbon)
.style('fill', function (d) { return colorsMap(d.target.index) })
.style('stroke', function (d) { return d3.rgb(colorsMap(d.target.index)).darker() })
g.groups text.group-label {
font: 11px sans-serif;
pointer-events: none;
z-index: 100000;
font-size: 30px;
font-weight: 700;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<div class="container" id="con1"></div>
And now I need to add some info to it to make it more useful. But for some reason I could not achieve adding labels on group sectors on their respective center points using arc.centroid(). I've tried to recreate some examples (1, 2, 3, 4, 5), but it didn't work out. For some reason, in my case arc.centroid() or anything_else.centroid() throws errors in console. Also, D3 produces very complicated objects, so I don't understand definitions in scope, when debugging with breakpoints.
So, here's the question: how to change my code to achieve required result - drawing svg text over centroids of each svg arc?
The problem was in the way you were trying to calculate the arc centroid; you need to specify the start and end angle, and then output the results in a suitable form. The easiest way to do so it using a transform, so then you only calculate the centroid position once, rather than trying to calculate x and y separately:
group.append('text')
.attr('transform', function (d) {
return 'translate(' +
arc.startAngle(d.startAngle)
.endAngle(d.endAngle)
.centroid() // this is an array, so will automatically be printed out as x,y
+ ')'
})
This gets the labels into approximately the correct position, but you can refine it further by translating the letters down slightly (the text baseline is currently at the centroid point, so moving the text down by 0.35em places the vertical centre of the text closer to the middle) and centring the labels using text-anchor: middle:
group.append('text')
.attr('transform', function (d) {
return 'translate(' +
arc.startAngle(d.startAngle)
.endAngle(d.endAngle)
.centroid() // this is an array, so will automatically be printed out as x,y
+ ')'
})
.attr('dy', '.35em')
.attr('text-anchor', 'middle')
.attr('class', 'group-label')
.text(function (d) { return labels[d.index] })
In context:
var parentSelector = '#con1'
var width = '100vh'
var height = '100vh'
var cssid = 'chord1'
var cssclass = 'svgChord'
var labels = ['a', 'b', 'c', 'd', 'e']
var data = [[11, 21, 31, 41, 51], [12, 22, 32, 42, 52], [13, 23, 33, 43, 53], [14, 24, 34, 44, 54], [15, 25, 35, 45, 55]]
var colors = ['#ddffdd', '#fd8b43', '#00922c', '#ffffff', '#F0d064']
var getWidth = function () { return document.querySelector('#' + cssid + '.' + cssclass).clientWidth }
var getHeight = function () { return document.querySelector('#' + cssid + '.' + cssclass).clientHeight }
var svg = d3.select(parentSelector)
.append('svg:svg')
.attr('width', width)
.attr('height', height)
.attr('class', cssclass)
.attr('id', cssid)
.attr('viewBox', '0,0,' + getWidth() + ',' + getHeight())
var outerRadius = Math.min(getWidth(), getHeight()) * 0.5 - 40
var innerRadius = outerRadius - 30
var colorsMap = d3.scaleOrdinal().range(colors)
var arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
var chord = d3.chord()
.padAngle(0.05)
.sortSubgroups(d3.descending)
var ribbon = d3.ribbon()
.radius(innerRadius)
var g = svg.append('g')
.attr('transform', 'translate(' + getWidth() / 2 + ',' + getHeight() / 2 + ')')
.datum(chord(data))
var groups = g.append('g')
.attr('class', 'groups')
var group = groups.selectAll('g')
.data(function (chords) { return chords.groups })
.enter()
.append('g')
.attr('class', 'group')
var arcSvg = group.append('path')
.attr('class', 'arc')
.attr('d', arc)
.style('fill', function (d) { return colorsMap(d.index) })
.style('stroke', function (d) { return d3.rgb(colorsMap(d.index)).darker() })
group.append('text')
.attr('dy', '.35em')
.attr('transform', function (d) {
return 'translate('
+ arc
.startAngle(d.startAngle)
.endAngle(d.endAngle)
.centroid() + ')'
})
.attr('class', 'group-label')
.attr('text-anchor', 'middle')
.text(function (d) { return labels[d.index] })
var ribbons = g.append('g')
.attr('class', 'ribbons')
.selectAll('path')
.data(function (d) { return d })
.enter()
.append('path')
.attr('d', ribbon)
.style('fill', function (d) { return colorsMap(d.target.index) })
.style('stroke', function (d) { return d3.rgb(colorsMap(d.target.index)).darker() })
g.groups text.group-label {
font: 11px sans-serif;
pointer-events: none;
z-index: 100000;
font-size: 30px;
font-weight: 700;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<div class="container" id="con1"></div>
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/
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>