i'm on the project for finishing my degree on computation, and i have a problem with D3, basically it works if I pass the data directly to the code, but with the data on a file, it says "n does not exist". I don't know why is this happening, here's my code:
PD: If anyone needs a sample of my data file, just ask for it.
Thanks in advance!
<code>
<!DOCTYPE html>
<meta charset="utf-8">
<title>Causa básica</title>
<style>
.axis path, .axis line
{
fill: none;
stroke: #777;
shape-rendering: crispEdges;
}
.axis text
{
font-family: 'Arial';
font-size: 13px;
}
.tick
{
stroke-dasharray: 1, 2;
}
.bar
{
fill: FireBrick;
}
</style>
<svg id="visualisation" width="1000" height="500"></svg>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
InitChart();
function InitChart() {
/*
var lineData = [{ //don't know if this is needed in source: <script src="http://d3js.org/d3.csv.js">
'x': 1, //, This is the sample data
'y': 1.0807955e-01
}, {
'x': 2,
'y': 1.2815365e-01
}, {
'x': 3,
'y': 9.3269178e-02
}, {
'x': 4,
'y': 9.3894191e-02
}];*/
var lineData;
d3.tsv("data.tsv", function(data) {
lineData=data //my data, that doesn't work
});
var vis = d3.select("#visualisation"),
WIDTH = 1000,
HEIGHT = 500,
MARGINS = {
top: 20,
right: 20,
bottom: 20,
left: 50
},
xRange = d3.scale.linear().range([MARGINS.left, WIDTH - MARGINS.right]).domain([d3.min(lineData, function (d) {
return d.x;
}),
d3.max(lineData, function (d) {
return d.x;
})
]),
yRange = d3.scale.linear().range([HEIGHT - MARGINS.top, MARGINS.bottom]).domain([d3.min(lineData, function (d) {
return d.y;
}),
d3.max(lineData, function (d) {
return d.y;
})
]),
xAxis = d3.svg.axis()
.scale(xRange)
.tickSize(5)
.tickSubdivide(true),
yAxis = d3.svg.axis()
.scale(yRange)
.tickSize(5)
.orient("left")
.tickSubdivide(true);
vis.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (HEIGHT - MARGINS.bottom) + ")")
.call(xAxis);
vis.append("svg:g")
.attr("class", "y axis")
.attr("transform", "translate(" + (MARGINS.left) + ",0)")
.call(yAxis);
var lineFunc = d3.svg.line()
.x(function (d) {
return xRange(d.x);
})
.y(function (d) {
return yRange(d.y);
})
.interpolate('basis');
vis.append("svg:path")
.attr("d", lineFunc(lineData))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none");
}
</script>
</code>
Asynchronous issue.
the function
d3.tsv("data.tsv", function(data) {
lineData=data //my data, that doesn't work
});
is called after the execution of the rest of code.
You can try to move all the code inside the function and after lineData=data;. Or build a function:
var lineData;
d3.tsv("data.tsv", function(data) {
seeData(data);
});
function seeData(lineData) {
var vis = d3.select("#visualisation"),
WIDTH = 1000,
HEIGHT = 500,
MARGINS = {
top: 20,
right: 20,
bottom: 20,
left: 50
},
// etc etc
}
Related
I am working with D3 voronoi. I have several lines and would like to have only one of them have a specific color, while all the others have a different color. I have tried using an if statement, but to no avail. Below is my code:
Style:
.axis--y path {
display: none;
fill: none;
stroke: #17BED5;
stroke-width: 1.0;
}
.axis--y line {
stroke: lightgrey;
opacity: 1
}
.cities {
fill: none;
stroke: #000000;
stroke-linejoin: round;
stroke-linecap: round;
stroke-width: 1px;
}
.city--hover {
stroke: #4575b4;
stroke-width: 2px;
}
.focus text {
text-anchor: middle;
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
}
.voronoi path {
fill: none;
pointer-events: all;
}
.voronoi--show path {
stroke: red;
stroke-opacity: 0.2;
}
#form {
position: absolute;
top: 20px;
right: 30px;
}
</style>
JavaScript:
var years,
yearKeys,
yearParse = d3.timeParse("%Y");
//Sets out the margins of the chart frm the edges of the page/screen.
var svg = d3.select("svg"),
margin = {top: 20, right: 30, bottom: 30, left: 40},
width = svg.attr("width") - margin.left - margin.right,
height = svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//Now we define the x and y axes and their respective scales. X will be the date, and y will be the unemployment rates.
var x = d3.scaleTime()
.range([0, width]);
var y = d3.scaleLinear()
.range([height, 0]);
var voronoi = d3.voronoi()
.x(function(d) {
return x(d.date); })
.y(function(d) {
return y(d.value); })
.extent([[-margin.left, -margin.top], [width + margin.right, height + margin.bottom]]);
//And we set out the values of our lines.
var line = d3.line()
.x(function(d) {
return x(d.date); })
.y(function(d) {
return y(d.value); });
//Now we call our chart.
d3.csv("data/eloVoronoi.csv", type, function(error, data) {
if (error) throw error;
x.domain(d3.extent(years));
y.domain([1500, d3.max(data, function(c) {
return d3.max(c.values, function(d) {
return d.value; }); })]).nice();
//We tell the computer that we want our x-axis to be at the bottom.
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
//And the y axis on the left, with some style attributes.
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y)
.ticks(6)
.tickSize(-width))
.append("text")
.attr("transform", "rotate(360)")
.attr("x", 4)
.attr("y", -14)
.attr("dy", "1em")
.style("text-anchor", "start")
.style("fill", "black")
.style("font-weight", "bold")
.text("Elo Rating");
g.append("g")
.attr("class", "cities")
.selectAll("path")
.data(data)
.enter().append("path")
.attr("d", function(d) { d.line = this;
return line(d.values); });
var focus = g.append("g")
.attr("transform", "translate(-100,-100)")
.attr("class", "focus");
focus.append("circle")
.attr("r", 3.5);
focus.append("text")
.attr("y", -10);
//This is where we group all the vornoi together so tht we can apply the mouseovers all together, instead of one by one.
var voronoiGroup = g.append("g")
.attr("class", "voronoi");
//The mouseovers/outs.
voronoiGroup.selectAll("path")
.data(voronoi.polygons(d3.merge(data.map(function(d) {
return d.values; }))))
.enter().append("path")
.attr("d", function(d) {
return d ? "M" + d.join("L") + "Z" : null; })
.on("mouseover", mouseover)
.on("mouseout", mouseout);
//
d3.select("#show-voronoi")
.property("disabled", false)
.on("change", function() { voronoiGroup.classed("voronoi--show", this.checked); });
//Helper function to assist in the mouseover.
function mouseover(d) {
d3.select(d.data.city.line).classed("city--hover", true);
d.data.city.line.parentNode.appendChild(d.data.city.line);
focus.attr("transform", "translate(" + x(d.data.date) + "," + y(d.data.value) + ")");
focus.select("text").text(d.data.city.name);
}
//And a helper function for the mouseout.
function mouseout(d) {
d3.select(d.data.city.line).classed("city--hover", false);
focus.attr("transform", "translate(-100,-100)");
}
});
//Now we use an if statement to call our chart onto the page.
function type(d, i, columns) {
if (!years) yearKeys = columns.slice(1), years = yearKeys.map(yearParse);
var c = {name: d.name.replace(/ (msa|necta div|met necta|met div)$/i, ""), values: null};
c.values = yearKeys.map(function(k, i) {
return {city: c, date: years[i], value: d[k] / 1}; });
return c;
}
I am having following code for line charts.
Here I'm showing two line charts in different <div> ids, I have added hover option for the line chart.
<html>
<head>
<script src="http://d3js.org/d3.v3.js"></script>
<style>
.x.axis path {
display: none;
}
.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
.overlay {
fill: none;
pointer-events: all;
}
.focus circle {
fill: none;
stroke: steelblue;
}
.axis path,
.axis line {
fill: none;
stroke: grey;
stroke-width: 2;
shape-rendering: crispEdges;
}
.grid .tick {
stroke: lightgrey;
stroke-opacity: 0.7;
shape-rendering: crispEdges;
}
.grid path {
stroke-width: 0;
}
</style>
</head>
<body>
<div id="chart1"></div>
<div id="chart2"></div>
<script>
var data = [{
x: 'w1',
y: 5
}, {
x: 'w2',
y: 28
}, {
x: 'w3',
y: 58
}, {
x: 'w4',
y: 88
}, {
x: 'w5',
y: 8
}, {
x: 'w6',
y: 48
}, {
x: 'w7',
y: 28
}, {
x: 'w8',
y: 68
}, {
x: 'w9',
y: 8
}];
div_id ="#chart1";
div_id1="#chart2";
draw_line(data,div_id);
draw_line(data,div_id1);
function draw_line(data,div_id)
{
width = 600,
height = 260,
margin = {
top: 30,
right: 20,
bottom: 30,
left: 50
};
width -= margin.left - margin.right;
height -= margin.top - margin.bottom;
// var parseDate = d3.time.format("%d-%b-%y").parse;
// var x = d3.time.scale().range([0, width]);
var x = d3.scale.ordinal().rangePoints([0, width]);
var y = d3.scale.linear().range([height, 0]);
var xAxis = d3.svg.axis().scale(x)
.orient("bottom")
// .ticks(0).tickSize(0)
// .tickFormat("").outerTickSize(0);
var yAxis = d3.svg.axis().scale(y)
.orient("left").tickSize(0).ticks(0)
.tickFormat("");
var svg = d3.select(div_id)
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("class", "bg-color")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// function for the y grid lines
function make_y_axis() {
return d3.svg.axis()
.scale(y)
.orient("left")
.ticks(10);
}
// Draw the y Grid lines
svg.append("g")
.attr("class", "grid")
.call(make_y_axis()
.tickSize(-width, 0, 0)
.tickFormat("")
);
// x.domain(d3.extent(data, function(d) {
// return d.x;
// }));
x.domain(data.map(function(d) { return d.x; }));
y.domain([0, d3.max(data, function(d) {
return d.y;
})]);
data.sort(function(a, b) {
return a.x - b.x;
});
/*x.domain([parseDate(data[0].x), parseDate(data[data.length - 1].x)]);
y.domain(d3.extent(data, function (d) {
return d.y;
}));*/
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
// Add the valueline path
svg.append("path")
.attr("class", "line");
// Define the line
var lineGen = d3.svg.line()
.x(function(d) {
return x(d.x);
})
.y(function(d) {
return y(d.y);
})
svg.append('path')
.attr("class", "line")
.attr('d', lineGen(data));
var focus = svg.append("g")
.attr("class", "focus")
.style("display", "none");
focus.append("circle")
.attr("r", 4.5);
focus.append("text")
.attr("x", 9)
.attr("dy", ".35em");
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.on("mouseover", function() {
focus.style("display", null);
})
.on("mouseout", function() {
focus.style("display", "none");
})
.on("mousemove", mousemove);
var bisectDate = d3.bisector(function(d) {
return d.x;
}).left;
function mousemove() {
var xPos = d3.mouse(this)[0];
var leftEdges = x.range();
var width = x.rangeBand();
var j;
for(j=0; xPos > (leftEdges[j] + width); j++) {}
var x0 = x.domain()[j];
i = bisectDate(data, x0, 1),
d0 = data[i - 1],
d1 = data[i],
d = x0 - d0.x > d1.x - x0 ? d1 : d0;
focus.attr("transform", "translate(" + x(d.x) + "," + y(d.y) + ")");
focus.select("text").text(d.x+":"+d.y);
}
}
</script>
</body>
</html>
I have to add mousehover option for both the chart at same...
Example:
If I hover first line chart means I have to show tooltip for both charts at same time and same position.
Hi all i am trying to add dates in the d3.js how can i achieve this i am new to d3.js when i try add integers instead of date in the X-axis it is working good ..
How to declare date and assign them
I am here by attaching the JS and HTML file
thanks in advance:)
InitChart();
function InitChart() {
/*var lineData = [{
'x': 1,
'y': 5
}, {
'x': 20,
'y': 20
}, {
'x': 40,
'y': 10
}, {
'x': 60,
'y': 40
}, {
'x': 80,
'y': 5
}, {
'x': 100,
'y': 60
}];*/
var lineData=[{"y": 0.8076999999999999, "x": "2016-01-08 03:01:19.418110"}, {"y": 0.692666666666667, "x": "2016-01-08 05:10:19.838509"}, {"y": 0.5674333333333333, "x": "2016-01-08 09:54:13.022163"}]
var vis = d3.select("#visualisation"),
WIDTH = 1000,
HEIGHT = 500,
MARGINS = {
top: 20,
right: 20,
bottom: 20,
left: 50
},
xRange = d3.scale.linear().range([MARGINS.left, WIDTH - MARGINS.right]).domain([d3.min(lineData, function (d) {
return d.x;
}),
d3.max(lineData, function (d) {
return d.x;
})
]),
yRange = d3.scale.linear().range([HEIGHT - MARGINS.top, MARGINS.bottom]).domain([d3.min(lineData, function (d) {
return d.y;
}),
d3.max(lineData, function (d) {
return d.y;
})
]),
xAxis = d3.svg.axis()
.scale(xRange)
.tickSize(5)
.tickSubdivide(true),
yAxis = d3.svg.axis()
.scale(yRange)
.tickSize(5)
.orient("left")
.tickSubdivide(true);
vis.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (HEIGHT - MARGINS.bottom) + ")")
.call(xAxis);
vis.append("svg:g")
.attr("class", "y axis")
.attr("transform", "translate(" + (MARGINS.left) + ",0)")
.call(yAxis);
var lineFunc = d3.svg.line()
.x(function (d) {
return xRange(d.x);
})
.y(function (d) {
return yRange(d.y);
})
.interpolate('linear');
vis.append("svg:path")
.attr("d", lineFunc(lineData))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none");
}
and here is the HTML
enter code here
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<title> D3 trial </title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<svg id="visualisation" width="400" height="500"></svg>
<script src='http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'> </script>
<script src='http://d3js.org/d3.v3.min.js'></script>
<script src="js/index.js"></script>
</body>
</html>
Have a look here, the example uses TypeScripit, but the concepts are the same. You basically need to use d3.time.scale() instead of d3.scale.linear(). You also need to parse your dates. Updated xRange should look like:
xRange = d3.time.scale().range([MARGINS.left, WIDTH - MARGINS.right]).domain(d3.extent(lineData, function (d) {
return new Date(d.x);
}))
I used the extent function instead of d3.min and d3.max, which has the same effect but with shorter notation.
Please apologies if this seems like a duplicate D3 question. I've spent 2 days trying to figure out how to do this.
I'm trying to create a multi-line chart with the x-axis as an ordinal scale, and the y axis as a normal linear scale.
Data is loaded from external JSON:
[
{
"arbeitsgang":"A1",
"y":5,
"z":4
},
{
"arbeitsgang":"A2",
"y":6,
"z":11
},
{
"arbeitsgang":"A3",
"y":4,
"z":45
}
]
And here's where I've written for trying to create the chart:
<!DOCTYPE html>
<meta charset="utf-8">
<style> /* set the CSS */
body { font: 12px Arial;}
path {
stroke: steelblue;
stroke-width: 2;
fill: none;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
</style>
<body>
<!-- load the d3.js library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.js"> </script>
<script>
var margin = {
top: 20,
right: 20,
bottom: 20,
left: 50
},
width = 600 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
var x = d3.scale.ordinal().rangeRoundBands([0, width],0.1);
var y = d3.scale.linear().rangeRound([height, 0]);
var ordinalScale = d3.scale.ordinal();
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
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 line = d3.svg.line()
.x(function(d) { return x(d.arbeitsgang); })
.y(function(d) { return y(d.koordinaten); });
d3.json("Arbeitsgang.json", function(error, data) {
ordinalScale.domain(d3.keys(data[0]).filter(function(key) { return key !== "arbeitsgang"; }));
data.forEach(function(d) {
d.arbeitsgang = d.arbeitsgang;
d.y = +d.y;
d.z = +d.z;
});
var arbeitsgange = ordinalScale.domain().map(function(name) {
return {
name: name,
values: data.map(function (d) {
return {arbeitsgang: d.arbeitsgang, koordinaten: +d[name]};
})
}
});
x.domain(d3.extent(data, function(d) { return d.arbeitsgang; }));
y.domain([
d3.min(arbeitsgange, function(c) { return d3.min(c.values, function(v) { return v.koordinaten; }); }),
d3.max(arbeitsgange, function(c) { return d3.max(c.values, function(v) { return v.koordinaten; }); })
]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
var graphen = svg.selectAll(".graphen")
.data(arbeitsgange)
.enter().append("g")
.attr("class", "graphen");
graphen.forEach(function(d) {
console.log(d);
});
graphen.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values)});
});
</script>
</body>
So there seems to be 2 mistakes:
1. x-axis only got 2 ticks ( A1, A3) but A2 is missing
2. there is no graph visible.
With html dom profiler you can see that there are 2 graphs:
<path class="line" d="M26,254LNaN,247L278,260"/>
<path class="line" d="M26,260LNaN,216L278,0"/>
As you can see there is something with NaN inside. Maybe it is an error while parsing. I simply don't know.
Thanks for your help
While we are using 'rangeRoundBands' for x axis scale
var x = d3.scale.ordinal().rangeRoundBands([0, width],0.1); ,
we need to set domain with all the points,
I did like this
x.domain(['A1','A2','A3']/*d3.extent(data, function(d) { return d.arbeitsgang; })*/);
Go through with this link and the working fiddle.
I'm creating a bar chart where hovering over each rectangle will generate a tooltip. So I created a "title" attribute for each rectangle and this was working
var hist = this.svg.selectAll(".hist")
.data(displayData, function(d) { return d.time; });
hist.enter().append("rect")
.attr('title', function (d) {
return tooltipDateFmt(d.time) + ": " + d.value.toFixed(1) + " gallons"; })
.attr("class", "hist");
hist.transition().duration(400)
.attr("x", function (d) { return x(d.time); })
.attr("y", function (d) { return y(d.value); })
.attr("width", function (d) { return 2.5; })
.attr("height", function (d) { return height - y(d.value); });
hist.exit().remove();
$('svg .hist').tooltip({
'container': 'body',
'placement': 'top'
});
But I then realized the titles are not updating when I updated the rects by clicking on a button and bind a new set of data into them. So I put the title in the update phase
var hist = this.svg.selectAll(".hist")
.data(displayData, function(d) { return d.time; });
hist.enter().append("rect")
.attr("class", "hist");
hist.transition().duration(400)
.attr('title', function (d) {
return tooltipDateFmt(d.time) + ": " + d.value.toFixed(1) + " gallons"; })
.attr("x", function (d) { return x(d.time); })
.attr("y", function (d) { return y(d.value); })
.attr("width", function (d) { return 2.5; })
.attr("height", function (d) { return height - y(d.value); });
hist.exit().remove();
But this is not showing any tooltips. Can anyone tell me what I have been wrong with?
bootstrap tooltip inserts a div (with the original title) into the DOM for each tooltip and then hides/shows it on mouseover. Changing the title later does not change the inserted div.
Instead of setting a title, I would use the title option and return a dynamic title.
$('svg .hist').tooltip({
'container': 'body',
'placement': 'top',
'title': function(){
var d = d3.select(this).datum(); // get data bound to rect
return tooltipDateFmt(d.time) + ": " + d.value.toFixed(1) + " gallons"; });
}
});
Here's a working example:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link data-require="bootstrap-css#3.3.1" data-semver="3.3.1" rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" />
<script data-require="jquery#2.1.3" data-semver="2.1.3" src="http://code.jquery.com/jquery-2.1.3.min.js"></script>
<script data-require="bootstrap#3.3.2" data-semver="3.3.2" src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
<style>
body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.bar {
fill: steelblue;
}
.x.axis path {
display: none;
}
</style>
</head>
<body>
<button id="changeTips">Change Tips</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script>
var data = [
{
x: 1660,
y: 1
},{
x: 1670,
y: 2
},{
x: 1680,
y: 3
},{
x: 1690,
y: 4
}
];
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear()
.range([0,width])
.domain([1650, 1700]);
var y = d3.scale.linear()
.range([height, 0])
.domain([0,5]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
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 + ")");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
var barWidth = (width / xAxis.ticks()[0]);
var bars = svg.selectAll(".bar")
.data(data)
.enter().append("rect")
/*
.attr('title', function (d) {
return d.y;
})
*/
.attr("class","bar")
.attr("width", barWidth)
.attr("x", function(d) { return x(d.x) - (barWidth / 2); })
.attr("y", function(d) { return y(d.y); })
.attr("height", function(d) { return height - y(d.y); });
var showY = true;
$('svg .bar').tooltip({
'container': 'body',
'placement': 'top',
'title': function(){
if (showY)
return (d3.select(this).datum().y);
else
return (d3.select(this).datum().x);
}
});
d3.select('#changeTips').on('click',function(){
showY = !showY;
});
</script>
</body>
</html>
Using data-original-title worked for me:
Instead of .attr('title', ...), use .attr('data-original-title', ...)
do it in both update and enter