Create transitions on data change d3.js - javascript

I'm trying to create a chart which basically has a number of horizontal lines on it. The data for the lines is taken from the function that generates random arrays of objects like this:
{
"process": 2,
"time": 5
}
And also there is a setInterval function that redraws the chart every 3s. How can I make it change not that abruptly, but with easing and transitions?
Here is my code below:
html:
<div class="container">
<div class="jumbotron"><h3>Operating System Processes Model</h3></div>
<div class="enter">Enter the number of processes: </div>
<input type="number" min="0" max="30" id="input">
</div>
<svg id="visualisation" width="1000" height="500"></svg>
utils.js:
// Renders grid in the system of axes
function renderGrid() {
// Vertical lines
for(var i = 5; i <= 50; i+=5) {
vis.append('svg:path')
.attr('d', lineGen(
[
{
"process": "0",
"time": + i
},
{
"process": "50",
"time": + i
},
]
))
.attr('stroke', '#777')
.attr('stroke-dasharray', "5,5")
.attr('stroke-width', 1)
.attr('fill', 'none');
// Horizontal lines
vis.append('svg:path')
.attr('d', lineGen(
[
{
"process": i,
"time": "0"
},
{
"process": i,
"time": "50"
},
]
))
.attr('stroke', '#777')
.attr('stroke-dasharray', "5,5")
.attr('stroke-width', 1)
.attr('fill', 'none');
};
}
// Generate single line
var lineGen = d3.svg.line()
.x(function(d) {
return xScale(d.time);
})
.y(function(d) {
return yScale(d.process);
})
.interpolate("basis");
// Generate random color
function getRandomColor() {
var letters = '0123456789ABCDEF'.split('');
var color = '#';
for (var i = 0; i < 6; i++ ) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
// Generate random data array
function getDataArray(count) {
var array = [];
for(var i = 0; i < count; ++i) {
var proc = Math.round(Math.random() * (50 - 1)) + 1;
var time1 = Math.round(Math.random() * (50 - 1)) + 1;
var time2 = Math.round(Math.random() * (50 - 1)) + 1;
var data = [
{
"process": proc,
"time": Math.max(time1, time2)
},
{
"process": proc,
"time": Math.min(time1, time2)
},
];
array.push(data);
}
return array;
}
// Generate random data
function renderProcesses() {
vis.selectAll('.line-process').remove();
// var processCount = Math.round(Math.random() * (50 - 1)) + 1;
var processCount = document.getElementById('input').value;
var arr = getDataArray(processCount);
for(var i = 0; i < processCount; ++i) {
var processLength = Math.round(Math.random() * (50 - 1)) + 1;
var color = getRandomColor();
vis.append('svg:path')
.attr('class', 'line-process')
.attr('d', lineGen(arr[i]))
.attr('stroke', color)
.attr('stroke-width', 5)
.attr('stroke-linecap', 'round')
.attr('fill', 'none')
.on("mouseover", function() {
d3.select(this)
.attr('stroke', "#1abc9c");
})
.on("mouseout", function() {
d3.select(this)
.attr('stroke', color)
});
}
}
axis.js:
// Draw the axis
var vis = d3.select("#visualisation"),
width = 1000,
height = 500,
margins = {
top: 20,
right: 20,
bottom: 20,
left: 50
},
xScale = d3.scale.linear().range([margins.left, width - margins.right]).domain([0,50]),
yScale = d3.scale.linear().range([height - margins.top, margins.bottom]).domain([0,50]),
xAxis = d3.svg.axis()
.scale(xScale),
yAxis = d3.svg.axis()
.scale(yScale)
.orient('left');
vis.append("svg:g")
.transition()
.attr("transform", "translate(0," + (height - margins.bottom) + ")")
.attr("class", "axis")
.call(xAxis);
vis.append("svg:g")
.transition()
.attr("transform", "translate(" + (margins.left) + ",0)")
.attr("class", "axis")
.call(yAxis);
styles for the axises:
.axis {
path {
fill: none;
stroke: #777;
shape-rendering: crispEdges;
}
text {
font-family: Lato;
font-size: 12px;
}
}
and finally, main.js:
renderGrid();
setInterval(renderProcesses, 3000);
Here is the screenshot:
So, Could you help me with those transitions?
And, another question, as you can see on the screenshot, some lines can be drown on each other. How can I avoid it?

Related

How to group labels in Timeline chart in JS

I really need some help. I'm trying to make my own Timeline chart (the same as google timeline). It works already but I can not figure out how to make a function for grouping labels by type. I hardcoded this function and it works for 3 groups/types. What I need is to make a universal function that groups all labels even if there are 100 of them. The second problem is that in the second and third groups are not in the same line if a.endTime >= b.startTime. I will be very thankful for every help
Here is my code:
var w = 800;
var h = 400;
var svg = d3
.select(".svg")
.append("svg")
.attr("width", w)
.attr("height", h)
.attr("class", "svg");
//main array
var items = [{
task: "conceptualize",
type: "development",
startTime: "2013-1-28", //year/month/day
endTime: "2013-2-1",
number: 2,
},
{
task: "sketch",
type: "development",
startTime: "2013-2-6",
endTime: "2013-2-9",
number: 2,
},
{
task: "color profiles",
type: "development",
startTime: "2013-2-6",
endTime: "2013-2-9",
number: 2,
},
{
task: "HTML",
type: "coding",
startTime: "2013-2-2",
endTime: "2013-2-6",
number: 1,
},
{
task: "write the JS",
type: "coding",
startTime: "2013-2-1",
endTime: "2013-2-6",
number: 1,
},
{
task: "eat",
type: "celebration",
startTime: "2013-2-8",
endTime: "2013-2-13",
number: 0,
},
{
task: "crying",
type: "celebration",
startTime: "2013-2-13",
endTime: "2013-2-16",
number: 0,
},
];
//array sorting
items.sort((a, b) => {
return (
a.number - b.number || Date.parse(b.startTime) - Date.parse(a.startTime)
);
});
//here create new array and add index for every label
var taskArray = [];
var stack = [];
items.map((e) => {
var lane = stack.findIndex(
(s) => s.endTime <= e.startTime && s.startTime < e.startTime
);
var yIndex = lane === -1 ? stack.length : lane;
taskArray.push({
...e,
yIndex,
});
stack[yIndex] = e;
});
//here hardcoded grouping based on index and type
var nRows = d3.max(taskArray, (d) =>
d.type === "celebration" ? d.yIndex + 1 : null
);
//here hardcoded grouping based on index and type
var nRows2 = d3.max(taskArray, (d) =>
d.type === "development" ? (d.yIndex = d.yIndex + 1 + nRows) : null
);
var dateFormat = d3.time.format("%Y-%m-%d");
var timeScale = d3.time
.scale()
.domain([
d3.min(taskArray, function(d) {
return dateFormat.parse(d.startTime);
}),
d3.max(taskArray, function(d) {
return dateFormat.parse(d.endTime);
}),
])
.range([0, w - 150]);
var categories = new Array();
for (var i = 0; i < taskArray.length; i++) {
categories.push(taskArray[i].type);
}
var catsUnfiltered = categories; //for vert labels
categories = checkUnique(categories);
makeGant(taskArray, w, h);
function makeGant(tasks, pageWidth, pageHeight) {
var barHeight = 20;
var gap = barHeight + 4;
var topPadding = 75;
var sidePadding = 75;
var colorScale = d3.scale
.linear()
.domain([0, categories.length])
.range(["#00B9FA", "#F95002"])
.interpolate(d3.interpolateHcl);
makeGrid(sidePadding, topPadding, pageWidth, pageHeight);
drawRects(
tasks,
gap,
topPadding,
sidePadding,
barHeight,
colorScale,
pageWidth,
pageHeight
);
vertLabels(gap, topPadding, sidePadding, barHeight, colorScale);
}
function drawRects(
theArray,
theGap,
theTopPad,
theSidePad,
theBarHeight,
theColorScale,
w,
h
) {
svg
.append("g")
.selectAll("rect")
.data(theArray)
.enter()
.append("rect")
.attr("x", 0)
.attr("y", function(d, i) {
return d.yIndex * theGap + theTopPad - 2;
})
.attr("width", function(d) {
return w - theSidePad / 2;
})
.attr("height", theGap)
.attr("stroke", "none")
.attr("fill", function(d) {
for (var i = 0; i < categories.length; i++) {
if (d.type == categories[i]) {
return d3.rgb(theColorScale(i));
}
}
})
.attr("opacity", 0.2);
var rectangles = svg.append("g").selectAll("rect").data(theArray).enter();
rectangles
.append("rect")
.attr("rx", 3)
.attr("ry", 3)
.attr("x", function(d) {
return timeScale(dateFormat.parse(d.startTime)) + theSidePad;
})
//here draw milestones depend on index
.attr("y", function(d, i) {
return d.yIndex * theGap + theTopPad;
})
.attr("width", function(d) {
return (
timeScale(dateFormat.parse(d.endTime)) -
timeScale(dateFormat.parse(d.startTime))
);
})
.attr("height", theBarHeight)
.attr("stroke", "none")
.attr("fill", function(d) {
for (var i = 0; i < categories.length; i++) {
if (d.type == categories[i]) {
return d3.rgb(theColorScale(i));
}
}
});
rectangles
.append("text")
.text(function(d) {
return d.task;
})
.attr("x", function(d) {
return (
(timeScale(dateFormat.parse(d.endTime)) -
timeScale(dateFormat.parse(d.startTime))) /
2 +
timeScale(dateFormat.parse(d.startTime)) +
theSidePad
);
})
.attr("y", function(d, i) {
return d.yIndex * theGap + 14 + theTopPad;
})
.attr("font-size", 11)
.attr("text-anchor", "middle")
.attr("text-height", theBarHeight)
.attr("fill", "#fff");
}
function makeGrid(theSidePad, theTopPad, w, h) {
var xAxis = d3.svg
.axis()
.scale(timeScale)
.orient("bottom")
.ticks(d3.time.days, 1)
.tickSize(-h + theTopPad + 20, 0, 0)
.tickFormat(d3.time.format("%d %b"));
var grid = svg
.append("g")
.attr("class", "grid")
.attr("transform", "translate(" + theSidePad + ", " + (h - 50) + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "middle")
.attr("fill", "#000")
.attr("stroke", "none")
.attr("font-size", 10)
.attr("dy", "1em");
}
function vertLabels(
theGap,
theTopPad,
theSidePad,
theBarHeight,
theColorScale
) {
var numOccurances = new Array();
var prevGap = 0;
for (var i = 0; i < categories.length; i++) {
numOccurances[i] = [categories[i], getCount(categories[i], catsUnfiltered)];
}
var axisText = svg
.append("g") //without doing this, impossible to put grid lines behind text
.selectAll("text")
.data(numOccurances)
.enter()
.append("text")
.text(function(d) {
return d[0];
})
.attr("x", 10)
.attr("y", function(d, i) {
if (i > 0) {
for (var j = 0; j < i; j++) {
prevGap += numOccurances[i - 1][1];
// console.log(prevGap);
return (d[1] * theGap) / 2 + prevGap * theGap + theTopPad;
}
} else {
return (d[1] * theGap) / 2 + theTopPad;
}
})
.attr("font-size", 11)
.attr("text-anchor", "start")
.attr("text-height", 14)
.attr("fill", function(d) {
for (var i = 0; i < categories.length; i++) {
if (d[0] == categories[i]) {
// console.log("true!");
return d3.rgb(theColorScale(i)).darker();
}
}
});
}
//from this stackexchange question: http://stackoverflow.com/questions/1890203/unique-for-arrays-in-javascript
function checkUnique(arr) {
var hash = {},
result = [];
for (var i = 0, l = arr.length; i < l; ++i) {
if (!hash.hasOwnProperty(arr[i])) {
//it works with objects! in FF, at least
hash[arr[i]] = true;
result.push(arr[i]);
}
}
return result;
}
//from this stackexchange question: http://stackoverflow.com/questions/14227981/count-how-many-strings-in-an-array-have-duplicates-in-the-same-array
function getCounts(arr) {
var i = arr.length, // var to loop over
obj = {}; // obj to store results
while (i) obj[arr[--i]] = (obj[arr[i]] || 0) + 1; // count occurrences
return obj;
}
// get specific from everything
function getCount(word, arr) {
return getCounts(arr)[word] || 0;
}
body {
background: #fff;
font-family: 'Open-Sans', sans-serif;
}
#container {
margin: 0 auto;
position: relative;
width: 800px;
overflow: visible;
}
.svg {
width: 800px;
height: 400px;
overflow: visible;
position: absolute;
}
.grid .tick {
stroke: lightgrey;
opacity: 0.3;
shape-rendering: crispEdges;
}
.grid path {
stroke-width: 0;
}
<div id="container">
<div class="svg"></div>
</div>
<script src="https://d3js.org/d3.v3.min.js"></script>
You can massively simplify your code, while making it dynamic. Some pointers in addition to the comments I left
Just parse your date objects ASAP, instead of doing it every time you need them;
Use d3.nest to group the categories;
Don't iterate over the categories to get the index for colouring, just use the second function argument (d, i);
Draw one g element per category, one rect.background within the g, and the tasks that fit that category as well. That way, you can remove categories extremely easily, it makes it much easier to calculate position, and you need to set the fill colours only once.
Let me know if you have any questions
var w = 800;
var h = 400;
var svg = d3
.select(".svg")
.append("svg")
.attr("width", w)
.attr("height", h)
.attr("class", "svg");
//main array
var items = [{
task: "conceptualize",
type: "development",
startTime: "2013-1-28", //year/month/day
endTime: "2013-2-1",
number: 2,
},
{
task: "sketch",
type: "development",
startTime: "2013-2-6",
endTime: "2013-2-9",
number: 2,
},
{
task: "color profiles",
type: "development",
startTime: "2013-2-6",
endTime: "2013-2-9",
number: 2,
},
{
task: "HTML",
type: "coding",
startTime: "2013-2-2",
endTime: "2013-2-6",
number: 1,
},
{
task: "write the JS",
type: "coding",
startTime: "2013-2-1",
endTime: "2013-2-6",
number: 1,
},
{
task: "eat",
type: "celebration",
startTime: "2013-2-8",
endTime: "2013-2-13",
number: 0,
},
{
task: "crying",
type: "celebration",
startTime: "2013-2-13",
endTime: "2013-2-16",
number: 0,
},
];
var dateFormat = d3.time.format("%Y-%m-%d");
items.forEach((e) => {
e.startTime = dateFormat.parse(e.startTime);
e.endTime = dateFormat.parse(e.endTime);
});
// Use d3.nest to group the items based on the category
// https://github.com/d3/d3-3.x-api-reference/blob/master/Arrays.md#d3_nest
// You can use .sortKeys to sort by key, for example
var categories = d3.nest()
.key(d => d.type)
.sortValues((a, b) => a.startTime - b.startTime)
.rollup(function(events) {
// Here, we see if we can apply some useful logic
// In this case, we search for overlapping events so they can be placed
// in different lanes
events.forEach(function(e, i) {
// Look only at the preceding events
// Remember that the events have been sorted already
const overlappingEvents = events.slice(0, i)
.filter(other => other.endTime > e.startTime);
if(overlappingEvents.length > 0) {
e.level = d3.max(overlappingEvents, e => e.level) + 1;
} else {
e.level = 0;
}
});
return events;
})
.entries(items);
// Set for each category the required number of lanes
let offset = 0;
categories.forEach(c => {
c.lanes = d3.max(c.values, d => d.level) + 1;
c.offset = offset;
offset += c.lanes;
});
var timeScale = d3.time
.scale()
.domain([
d3.min(items, d => d.startTime),
d3.max(items, d => d.endTime)
])
.range([0, w - 150]);
makeGant(categories, w, h);
function makeGant(categories, pageWidth, pageHeight) {
var barHeight = 20;
var gap = barHeight + 4;
var topPadding = 75;
var sidePadding = 75;
var colorScale = d3.scale
.linear()
.domain([0, categories.length])
.range(["#00B9FA", "#F95002"])
.interpolate(d3.interpolateHcl);
makeGrid(sidePadding, topPadding, pageWidth, pageHeight);
drawRects(
categories,
gap,
topPadding,
sidePadding,
barHeight,
colorScale,
pageWidth,
pageHeight
);
vertLabels(gap, topPadding, sidePadding, barHeight, colorScale);
}
function drawRects(
categories,
theGap,
theTopPad,
theSidePad,
theBarHeight,
theColorScale,
w,
h
) {
const categoryGroups = svg
.append("g")
.selectAll("g")
.data(categories)
.enter()
.append("g")
.attr("transform", function(d, i) {
// Take the preceding categories and sum their required number of levels
return `translate(0, ${d.offset * theGap + theTopPad - 2})`;
})
// All children inherit this attribute
.attr("fill", function(d, i) {
return d3.rgb(theColorScale(i));
})
// Just draw one rectangle for all lanes of this category
categoryGroups
.append("rect")
.attr("class", "background")
.attr("width", function(d) {
return w - theSidePad / 2;
})
.attr("height", function(d) {
return theGap * d.lanes;
})
.attr("stroke", "none")
.attr("opacity", 0.2);
//
var rectangles = categoryGroups
.selectAll(".task")
.data(function(d) { return d.values; })
.enter();
rectangles
.append("rect")
.attr("class", "task")
.attr("rx", 3)
.attr("ry", 3)
.attr("x", function(d) {
return timeScale(d.startTime) + theSidePad;
})
//here draw milestones depend on index
.attr("y", function(d, i) {
return d.level * theGap;
})
.attr("width", function(d) {
return timeScale(d.endTime) - timeScale(d.startTime);
})
.attr("height", theBarHeight)
.attr("stroke", "none");
rectangles
.append("text")
.text(function(d) {
return d.task;
})
.attr("x", function(d) {
return (timeScale(d.endTime) - timeScale(d.startTime)) / 2 +
timeScale(d.startTime) + theSidePad;
})
.attr("y", function(d, i) {
return d.level * theGap + 14;
})
.attr("font-size", 11)
.attr("text-anchor", "middle")
.attr("text-height", theBarHeight)
.attr("fill", "#fff");
}
function makeGrid(theSidePad, theTopPad, w, h) {
var xAxis = d3.svg
.axis()
.scale(timeScale)
.orient("bottom")
.ticks(d3.time.days, 1)
.tickSize(-h + theTopPad + 20, 0, 0)
.tickFormat(d3.time.format("%d %b"));
var grid = svg
.append("g")
.attr("class", "grid")
.attr("transform", "translate(" + theSidePad + ", " + (h - 50) + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "middle")
.attr("fill", "#000")
.attr("stroke", "none")
.attr("font-size", 10)
.attr("dy", "1em");
}
function vertLabels(
theGap,
theTopPad,
theSidePad,
theBarHeight,
theColorScale
) {
var axisText = svg
.append("g") //without doing this, impossible to put grid lines behind text
.selectAll("text")
.data(categories) // one label per category
.enter()
.append("text")
.text(function(d) {
return d.key;
})
.attr("x", 10)
.attr("y", function(d, i) {
return (d.offset + d.lanes / 2) * theGap + theTopPad;
})
.attr("font-size", 11)
.attr("text-anchor", "start")
.attr("text-height", 14)
.attr("fill", function(d, i) {
return d3.rgb(theColorScale(i)).darker();
});
}
body {
background: #fff;
font-family: 'Open-Sans', sans-serif;
}
#container {
margin: 0 auto;
position: relative;
width: 800px;
overflow: visible;
}
.svg {
width: 800px;
height: 400px;
overflow: visible;
position: absolute;
}
.grid .tick {
stroke: lightgrey;
opacity: 0.3;
shape-rendering: crispEdges;
}
.grid path {
stroke-width: 0;
}
<div id="container">
<div class="svg"></div>
</div>
<script src="https://d3js.org/d3.v3.js"></script>

Limit number of Y axis ticks by keeping a top tick above the bar in d3

In my d3 bar chart, I should have a top Y-axis tick (with horizontal grid line) above the tallest bar if it goes above the last tick. This is achieved by calculating the last tick, then applied using tickValues().
Also there should be maximum 5 ticks and grid lines including the x-axis domain (0). I have tried this using ticks() but it is not working with tickValues(). Any solution for this?
// container size
var margin = {top: 10, right: 10, bottom: 30, left: 30},
width = 400,
height = 300;
var data = [
{"month":"DEC","setup":{"count":26,"id":1,"label":"Set Up","year":"2016","graphType":"setup"}},
{"month":"JAN","setup":{"count":30,"id":1,"label":"Set Up","year":"2017","graphType":"setup"}},
{"month":"FEB","setup":{"count":30,"id":1,"label":"Set Up","year":"2017","graphType":"setup"}}];
var name = 'dashboard';
// x scale
var xScale = d3.scale.ordinal()
.rangeRoundBands([0, width], 0.2);
// set x and y scales
xScale.domain(data.map(function(d) { return d.month; }));
// x axis
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom')
.outerTickSize(0);
var yScale = d3.scale.linear()
.domain([0, d3.max(data, function(d) {
return d.setup.count;
})])
.range([height, 0]);
var ticks = yScale.ticks(),
lastTick = ticks[ticks.length-1];
var newLastTick = lastTick + (ticks[1] - ticks[0]);
if (lastTick < yScale.domain()[1]){
ticks.push(lastTick + (ticks[1] - ticks[0]));
}
// adjust domain for further value
yScale.domain([yScale.domain()[0], newLastTick]);
// y axis
var yAxis = d3.svg.axis()
.scale(yScale)
.orient('left')
.tickSize(-width, 0, 0)
.tickFormat(d3.format('d'))
.tickValues(ticks);
// create svg container
var svg = d3.select('#chart')
.append('svg')
.attr('class','d3-setup-barchart')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
//.on('mouseout', tip.hide);
// apply tooltip
//svg.call(tip);
// Horizontal grid (y axis gridline)
svg.append('g')
.attr('class', 'grid horizontal')
.call(d3.svg.axis()
.scale(yScale)
.orient('left')
.tickSize(-width, 0, 0)
.tickFormat('')
.tickValues(ticks)
);
// create bars
var bars = svg.selectAll('.bar')
.data(data)
.enter()
.append('g');
bars.append('rect')
.attr('class', function(d,i) {
return 'bar';
})
.attr('id', function(d, i) {
return name+'-bar-'+i;
})
.attr('x', function(d) { return xScale(d.month); })
.attr('width', xScale.rangeBand())
.attr('y', function(d) { return yScale(d.setup.count); })
.attr('height', function(d) { return height - yScale(d.setup.count); })
.on('click', function(d, i) {
d3.select(this.nextSibling)
.classed('label-text selected', true);
d3.select(this)
.classed('bar selected', true);
d3.select('#'+name+'-axis-text-'+i)
.classed('axis-text selected', true);
});
//.on('mouseover', tip.show)
//.on('mouseout', tip.hide);
// apply text at the top
bars.append('text')
.attr('class',function(d,i) {
return 'label-text';
})
.attr('x', function(d) { return xScale(d.month) + (xScale.rangeBand()/2) - 10; })
.attr('y', function(d) { return yScale(d.setup.count) + 2 ; })
.attr('transform', function() { return 'translate(10, -10)'; })
.text(function(d) { return d.setup.count; });
// draw x axis
svg.append('g')
.attr('id', name+'-x-axis')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
// apply class & id to x-axis texts
d3.select('#'+name+'-x-axis')
.selectAll('text')
.attr('class', function(d,i) {
return 'axis-text';
})
.attr('id', function(d,i) { return name+'-axis-text-' + i; });
// draw y axis
svg.append('g')
.attr('class', 'y axis')
.call(yAxis)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end');
// remove 0 in y axis
svg.select('.y')
.selectAll('.tick')
.filter(function (d) {
return d === 0 || d % 1 !== 0;
}).remove();
svg
.select('.horizontal')
.selectAll('.tick')
.filter(function (d) {
return d === 0 || d % 1 !== 0;
}).remove();
JSFiddle
As I told you in my comment, this would be very easy if you were using D3 v4.x: you could simply set the tickValues using d3.ticks or d3.range.
But there is a solution if you want to stick with D3 v3.
The default approach in your case would be setting the number of ticks using scale.ticks. However, as the API says,
If count is a number, then approximately count ticks will be returned. If count is not specified, it defaults to 10. The specified count is only a hint; the scale may return more or fewer values depending on the input domain. (emphasis mine)
So, you can't use scale.ticks here to set a fixed number of 5 ticks.
My solution, therefore, involves creating your own function to calculate the ticks. It's not complicated at all. This is it:
function createTicks(start, stop, count) {
var difference = stop - start;
var steps = difference / (count - 1);
var arr = [start];
for (var i = 1; i < count; i++) {
arr.push(~~(start + steps * i))
}
return arr;
}
This function takes three arguments: the first value (start), the last value (stop) and the number of ticks (count). I'm using the double NOT because, for whatever reason, you are filtering out non-integer values.
So, we just need to set the maximum tick in the yScale domain itself. For instance, making the maximum tick 10% bigger than the maximum value:
var yScale = d3.scale.linear()
.domain([0, d3.max(data, function(d) {
return d.setup.count;
}) * 1.1])
// ^----- 10% increase
.range([height, 0]);
(if you want, you can keep your math to get the new last tick, I'm just showing a different way to set a maximum value for the domain which is different from the maximum value in the data)
Then, we define the ticks for the y axis:
var axisTicks = createTicks(yScale.domain()[0], yScale.domain()[1], 5);
Using our customised function with your domain, it returns this array:
[0, 8, 16, 24, 33]
Then, it's just a matter of using that array in axis.tickValues.
Here is your updated fiddle: https://jsfiddle.net/7ktzpnno/
And here the same code in the Stack snippet:
// container size
var margin = {
top: 10,
right: 10,
bottom: 30,
left: 30
},
width = 400,
height = 300;
var data = [{
"month": "DEC",
"setup": {
"count": 26,
"id": 1,
"label": "Set Up",
"year": "2016",
"graphType": "setup"
}
}, {
"month": "JAN",
"setup": {
"count": 30,
"id": 1,
"label": "Set Up",
"year": "2017",
"graphType": "setup"
}
}, {
"month": "FEB",
"setup": {
"count": 30,
"id": 1,
"label": "Set Up",
"year": "2017",
"graphType": "setup"
}
}];
var name = 'dashboard';
// x scale
var xScale = d3.scale.ordinal()
.rangeRoundBands([0, width], 0.2);
// set x and y scales
xScale.domain(data.map(function(d) {
return d.month;
}));
// x axis
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom')
.outerTickSize(0);
var yScale = d3.scale.linear()
.domain([0, d3.max(data, function(d) {
return d.setup.count;
}) * 1.1])
.range([height, 0]);
var axisTicks = createTicks(yScale.domain()[0], yScale.domain()[1], 5);
function createTicks(start, stop, count) {
var difference = stop - start;
var steps = difference / (count - 1);
var arr = [start];
for (var i = 1; i < count; i++) {
arr.push(~~(start + steps * i))
}
return arr;
}
// y axis
var yAxis = d3.svg.axis()
.scale(yScale)
.orient('left')
.tickSize(-width, 0, 0)
.tickValues(axisTicks);
// tooltip
// var tip = d3.tip()
// .attr('class', 'd3-tip')
// .offset([-10, 0])
// .html(function(d) {
// return '<span class="tooltip-line">'+d.patientSetup.label+': '+
// d.patientSetup.count + '</span><span>'+d.patientNotSetup.label+': '+
// d.patientNotSetup.count + '</span>';
// });
// create svg container
var svg = d3.select('#chart')
.append('svg')
.attr('class', 'd3-setup-barchart')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
//.on('mouseout', tip.hide);
// apply tooltip
//svg.call(tip);
// Horizontal grid (y axis gridline)
svg.append('g')
.attr('class', 'grid horizontal')
.call(d3.svg.axis()
.scale(yScale)
.orient('left')
.tickSize(-width, 0, 0)
.tickValues(axisTicks)
);
// create bars
var bars = svg.selectAll('.bar')
.data(data)
.enter()
.append('g');
bars.append('rect')
.attr('class', function(d, i) {
return 'bar';
})
.attr('id', function(d, i) {
return name + '-bar-' + i;
})
.attr('x', function(d) {
return xScale(d.month);
})
.attr('width', xScale.rangeBand())
.attr('y', function(d) {
return yScale(d.setup.count);
})
.attr('height', function(d) {
return height - yScale(d.setup.count);
})
.on('click', function(d, i) {
d3.select(this.nextSibling)
.classed('label-text selected', true);
d3.select(this)
.classed('bar selected', true);
d3.select('#' + name + '-axis-text-' + i)
.classed('axis-text selected', true);
});
//.on('mouseover', tip.show)
//.on('mouseout', tip.hide);
// apply text at the top
bars.append('text')
.attr('class', function(d, i) {
return 'label-text';
})
.attr('x', function(d) {
return xScale(d.month) + (xScale.rangeBand() / 2) - 10;
})
.attr('y', function(d) {
return yScale(d.setup.count) + 2;
})
.attr('transform', function() {
return 'translate(10, -10)';
})
.text(function(d) {
return d.setup.count;
});
// draw x axis
svg.append('g')
.attr('id', name + '-x-axis')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
// apply class & id to x-axis texts
d3.select('#' + name + '-x-axis')
.selectAll('text')
.attr('class', function(d, i) {
return 'axis-text';
})
.attr('id', function(d, i) {
return name + '-axis-text-' + i;
});
// draw y axis
svg.append('g')
.attr('class', 'y axis')
.call(yAxis)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end');
// remove 0 in y axis
svg.select('.y')
.selectAll('.tick')
.filter(function(d) {
return d === 0 || d % 1 !== 0;
}).remove();
svg
.select('.horizontal')
.selectAll('.tick')
.filter(function(d) {
return d === 0 || d % 1 !== 0;
}).remove();
.d3-setup-barchart {
background-color: #666666;
}
.d3-setup-barchart .axis path {
fill: none;
stroke: #000;
}
.d3-setup-barchart .bar {
fill: #ccc;
}
.d3-setup-barchart .bar:hover {
fill: orange;
cursor: pointer;
}
.d3-setup-barchart .bar.selected {
fill: orange;
stroke: #fff;
stroke-width: 2;
}
.d3-setup-barchart .label-text {
text-anchor: middle;
font-size: 12px;
font-weight: bold;
fill: orange;
opacity: 0;
}
.d3-setup-barchart .label-text.selected {
opacity: 1;
}
.d3-setup-barchart .axis text {
fill: rgba(255, 255, 255, 0.6);
font-size: 9px;
}
.d3-setup-barchart .axis-text.selected {
fill: orange;
}
.d3-setup-barchart .y.axis path {
display: none;
}
.d3-setup-barchart .y.axis text {
font-size: 6px;
}
.d3-setup-barchart .x.axis path {
fill: none;
stroke: #353C41;
}
.d3-setup-barchart .grid .tick {
stroke: #fff;
opacity: .18 !important;
stroke-width: 0;
}
.d3-setup-barchart .grid .tick line {
stroke-width: .5 !important;
}
.d3-setup-barchart .grid path {
stroke-width: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div id="chart"></div>
PS: In your question, you said "there should be maximum 5 ticks and grid lines including the x-axis domain (0).". However, in your code, you are deliberately removing the 0 tick. If you want to see the 0 tick in the y axis, remove that block: https://jsfiddle.net/jz0q547u/

d3.js hybrid curved label/bar, pie chart

I am trying to recreate this chart design. With a dougnut chart, surrounded by curved data labels and an inside curved bar chart.
//starting demo
http://jsfiddle.net/NYEaX/1753/
//LATEST demo with inner bars//
http://jsfiddle.net/NYEaX/1761/
I've tried to map out the outer labels using a path, but the labels are not showing up correctly?
var labelRadius = (radius * 0.95);
var numBars = data.length;
// Labels
var labels = svg.append('g')
.classed('labels', true);
labels.append('def')
.append('path')
.attr('id', 'label-path')
.attr('d', 'm0 ' + -labelRadius + ' a' + labelRadius + ' ' + labelRadius + ' 0 1,1 -0.01 0');
labels.selectAll('text')
.data(data)
.enter()
.append('text')
.style('text-anchor', 'middle')
.append('textPath')
.attr('xlink:href', '#label-path')
.attr('startOffset', function(d, i) {
return i * 100 / numBars + 50 / numBars + '%';
})
.text(function(d) {
return d.group;
});
Live Demo:
http://jsfiddle.net/zu8m9ckd/8/
var $this = $("#progress");
var data = [{
"group": "Chinese",
"value": 22.6,
"children": [{
"growth": 1277.4
}]
}, {
"group": "Portuguese",
"value": 4.2,
"children": [{
"growth": 989.6
}]
}, {
"group": "Spanish",
"value": 7.8,
"children": [{
"growth": 743.2
}]
}, {
"group": "Rest",
"value": 17.8,
"children": [{
"growth": 588.5
}]
}, {
"group": "French",
"value": 3.0,
"children": [{
"growth": 398.2
}]
}, {
"group": "English",
"value": 27.3,
"children": [{
"growth": 281.2
}]
}, {
"group": "German",
"value": 3.8,
"children": [{
"growth": 173.1
}]
}, {
"group": "Japanese",
"value": 5.0,
"children": [{
"growth": 110.6
}]
}, {
"group": "Korean",
"value": 2.0,
"children": [{
"growth": 107.1
}]
}, {
"group": "Arabic",
"value": 3.3,
"children": [{
"growth": 2501.2
}]
}, {
"group": "Russian",
"value": 3.0,
"children": [{
"growth": 1825.8
}]
}];
var w = 500;
var h = w;
var radius = Math.min(w, h) / 2 - 50;
var svg = d3.select($this[0])
.append("svg")
.attr("width", w)
.attr("height", h)
.append("g")
svg.append("g")
.attr("class", "innerslices");
svg.append("g")
.attr("class", "slices");
svg.append("g")
.attr("class", "labels");
svg.append("g")
.attr("class", "labelsvals");
var pie = d3.layout.pie()
.sort(null)
.value(function (d) {
return d.value;
});
const outerRadius=0.85;
const innerRadius=0.75;
const earthRadius=0.05;
const arc = d3.svg.arc()
.outerRadius(radius * outerRadius)
.innerRadius(radius * innerRadius);
const outerArc = d3.svg.arc()
.innerRadius(radius - 20)
.outerRadius(radius - 20);
const innerArc = d3.svg.arc()
.innerRadius(radius - 55)
.outerRadius(radius - 55);
svg.attr("transform", "translate(" + w / 2 + "," + h / 2 + ")");
function colores_google(n) {
var colores_g = ["#e9168a", "#f8dd2f", "#448875", "#c3bd75", "#2b2d39", "#311854", "#553814", "#f7b363", "#89191d", "#c12f34", "#2b2a2c", "#c5b8a6", "#57585b"];
return colores_g[n % colores_g.length];
}
var totalsArray = [];
$.each(data, function (index, value) {
value["groupid"] = index;
var total = 0;
$.each(value.children, function (i, v) {
v["groupid"] = index;
total += v.growth;
});
value["total"] = total;
totalsArray.push(total);
});
var maxTotal = Math.max.apply(Math, totalsArray);
//slices
var slice = svg.select(".slices").selectAll("path.slice")
.data(pie(data))
slice.enter()
.insert("path")
.style("fill", function (d) {
return "#3b453f"; //colores_google(d.data.groupid);
})
.attr("class", "slice");
slice
.transition().duration(1000)
.attr("d", function (d) {
return arc(d);
})
slice.exit()
.remove();
//slices
//innerslices
var innerslice = svg.select(".innerslices").selectAll("path.innerslice")
.data(pie(data));
innerslice.enter()
.insert("path")
.style("fill", function (d) {
return "#8fdfff"; //colores_google(d.data.groupid);
})
.attr("class", "innerslice");
innerslice
.transition().duration(1000)
.attr("d", function (d) {
var arc3 = d3.svg.arc()
.outerRadius(radius * innerRadius)
.innerRadius(radius * (innerRadius-(innerRadius-earthRadius) * (d.data.children[0].growth / maxTotal)));
return arc3(d);
})
innerslice.exit()
.remove();
//innerslice
var pieData = pie(data);
var pieAngle = pieData.map(function (p) {
return (p.startAngle + p.endAngle) / 2 / Math.PI * 180;
});
const labels = svg.append('g')
.classed('labels', true);
//base on angle to change `text-anchor` and `transform(rotate)` to make the position of text correct
labels.selectAll('text')
.data(data)
.enter()
.append('text')
.style('text-anchor', function (d, i) { //important
const p = pieData[i];
const angle = pieAngle[i];
if (angle > 0 && angle <= 180) { //text-anchor depends on the angle
return "start"
}
return "end"
})
.attr("transform", function (d, i) { //important
const p = pieData[i];
let angle = pieAngle[i];
if (angle > 0 && angle <= 180) { //rotation depends on the angle
angle = angle - 180;
}
return `translate(${outerArc.centroid(p)}) rotate(${angle+90} 0 0) `
})
.text(function (d) {
return d.group;
});
//similar with outer black text
labels.selectAll('text.inner')
.data(data)
.enter()
.append('text')
.attr("class","inner")
.style('text-anchor', function (d, i) { //important
const p = pieData[i];
const angle = pieAngle[i];
if (angle > 0 && angle <= 180) { //text-anchor depends on the angle
return "end"
}
return "start"
})
.attr("transform", function (d, i) { //important
const p = pieData[i];
let angle = pieAngle[i];
if (angle > 0 && angle <= 180) { //rotation depends on the angle
angle = angle - 180;
}
return `translate(${innerArc.centroid(p)}) rotate(${angle+90} 0 0) `
})
.text(function (d) {
return d.children[0].growth+"%";
});
const labelFontSize = 10;
const labelValRadius = (radius * 0.80 - labelFontSize * 0.35); //calculate correct radius
const labelValRadius1 = (radius * 0.80 + labelFontSize * 0.35); //why 0.35? I don't know. Try to google it.
const labelsVals = svg.append('g')
.classed('labelsvals', true);
//define two paths to make the direction of labels correct
labelsVals.append('def')
.append('path')
.attr('id', 'label-path-1')
.attr('d', `m0 ${-labelValRadius} a${labelValRadius} ${labelValRadius} 0 1,1 -0.01 0`);
labelsVals.append('def')
.append('path')
.attr('id', 'label-path-2')
.attr('d', `m0 ${-labelValRadius1} a${labelValRadius1} ${labelValRadius1} 0 1,0 0.01 0`);
labelsVals.selectAll('text')
.data(data)
.enter()
.append('text')
.style('font-size', labelFontSize)
.style('font-weight', "bold")
.style('text-anchor', 'middle')
.append('textPath')
.attr('href', function (d, i) {
const p = pieData[i];
const angle = pieAngle[i];
if (angle > 90 && angle <= 270) { //based on angle to choose the path
return '#label-path-2';
} else {
return '#label-path-1';
}
})
.attr('startOffset', function (d, i) {
const p = pieData[i];
const angle = pieAngle[i];
let percent = (p.startAngle + p.endAngle) / 2 / 2 / Math.PI * 100;
if (angle > 90 && angle <= 270) { //calculate the correct percent for each path respectively
return 100 - percent + "%";
}
return percent + "%";
})
.text(function (d) {
if (d.value > 2) {//according to the simple image, the percent less than 3% should only show int part
return d.value.toFixed(1) + "%";
} else {
return d.value.toFixed(0) + "%";
}
});
body {
background: #eeeeee;
}
path {
stroke-width: 1px;
stroke: #eeeeee;
}
.small {
fill: steelblue;
}
.big {
stroke: #666;
fill: #ddd;
}
.small:hover {
stroke: steelblue;
fill: lightsteelblue;
}
.test {
padding: 30px
}
#progress {
position: relative;
margin-top: 20px
}
.progresschart {
background: white;
border-radius: 100px;
width: 100px;
height: 100px;
overflow: hidden;
border: 1px solid grey;
margin-top: 5px;
}
.bar {
fill: steelblue;
}
.bar:hover {
fill: brown;
}
.progresslabels {
position: absolute;
top: 0px;
left: 0;
}
.labelsvals {
fill: #ffffff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="progress"></div>
There are many parts in your code should be corrected. I choose two main parts to explain:
White Label:
//base on angle to change `text-anchor` and `transform(rotate)` to make the position of text correct
labels.selectAll('text')
.data(data)
.enter()
.append('text')
.style('text-anchor', function (d, i) { //important
const p = pieData[i];
const angle = pieAngle[i];
if (angle > 0 && angle <= 180) { //text-anchor depends on the angle
return "start"
}
return "end"
})
.attr("transform", function (d, i) { //important
const p = pieData[i];
let angle = pieAngle[i];
if (angle > 0 && angle <= 180) { //rotation depends on the angle
angle = angle - 180;
}
return `translate(${outerArc.centroid(p)}) rotate(${angle+90} 0 0) `
})
.text(function (d) {
return d.group;
});
Outer Black Label:
const labelFontSize = 10;
const labelValRadius = (radius * 0.80 - labelFontSize * 0.35); //calculate correct radius
const labelValRadius1 = (radius * 0.80 + labelFontSize * 0.35); //why 0.35? I don't know. Try to google it.
const labelsVals = svg.append('g')
.classed('labelsvals', true);
//define two paths to make the direction of labels correct
labelsVals.append('def')
.append('path')
.attr('id', 'label-path-1')
.attr('d', `m0 ${-labelValRadius} a${labelValRadius} ${labelValRadius} 0 1,1 -0.01 0`);
labelsVals.append('def')
.append('path')
.attr('id', 'label-path-2')
.attr('d', `m0 ${-labelValRadius1} a${labelValRadius1} ${labelValRadius1} 0 1,0 0.01 0`);
labelsVals.selectAll('text')
.data(data)
.enter()
.append('text')
.style('font-size', labelFontSize)
.style('font-weight', "bold")
.style('text-anchor', 'middle')
.append('textPath')
.attr('href', function (d, i) {
const p = pieData[i];
const angle = pieAngle[i];
if (angle > 90 && angle <= 270) { //based on angle to choose the path
return '#label-path-2';
} else {
return '#label-path-1';
}
})
.attr('startOffset', function (d, i) {
const p = pieData[i];
const angle = pieAngle[i];
let percent = (p.startAngle + p.endAngle) / 2 / 2 / Math.PI * 100;
if (angle > 90 && angle <= 270) { //calculate the correct percent for each path respectively
return 100 - percent + "%";
}
return percent + "%";
})
.text(function (d) {
if (d.value > 2) {//according to the simple image, the percent less than 3% should only show int part
return d.value.toFixed(1) + "%";
} else {
return d.value.toFixed(0) + "%";
}
});
Inner Black Label
//similar with outer black label
labels.selectAll('text.inner')
.data(data)
.enter()
.append('text')
.attr("class","inner")
.style('text-anchor', function (d, i) { //important
const p = pieData[i];
const angle = pieAngle[i];
if (angle > 0 && angle <= 180) { //text-anchor depends on the angle
return "end"
}
return "start"
})
.attr("transform", function (d, i) { //important
const p = pieData[i];
let angle = pieAngle[i];
if (angle > 0 && angle <= 180) { //rotation depends on the angle
angle = angle - 180;
}
return `translate(${innerArc.centroid(p)}) rotate(${angle+90} 0 0) `
})
.text(function (d) {
return d.children[0].growth+"%";
});
In this fiddle I've managed to get the doughnut ring, with the inner bars - and some labels.
But I need to get the labels in the correct orientations, colors, scales reflecting more accurately.
//latest code
http://jsfiddle.net/NYEaX/1761/
//innerslices
var arc2 = d3.svg.arc()
.outerRadius(radius * 0.75)
.innerRadius(radius * 0.25);
var innerslice = svg.select(".innerslices").selectAll("path.innerslice")
.data(pie(data));
innerslice.enter()
.insert("path")
.style("fill", function(d) {
return "#8fdfff";//colores_google(d.data.groupid);
})
.attr("class", "innerslice");
innerslice
.transition().duration(1000)
.attrTween("d", function(d) {
var arc3 = d3.svg.arc()
.outerRadius(radius * 0.75)
.innerRadius(radius * 0.25 * (d.data.children[0].growth / maxTotal));
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc3(interpolate(t));
};
})
innerslice.exit()
.remove();
//innerslice

Plot d3 chart on to Leaflet / Mapbox map

I have a radial d3 chart that I would like to plot / overlay on to a Leaflet / Mapbox map at lat,long co-ordinates.
I have looked around and the solutions all seem to be plotting circles onto the map using cx, cy positions, which is not what I need.
Can I add the SVG surrounding the chart to the map?
Below is the code and a link to the radial chart (which may or may not be of any use for this question as I just want to know how to plot an SVG on to the map)
CODE FOR D3 CHART
var width = 300;
var height = 300;
var twoPi = 2 * Math.PI; // Full circle
var formatPercent = d3.format(".0%");
var data = [{
"range": "0-20",
"count": 20
}, {
"range": "21-40",
"count": 10
}, {
"range": "41-60",
"count": 17
}, {
"range": "61-80",
"count": 49
}, {
"range": "81-100",
"count": 90
}];
var max = d3.max(data, function(d) {
return +d.count;
});
var percent = d3.max(data, function(d) {
return +d.count / 10;
});
var radiusBackground = .25;
var radiusForeground = .25;
var gap = 28;
var maxCount = max + percent;
var svg = d3.select(map.getPanes().overlayPane).append("svg")
.attr("width", 1000)
.attr("height", 1000)
.attr("transform", "translate(" + map.latLngToLayerPoint(latLng).x + "," + map.latLngToLayerPoint(latLng).y + ")");
var g = svg.append("g")
.attr("transform", "translate(" + width / 10 + "," + height / 10 + ")")
.attr("class", "leaflet-zoom-hide");
var background = g.selectAll(".twoPi")
.data(data).enter()
.append("path")
.attr("class", "twoPi")
.each(full);
var foreground = g.selectAll(".outerPath")
.data(data).enter()
.append("path")
.attr("class", "outerPath")
.each(drawArc);
map.on("viewreset", update);
update();
function update() {
foreground.attr("transform",
function(d) {
return "translate("+
map.latLngToLayerPoint(latLng).x/10000 +","+
map.latLngToLayerPoint(latLng).y/10000 +")";
}
)
background.attr("transform",
function(d) {
return "translate("+
map.latLngToLayerPoint(latLng).x/10000 +","+
map.latLngToLayerPoint(latLng).y/10000 +")";
}
)
}
function full() {
var arc = d3.svg.arc()
.startAngle(0)
.endAngle(twoPi)
.innerRadius(0 + gap * radiusBackground)
.outerRadius(26 + gap * radiusBackground);
d3.select(this)
.attr("transform", "translate(" + (width / 2.5) + "," + (height / 2.5) + ")")
.attr("d", arc)
.style("opacity", "0.5");
radiusBackground++;
}
function drawArc(d, i) {
var arc = d3.svg.arc()
.startAngle(0)
.endAngle(twoPi * (d.count / maxCount))
.innerRadius(0 + gap * radiusForeground)
.outerRadius(26 + gap * radiusForeground);
d3.select(this)
.attr("transform", "translate(" + (width / 2.5) + "," + (height / 2.5) + ")")
.attr("d", arc)
.attr("id", "path" + i)
.on("mouseover", function() {
d3.select(this)
.style("fill", "red")
.style("cursor", "pointer");
})
.on("mouseout", function() {
d3.select(this)
.style("fill", "#8FAAE5");
})
radiusForeground++;
var text = g.append("text")
.attr("x", 12 + radiusForeground)
.attr("dy", 20)
.style("pointer-events", "none");
text.append("textPath")
.attr("fill", "white")
.attr("xlink:href", "#path" + i)
.text(d.count);
}
CODEPEN LINK TO D3 CHART
CODE FOR MAP (CURRENTLY WRONG I ASSUME)
L.mapbox.accessToken = 'ACCESS TOKEN HERE';
var map = L.mapbox.map('map', 'NAME HERE', {zoomControl: false}).setView([53.4054, -2.9858], 16);
var myLayer = L.mapbox.featureLayer().addTo(map);
var geojson = [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [53.4054, -2.9858]
},
"properties": {
"icon": {
"className": "sensor-icon",
//"html": "▼",
"iconSize": null
}
}
},
];
myLayer.on('layeradd', function(e) {
var marker = e.layer,
feature = marker.feature;
marker.setIcon(L.divIcon(feature.properties.icon));
});
myLayer.setGeoJSON(geojson);
new L.Control.Zoom({ position: 'bottomleft' }).addTo(map);
var latLng = new L.LatLng(geojson[0].geometry.coordinates[0], geojson[0].geometry.coordinates[1]);
Thank you in advance
You'll want to use http://leafletjs.com/reference.html#map-panes. Specifically, you'll do map.getPanes().overlayPane, which will return an element you can append your SVG into.

How to create a horizontal legend with d3.js

I've been trying to create a horizontal legend for my graph using d3.js. I've struggled to get the x-axis spacing correct with dynamic labels.
The problem is that the labels are not of a consistent width, here's a full example and this is my function to calculate the x position:
function legendXPosition(data, position, avgFontWidth){
if(position == 0){
return 0;
} else {
var xPostiion = 0;
for(i = 0; i < position; i++){
xPostiion += (data[i].length * avgFontWidth);
}
return xPostiion;
}
}
Does anyone have any suggestions on how to improve this?
I suggest referencing this question: SVG get text element width
Render the first legend entry as you already are. Store this entry, or assign ids such that you can look them up through selection.
When rendering subsequent entries, get the previous 'text' element and x offset. Compute the new legend entry offset using the exact width of the previous text element
var myNewXOffset = myPreviousXOffset + myPreviousText.getBBox().width
Modified the code to get the following result
The reason your labels are not of a consistent width is due to two factors. Both of them are related to your function to calculate the x position:
the avgFontWidth parameter
It should be smaller. You assigned it the value of 15 in your example, but the actual averageFontWidth is way smaller than 15, it was between 5 to 7. The extra space between your legend labels come from this inaccurate value of average font width.
Your avgFontWidth is just an average. In reality, fonts width change.For example, the r in the picture below is way narrower than the P.
Thus the five letter word rrrrr can be way shorter than PPPPP when the font family is set in some way. In conclusion, you shall not use avgFontWidth. You should use SVG get text element width as cmonkey suggests
the logic of xPostiion += (data[i].length * avgFontWidth)
The above formula intends to calculate the length of the text, but the width of the label shall also be added, xPostiion should be
xPostiion += (data[i].length * avgFontWidth) + widthOftheLabel
The modified code:
function drawlinegraph(data, startDateString, endDateString, maxValue, minValue, colour,
divID, labels) {
var m = {
top: 60,
right: 0,
bottom: 35,
left: 80
},
w = 770 - m.left - m.right,
h = 180 - m.top - m.bottom,
dateFormat = "%Y-%m-%d";
labels = ["ada", "adas", "asdasdasd", "sd"];
var parseDate = d3.time.format(dateFormat).parse;
var startDate = parseDate(startDateString);
var endDate = parseDate(endDateString);
// Define the x scale
var x = d3.time.scale()
.domain([startDate, endDate])
.range([0, w]);
// Format x-axis labels
x.tickFormat(d3.time.format(dateFormat));
// Define the y scale
var y = d3.scale.linear()
.domain([minValue, maxValue])
.range([h, 0]);
var graph = d3.select(divID).append("svg:svg")
.attr("width", w + m.right + m.left)
.attr("height", h + m.top + m.bottom)
.append("svg:g")
.attr("transform", "translate(" + m.left + "," + m.top + ")");
// create x-axis
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(4)
.tickSize(-h)
.tickFormat(d3.time.format("%Y/%m"));
// Add the x-axis.
graph.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
// Overide the default behaviour of d3 axis labels
d3.selectAll(".x.axis g text")[0].forEach(function(e) {
e.attributes[0].value = 8; // y
});
// create y-axis
var yAxisLeft = d3.svg.axis()
.scale(y)
.ticks(12)
.orient("left")
.tickSize(-w);
// Add the y-axis to the left
graph.append("svg:g")
.attr("class", "y axis")
.attr("transform", "translate(0,0)")
.call(yAxisLeft);
var i = 0;
$.each(data, function(key, value) {
value.forEach(function(d) {
d.date = parseDate(d.date);
});
var line = d3.svg.line()
.x(function(d) {
return x(d.date);
})
.y(function(d) {
return y(d.value);
});
graph.append("path")
.datum(value)
.attr("d", line)
.attr("class", "line")
.attr("stroke", colour[i]);
i++;
});
var legend = graph.append("g")
.attr("class", "legend")
.attr("height", 100)
.attr("width", 100)
.attr('transform', 'translate(-5,' + (h + 35) + ')');
legend.selectAll('rect')
.data(labels)
.enter()
.append("rect")
.attr("x", function(d, i) {
var xPost = legendXPosition(labels, i, 6);
return xPost;
})
.attr("y", -6)
.attr("width", 20)
.attr("height", 5)
.style("fill", function(d, i) {
var color = colour[i];
return color;
});
legend.selectAll('text')
.data(labels)
.enter()
.append("text")
.attr("x", function(d, i) {
var xPost = legendXPositionText(labels, i, 22, 6);
return xPost;
})
.attr("y", -1)
.text(function(d) {
return d;
});
};
function legendXPositionText(data, position, textOffset, avgFontWidth) {
return legendXPosition(data, position, avgFontWidth) + textOffset;
}
function legendXPosition(data, position, avgFontWidth) {
if (position == 0) {
return 0;
} else {
var xPostiion = 0;
for (i = 0; i < position; i++) {
xPostiion += (data[i].length * avgFontWidth + 40);
}
return xPostiion;
}
}
var benchmark_line_graph_colours = ["#524364", "#937ab1", "#ab5b02", "#faa757"],
benchmark_line_graph_data = {
"Beassa ALBI TR ZAR": [{
"date": "2012-08-31",
"value": 101.1
}, {
"date": "2012-09-28",
"value": 101.89
}, {
"date": "2012-10-31",
"value": 101.09
}],
"FTSE/JSE All Share TR ZAR": [{
"date": "2012-08-31",
"value": 99.72
}, {
"date": "2012-09-28",
"value": 101.24
}, {
"date": "2012-10-31",
"value": 105.29
}],
"STeFI Composite ZAR": [{
"date": "2012-08-31",
"value": 100.23
}, {
"date": "2012-09-28",
"value": 100.52
}, {
"date": "2012-10-31",
"value": 100.77
}],
"portfolio": [{
"date": "2012-08-31",
"value": 101.55
}, {
"date": "2012-09-28",
"value": 101.15
}, {
"date": "2012-10-31",
"value": 102.08
}]
};
drawlinegraph(benchmark_line_graph_data,
"2012-08-31",
"2012-10-31",
105.84700000000001,
99.163, benchmark_line_graph_colours,
"body");
body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis text {
padding-top: 5px;
text-anchor: "right";
}
.line {
fill: none;
stroke-width: 1.5px;
}
.y.axis line,
.y.axis path {
stroke-dasharray: 2, 2;
}
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<script src="http://d3js.org/d3.v3.js"></script>

Categories

Resources