Related
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>
I've the following d3 chart which is both grouped and each grouped contains a stacked bar. But somehow, I feel this is not a proper way to implement and little complicated. If there was only stacked bar chart, I would have used d3.stack(). Can someone let me know is there any better way to do this?
Snippet as follows:
var data = [
{
Category: "cat1",
type1: 300,
type2: 450,
type3: 120
},
{
Category: "cat2",
type1: 400,
type2: 100,
type3: 200
},
{
Category: "cat3",
type1: 400,
type2: 100,
type3: 200
},
{
Category: "cat4",
type1: 400,
type2: 100,
type3: 200
}
];
var margin = { top: 20, right: 20, bottom: 30, left: 40 },
width = 500 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var barWidth = 40;
var x0 = d3.scaleBand().range([0, width]);
var x1 = d3.scaleBand();
var y = d3.scaleLinear().range([height, 0]);
var xAxis = d3.axisBottom(x0);
var yAxis = d3.axisLeft(y).tickFormat(d3.format(".2s"));
var color = d3.scaleOrdinal().range(["#98abc5", "#8a89a6", "#7b6888"]);
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 yBegin;
var innerColumns = {
column1: ["type1", "type2"],
column2: ["type3"]
};
var columnHeaders = d3.keys(data[0]).filter(function(key) {
return key !== "Category";
});
color.domain(
d3.keys(data[0]).filter(function(key) {
return key !== "Category";
})
);
var groupData = data.forEach(function(d) {
var yColumn = new Array();
d.columnDetails = columnHeaders.map(function(name) {
for (ic in innerColumns) {
if (innerColumns[ic].indexOf(name) >= 0) {
if (!yColumn[ic]) {
yColumn[ic] = 0;
}
yBegin = yColumn[ic];
yColumn[ic] += +d[name];
return {
name: name,
column: ic,
yBegin: yBegin,
yEnd: +d[name] + yBegin
};
}
}
});
d.total = d3.max(d.columnDetails, function(d) {
return d.yEnd;
});
});
//console.log(data);
x0.domain(
data.map(function(d) {
return d.Category;
})
);
x1.domain(d3.keys(innerColumns)).range([0, x0.bandwidth()]);
y.domain([
0,
1.15 *
d3.max(data, function(d) {
return d.total;
})
]);
svg
.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg
.append("g")
.attr("class", "y axis")
.call(yAxis);
var stackedbar = svg
.selectAll(".stackedbar")
.data(data)
.enter()
.append("g")
.attr("class", "g")
.attr("transform", function(d) {
return "translate(" + x0(d.Category) + ",0)";
});
stackedbar
.selectAll("rect")
.data(function(d) {
return d.columnDetails;
})
.enter()
.append("rect")
.attr("width", barWidth)
.attr("x", function(d) {
return x1(d.column) + (x1.bandwidth() - barWidth) / 2;
})
.attr("y", function(d) {
return y(d.yEnd);
})
.attr("height", function(d) {
return y(d.yBegin) - y(d.yEnd);
})
.style("fill", function(d) {
return color(d.name);
});
body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.bar {
fill: steelblue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.6.0/d3.min.js"></script>
I have tried to achieve grouped stacked bar chart using d3.stack(). Here are 2 important parts of my solution:
var datasets=[d3.stack().keys(['type1','type3'])(data),
d3.stack().keys(['type2'])(data)];
var num_groups=datasets.length;
Here I am using .keys() method of d3.stack to create datasets for each group. After this we can use the code for creating stacked bar chart like this:
d3.range(num_groups).forEach(function(gnum) {
svg.selectAll('g.group'+gnum)
.data(datasets[gnum])
.enter()
.append('g')
.attr('fill',accent)
.attr('class', 'group'+gnum)
.selectAll('rect')
.data(d=>d)
.enter()
.append('rect')
.attr('x',(d,i)=>xscale(xlabels[i])+(xscale.bandwidth()/num_groups)*gnum)
.attr('y',d=>yscale(d[1]))
.attr('width',xscale.bandwidth()/num_groups)
.attr('height',d=>yscale(d[0])-yscale(d[1]));
});
You can see the complete solution here: https://jsfiddle.net/Abhi9kt/28wzdrfk/4/
You might need to have for ... in loops to build that object in your innerColumns like this:
itemLookup= data[0],
category = d3.keys(itemLookup.category),
items = d3.keys(itemLookup.category[type1[0]]),
columnHeaders = [],
innerColumns = (function(){
var result = {};
for(var i = 0, ii = category.length; i<ii; i++){
var holder = [];
for(var j = 0, jj = items.length; j<jj; j++){
columnHeaders.push(items[j]+'-'+category[i]);
holder.push(items[j]+'-'+category[i]);
result[category[i]] = holder;
}
}
return result;
})()...
You can refer to this for reference: https://jsfiddle.net/kyroskoh/tmec32z0/
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/
I trying to display a d3 linechart. I've a problem - I cannot stop the date from repeating. How can I stop the date keep repeating? I only want to show two columns(17/12/2013 and 18/12/2013) based on the JSON data reflected below. Or what do I need to do so the first tickmark would show 17/12/2013 and the last one would show 18/12/2013?
[
{
"key": "Excited",
"values": [ [1387212490000, 0], [1387298890000 , 10] ]
},
{
"key": "Sad",
"values": [ [1387212490000, 20], [1387298890000 , 50] ]
},
{
"key": "Angry",
"values": [ [1387212490000, 30], [1387298890000 , 30] ]
},
{
"key": "Happy",
"values": [ [1387212490000, 40], [1387298890000 , 70] ]
}
]
Below is the JS script
$(document).ready(function() {
d3.json('sales.json', function(data) {
nv.addGraph(function() {
var chart = nv.models.lineChart().x(function(d) {
return d[0]
}).y(function(d) {
return d[1]
}).color(d3.scale.category10().range())
.useInteractiveGuideline(true);
chart.xAxis.tickFormat(function(d) {
return d3.time.format('%d/%m/%Y')(new Date(d))
});
//chart.xScale(d3.time.scale());
d3.select('#nvd3 svg').datum(data).transition().duration(500).call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
});
});
You didn't show enough code so it may be difficult to debug...
Anyway, try this and I'm working on an example to prove it...
chart.xAxis
.tickFormat(function(d) {
return d3.time.format('%d/%m/%Y')(new Date(d))
})
.ticks(d3.time.days, 1)
<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/1.11.1/jquery.min.js"></script>
var margin = {top: 20, right: 40, bottom: 30, left: 20},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom,
barWidth = Math.floor(width / 19) - 1;
var x = d3.scale.linear()
.range([barWidth / 2, width - barWidth / 2]);
var y = d3.scale.linear()
.range([height, 0]);
var yAxis = d3.svg.axis()
.scale(y)
.orient("right")
.tickSize(-width)
.tickFormat(function(d) { return Math.round(d / 1e6) + "M"; });
// An SVG element with a bottom-right origin.
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 + ")");
// A sliding container to hold the bars by birthyear.
var birthyears = svg.append("g")
.attr("class", "birthyears");
// A label for the current year.
var title = svg.append("text")
.attr("class", "title")
.attr("dy", ".71em")
.text(2000);
d3.csv("population.csv", function(error, data) {
// Convert strings to numbers.
data.forEach(function(d) {
d.people = +d.people;
d.year = +d.year;
d.age = +d.age;
});
// Compute the extent of the data set in age and years.
var age1 = d3.max(data, function(d) { return d.age; }),
year0 = d3.min(data, function(d) { return d.year; }),
year1 = d3.max(data, function(d) { return d.year; }),
year = year1;
// Update the scale domains.
x.domain([year1 - age1, year1]);
y.domain([0, d3.max(data, function(d) { return d.people; })]);
// Produce a map from year and birthyear to [male, female].
data = d3.nest()
.key(function(d) { return d.year; })
.key(function(d) { return d.year - d.age; })
.rollup(function(v) { return v.map(function(d) { return d.people; }); })
.map(data);
// Add an axis to show the population values.
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + width + ",0)")
.call(yAxis)
.selectAll("g")
.filter(function(value) { return !value; })
.classed("zero", true);
// Add labeled rects for each birthyear (so that no enter or exit is required).
var birthyear = birthyears.selectAll(".birthyear")
.data(d3.range(year0 - age1, year1 + 1, 5))
.enter().append("g")
.attr("class", "birthyear")
.attr("transform", function(birthyear) { return "translate(" + x(birthyear) + ",0)"; });
birthyear.selectAll("rect")
.data(function(birthyear) { return data[year][birthyear] || [0, 0]; })
.enter().append("rect")
.attr("x", -barWidth / 2)
.attr("width", barWidth)
.attr("y", y)
.attr("height", function(value) { return height - y(value); });
// Add labels to show birthyear.
birthyear.append("text")
.attr("y", height - 4)
.text(function(birthyear) { return birthyear; });
// Add labels to show age (separate; not animated).
svg.selectAll(".age")
.data(d3.range(0, age1 + 1, 5))
.enter().append("text")
.attr("class", "age")
.attr("x", function(age) { return x(year - age); })
.attr("y", height + 4)
.attr("dy", ".71em")
.text(function(age) { return age; });
// Allow the arrow keys to change the displayed year.
window.focus();
d3.select(window).on("keydown", function() {
switch (d3.event.keyCode) {
case 37: year = Math.max(year0, year - 10); break;
case 39: year = Math.min(year1, year + 10); break;
}
update();
});
function update() {
if (!(year in data)) return;
title.text(year);
birthyears.transition()
.duration(750)
.attr("transform", "translate(" + (x(year1) - x(year)) + ",0)");
birthyear.selectAll("rect")
.data(function(birthyear) { return data[year][birthyear] || [0, 0]; })
.transition()
.duration(750)
.attr("y", y)
.attr("height", function(value) { return height - y(value); });
}
});
svg {
font: 10px sans-serif;
}
.y.axis path {
display: none;
}
.y.axis line {
stroke: #fff;
stroke-opacity: .2;
shape-rendering: crispEdges;
}
.y.axis .zero line {
stroke: #000;
stroke-opacity: 1;
}
.title {
font: 300 78px Helvetica Neue;
fill: #666;
}
.birthyear,
.age {
text-anchor: middle;
}
.birthyear {
fill: #fff;
}
rect {
fill-opacity: .6;
fill: #e377c2;
}
rect:first-child {
fill: #1f77b4;
}
I've created a sample Asp.Net MVC 4 application where I've used D3.js to append an SVG element and then inside the SVG I've appended a text element (see code below). This all works fine until I try to append an img to the SVG using a local png file. The img gets appended to the DOM, but the img is not rendered on the page. Any ideas what I'm doing wrong here, and how to go about fixing it?
#{
ViewBag.Title = "Home Page";
}
<script src="~/Scripts/d3.v3.js"></script>
<script type="text/javascript">
var svg = d3.select("body")
.append("svg")
.attr("width", 200)
.attr("height", 100)
.style("border", "1px solid black");
var text = svg.selectAll("text")
.data([0])
.enter()
.append("text")
.text("Testing")
.attr("x", "40")
.attr("y", "60");
var imgs = svg.selectAll("img").data([0]);
imgs.enter()
.append("img")
.attr("xlink:href", "#Url.Content("~/Content/images/icons/refresh.png")")
.attr("x", "60")
.attr("y", "60")
.attr("width", "20")
.attr("height", "20");
</script>
#Richard Marr - Below is an attempt to do the same thing in straight HTML, which gives me the same result. I'm not sure about my code to get the refresh.png file from the local drive this way.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="http://d3js.org/d3.v2.js"></script>
</head>
<body>
<script type="text/javascript">
var svg = d3.select("body")
.append("svg")
.attr("width", 200)
.attr("height", 100)
.style("border", "1px solid black");
var text = svg.selectAll("text")
.data([0])
.enter()
.append("text")
.text("Testing")
.attr("x", "40")
.attr("y", "60");
var imgs = svg.selectAll("img").data([0]);
imgs.enter()
.append("svg:img")
.attr("xlink:href", "file:///D:/d3js_projects/refresh.png")
.attr("x", "60")
.attr("y", "60")
.attr("width", "20")
.attr("height", "20");
</script>
</body>
</html>
nodeEnter.append("svg:image")
.attr('x', -9)
.attr('y', -12)
.attr('width', 20)
.attr('height', 24)
.attr("xlink:href", "resources/images/check.png")
In SVG (contrasted with HTML), you will want to use <image> instead of <img> for elements.
Try changing your last block with:
var imgs = svg.selectAll("image").data([0]);
imgs.enter()
.append("svg:image")
...
My team also wanted to add images inside d3-drawn circles, and came up with the following (fiddle):
index.html:
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="timeline.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js"></script>
<script src="https://code.jquery.com/jquery-2.2.4.js"
integrity="sha256-iT6Q9iMJYuQiMWNd9lDyBUStIq/8PuOW33aOqmvFpqI="
crossorigin="anonymous"></script>
<script src="./timeline.js"></script>
</head>
<body>
<div class="timeline"></div>
</body>
</html>
timeline.css:
.axis path,
.axis line,
.tick line,
.line {
fill: none;
stroke: #000000;
stroke-width: 1px;
}
timeline.js:
// container target
var elem = ".timeline";
var props = {
width: 1000,
height: 600,
class: "timeline-point",
// margins
marginTop: 100,
marginRight: 40,
marginBottom: 100,
marginLeft: 60,
// data inputs
data: [
{
x: 10,
y: 20,
key: "a",
image: "https://unsplash.it/300/300",
id: "a"
},
{
x: 20,
y: 10,
key: "a",
image: "https://unsplash.it/300/300",
id: "b"
},
{
x: 60,
y: 30,
key: "a",
image: "https://unsplash.it/300/300",
id: "c"
},
{
x: 40,
y: 30,
key: "a",
image: "https://unsplash.it/300/300",
id: "d"
},
{
x: 50,
y: 70,
key: "a",
image: "https://unsplash.it/300/300",
id: "e"
},
{
x: 30,
y: 50,
key: "a",
image: "https://unsplash.it/300/300",
id: "f"
},
{
x: 50,
y: 60,
key: "a",
image: "https://unsplash.it/300/300",
id: "g"
}
],
// y label
yLabel: "Y label",
yLabelLength: 50,
// axis ticks
xTicks: 10,
yTicks: 10
}
// component start
var Timeline = {};
/***
*
* Create the svg canvas on which the chart will be rendered
*
***/
Timeline.create = function(elem, props) {
// build the chart foundation
var svg = d3.select(elem).append('svg')
.attr('width', props.width)
.attr('height', props.height);
var g = svg.append('g')
.attr('class', 'point-container')
.attr("transform",
"translate(" + props.marginLeft + "," + props.marginTop + ")");
var g = svg.append('g')
.attr('class', 'line-container')
.attr("transform",
"translate(" + props.marginLeft + "," + props.marginTop + ")");
var xAxis = g.append('g')
.attr("class", "x axis")
.attr("transform", "translate(0," + (props.height - props.marginTop - props.marginBottom) + ")");
var yAxis = g.append('g')
.attr("class", "y axis");
svg.append("text")
.attr("class", "y label")
.attr("text-anchor", "end")
.attr("y", 1)
.attr("x", 0 - ((props.height - props.yLabelLength)/2) )
.attr("dy", ".75em")
.attr("transform", "rotate(-90)")
.text(props.yLabel);
// add placeholders for the axes
this.update(elem, props);
};
/***
*
* Update the svg scales and lines given new data
*
***/
Timeline.update = function(elem, props) {
var self = this;
var domain = self.getDomain(props);
var scales = self.scales(elem, props, domain);
self.drawPoints(elem, props, scales);
};
/***
*
* Use the range of values in the x,y attributes
* of the incoming data to identify the plot domain
*
***/
Timeline.getDomain = function(props) {
var domain = {};
domain.x = props.xDomain || d3.extent(props.data, function(d) { return d.x; });
domain.y = props.yDomain || d3.extent(props.data, function(d) { return d.y; });
return domain;
};
/***
*
* Compute the chart scales
*
***/
Timeline.scales = function(elem, props, domain) {
if (!domain) {
return null;
}
var width = props.width - props.marginRight - props.marginLeft;
var height = props.height - props.marginTop - props.marginBottom;
var x = d3.scale.linear()
.range([0, width])
.domain(domain.x);
var y = d3.scale.linear()
.range([height, 0])
.domain(domain.y);
return {x: x, y: y};
};
/***
*
* Create the chart axes
*
***/
Timeline.axes = function(props, scales) {
var xAxis = d3.svg.axis()
.scale(scales.x)
.orient("bottom")
.ticks(props.xTicks)
.tickFormat(d3.format("d"));
var yAxis = d3.svg.axis()
.scale(scales.y)
.orient("left")
.ticks(props.yTicks);
return {
xAxis: xAxis,
yAxis: yAxis
}
};
/***
*
* Use the general update pattern to draw the points
*
***/
Timeline.drawPoints = function(elem, props, scales, prevScales, dispatcher) {
var g = d3.select(elem).selectAll('.point-container');
var color = d3.scale.category10();
// add images
var image = g.selectAll('.image')
.data(props.data)
image.enter()
.append("pattern")
.attr("id", function(d) {return d.id})
.attr("class", "svg-image")
.attr("x", "0")
.attr("y", "0")
.attr("height", "70px")
.attr("width", "70px")
.append("image")
.attr("x", "0")
.attr("y", "0")
.attr("height", "70px")
.attr("width", "70px")
.attr("xlink:href", function(d) {return d.image})
var point = g.selectAll('.point')
.data(props.data);
// enter
point.enter()
.append("circle")
.attr("class", "point")
.on('mouseover', function(d) {
d3.select(elem).selectAll(".point").classed("active", false);
d3.select(this).classed("active", true);
if (props.onMouseover) {
props.onMouseover(d)
};
})
.on('mouseout', function(d) {
if (props.onMouseout) {
props.onMouseout(d)
};
})
// enter and update
point.transition()
.duration(1000)
.attr("cx", function(d) {
return scales.x(d.x);
})
.attr("cy", function(d) {
return scales.y(d.y);
})
.attr("r", 30)
.style("stroke", function(d) {
if (props.pointStroke) {
return d.color = props.pointStroke;
} else {
return d.color = color(d.key);
}
})
.style("fill", function(d) {
if (d.image) {
return ("url(#" + d.id + ")");
}
if (props.pointFill) {
return d.color = props.pointFill;
} else {
return d.color = color(d.key);
}
});
// exit
point.exit()
.remove();
// update the axes
var axes = this.axes(props, scales);
d3.select(elem).selectAll('g.x.axis')
.transition()
.duration(1000)
.call(axes.xAxis);
d3.select(elem).selectAll('g.y.axis')
.transition()
.duration(1000)
.call(axes.yAxis);
};
$(document).ready(function() {
Timeline.create(elem, props);
})
I do not know why, but the image should not be duplicated, tripled, etc ... should remove the previous one and load it again but with another rotation data. This is my code:
data.csv
enter image description here
d3.csv("data/data.csv").then(function(data){
//console.log(data);
// Clean data
formattedData = data.map(function(id){
id.rot_1 = +id.rot_1;
id.trans_1 = +id.trans_1;
return id;
});
// First run of the visualization
update(formattedData[0]);})
$("#play-button")
.on("click", function(){
var button = $(this);
if (button.text() == "Play"){
button.text("Pause");
interval = setInterval(step, 1000);
}
else {
button.text("Play");
clearInterval(interval);
}})
function step(){
// At the end of our data, loop back
time = (time < 76) ? time+1 : 0
update(formattedData[time]); }
function update(data) {
// Standard transition time for the visualization
var t = d3.transition()
.duration(1000);
//console.log(d3.selectAll(data));
//console.log(data)
// original
var imgs1 = g.append("image") // en vez de g es svg
.attr("xlink:href", "img/picturetest.png");
// EXIT old elements not present in new data.
imgs1.exit()
.attr("class", "exit")
.selectAll("svg:image")
.remove();
//console.log(data)
// ENTER new elements present in new data.
imgs1.enter()
.append("svg:image") // svg:image
//.attr("xlink:href", "img/picturetest.png")
.attr("class", "enter")
.merge(imgs1)
.transition(t)
.attr("x", 0) // 150
.attr("y", 0) // 80
.attr("width", 200)
.attr("height", 200)
.attr("transform", "rotate("+data.rot_1+") translate("+data.trans_1+")" ); }`
var svg = d3.select("body")
.append("svg")
.style("width", 200)
.style("height", 100)