I would like to be able to do something similair to what Mike Bostock does in his second example in the documentation for .textTween. I've managed to do a lot of work towards solving this, but I can't quite get it right. I'm completely new to JavaScript so perhaps thats the problem.
In the case of the observable notebook, the number oscilates between different random variables, which are assigned to the _current parameter for the next oscillation. How would I do this with only two numbers, which I would like to go back and forth between?
I tried working it into some code like this but to no avail -
var svg = d3.select("body")
.append("svg")
.attr("width", 960)
.attr("height", 500);
function textrepeat() {
var textrepeat = svg.append("text")
.attr("fill", "steelblue")
.attr("class", "txt")
.attr("x", 30)
.attr("y", 30)
repeat();
function repeat() {
textrepeat
.text("300")
.transition()
.duration(2000)
.tweening ??? //here is where to insert it?
.text("1000")
.transition()
.duration(2000)
.text("300")
.on("end", repeat);
};
};
textrepeat();
Thanks in advance
If I correctly understand what you want, all you need is two textTween functions. For instance, going from 300 to 1000 and back:
var svg = d3.select("body")
.append("svg");
function textrepeat() {
var textrepeat = svg.append("text")
.attr("fill", "steelblue")
.attr("x", 30)
.attr("y", 50);
repeat();
function repeat() {
textrepeat.transition()
.duration(2000)
.textTween(function() {
return function(t) {
return ~~d3.interpolate(300, 1001)(t)
};
})
.transition()
.duration(2000)
.textTween(function() {
return function(t) {
return ~~d3.interpolate(1001, 300)(t)
};
})
.on("end", repeat);
};
};
textrepeat();
text {
font-size: 46px;
font-weight: 700;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
PS: transition.textTween was added in D3 v5.14. If you're using a previous version, change it for .tween("text", function() { etc....
Related
I'm trying to create a dynamic visualization using circles that 'spread' out over some amount of time and all of the circles have the same centerpoint.
I have a separate script that creates the circles and stores the data in a JSON file - the first circle in the JSON file is the smallest circle on top of the image linked above.
Please see code snippet below. Basically, the script appends the circle data into circles into an svg with visibility set to none. The script reveal the circles one by one.
In the appending function, I tried using the .lower() function to reverse the order that the circles are appended to the svg because if I were to append in the order that the JSON file is in, each consecutive circle would hide the one below it. But then the animation plots backwards, where the larger circle plots first.
In the revealing function, I then tried adding a similar '.lower()' function to the transition method so each consecutive circle would reveal behind the previously revealed circle but then the code breaks. I'm just at a loss here - any pointers would be much appreciated.
html,
body,
#svg {
background-color: #FFFFFF;
}
<html>
<head>
<meta charset="utf-8" />
<title>Visualizer</title>
</head>
<body>
<div>
<button onclick="plotStatically(0, 0, 'testingcircle.json')">Draw Static ►</button>
<button onclick="plotConsecutively(0, 0, 'testingcircle.json')">Draw Dynamic ►</button>
</div>
<script src="https://d3js.org/d3.v5.min.js" charset="utf-8"></script>
<script>
function plotConsecutively(x, y, nameFile) {
d3.json(nameFile).then(function(data) {
var svgHeight = window.innerHeight - 100;
var svgWidth = window.innerWidth - 10;
var svg = d3.select('body').append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
svg.selectAll("circle")
.data(data)
.enter()
.append('circle')
.attr('r', function(d) {
return d.r;
})
.attr('cx', function(d) {
return d.cx + x;
})
.attr('cy', function(d) {
return d.cy + y;
})
.attr('fill', function(d) {
return d.fill;
})
.attr('visibility', 'hidden')
svg.selectAll("circle")
.transition()
.delay(function(d, i) {
return 3.4 * i;
})
.duration(10)
.attr('visibility', 'visible');
})
}
function plotStatically(x, y, nameFile) {
d3.json(nameFile).then(function(data) {
var svgHeight = window.innerHeight - 100;
var svgWidth = window.innerWidth - 10;
var svg = d3.select('body').append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
svg.selectAll("circle")
.data(data)
.enter()
.append('circle')
.attr('r', function(d) {
return d.r;
})
.attr('cx', function(d) {
return d.cx;
})
.attr('cy', function(d) {
return d.cy;
})
.attr('fill', function(d) {
return d.fill;
});
})
}
</script>
</body>
</html>
I think you were pretty much there.
As you said, the larger circles need to be appended to the svg first so that they don't block out the smaller circles beneath them. I think this is most easily done simply by reversing the order of the data array just after you get the results of the json file:
d3.json(nameFile).then(function(data) {
data = data.reverse();
...
Then, in order to show the circles from the inside out, you can change your delay function so that the items at the end of the array that you want to show first (the smaller circles) have the smallest delay, and the items at the beginning of the array that you want to show last (the larger circles) have the largest delay.
The third argument to the delay() function is the NodesList containing all the selected DOM elements, so you can use the length property of that array in your calculations.
...
.delay(function(d, i, circleNodes) {
return 3.4 * ((circleNodes.length - 1) - i);
})
...
let data = [
{"r":5,"cx":100,"cy":100,"fill":"red"}, {"r":10,"cx":100,"cy":100,"fill":"magenta"},{"r":15,"cx":100,"cy":100,"fill":"orange"},{"r":20,"cx":100,"cy":100,"fill":"green"},{"r":25,"cx":100,"cy":100,"fill":"blue"}
];
data = data.reverse();
function plotConsecutively() {
var svg = d3.select('#svg')
.append('svg')
.attr('width', 200)
.attr('height', 200);
svg.selectAll("circle")
.data(data)
.enter()
.append('circle')
.attr('r', function(d) {
return d.r;
})
.attr('cx', function(d) {
return d.cx;
})
.attr('cy', function(d) {
return d.cy;
})
.attr('fill', function(d) {
return d.fill;
})
.attr('visibility', 'hidden')
svg.selectAll('circle')
.transition()
.delay(function(d, i, nodes) {
return 150 * ((nodes.length - 1) - i);
})
.duration(10)
.attr('visibility', 'visible');
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button onclick="plotConsecutively()">Draw Dynamic ►</button>
<div id="svg"></div>
The following is my code. Yet when I run it I get a blank page. Why is this the case ? Also how can I use data from hundreds of columns to make a simple interactive visual using d3 ? I would like to add that the following csv file "LoanStats3a.csv" is in the same folder.
<html>
<title>Loans</title>
<link href="../css/jquery-ui.css" rel="stylesheet" />
<script src="../Scripts/jquery-1.12.4"></script>
<script src="../Scripts/jquery-1.12.4.js"></script>
<script src="../Scripts/jquery-ui.js"></script>
<script src="http://d3js.org/d3.v3.js" charset="utf-8"></script>
<style>
#LoanStats3a{
color: blueviolet;
}
</style>
<body>
<script>
d3.csv("LoanStats3a", function (file1){
var bg = d3.select("body").append("svg")
.attr("width", 5000)
.attr("height", 5000);
bg.selectAll("rect")
.data(file1)
.enter()
.attr("width", function(d){return d.loan_amnt / 100;})
.attr("height", function(d) {return d.term;})
.attr("y", function (d,i) {return i *50;})
.attr("fill", function (d){"red","blue";});
}
</script>
</body>
This is because after binding the data to your empty selection, you have to append a rect element for each data.
Also, your attribute "fill" is incorrect.
bg.selectAll("rect")
.data(file1)
.enter()
.append("rect") // <= You need to create a rect for each data
.attr("width", function(d){return d.loan_amnt / 100;})
.attr("height", function(d) {return d.term;})
.attr("y", function (d,i) {return i *50;})
.attr("fill", "blue");
If you want to change the color depending on the data, create a function and return something.
// For example
.attr("fill", function(d){return d.loan_amnt > 25000 ? "blue" : "red"});
Here's a JsFiddle with random data : DEMO
EDIT : If it's still not working, it's probably a problem with your data because the only thing different between our code is that I used custom data in the JsFiddle.
I notice that your csv file doesn't have the extension .csv, it's just LoanStats3a ?
You should do a console.log(file1), to check if your data are correct.
Take a look at D3 CSV for how to load a csv file.
You are missing a closing ) at the end:
.attr("fill", function (d){"red","blue";});
}
// ^ Here should be a )
</script>
It helps if you have proper indent:
<script>
d3.csv("LoanStats3a", function(file1) {
var bg = d3.select("body").append("svg")
.attr("width", 5000)
.attr("height", 5000);
bg.selectAll("rect")
.data(file1)
.enter()
.attr("width", function(d) {
return d.loan_amnt / 100;
})
.attr("height", function(d) {
return d.term;
})
.attr("y", function(d, i) {
return i * 50;
})
.attr("fill", function(d) {
"red", "blue"; // What is going on here?
// Did you for to return?
// return "blue";
});
});
</script>
I am new to D3.js and am trying to build rectangles that represent all nodes from an XML file. So far so good but I want interactivity with each of the rectangles I draw and to be able to capture the nodes that have been touched for further processing. So let's say I click on a rectangle, I can make it react by doing an onclick event (like increasing the font size) but I can't seem to retrieve some of the info. I'd like to create an array with the text of each item that was clicked on.
Here's the code for one instance of the rectangle.
d3.select("#chart")
.append("svg")
.attr("width", 600)
.attr("height", 2000)
.style("background", "#93A1A1")
d3.select("svg")
.append("rect").attr("x", 50)
.attr("y", 25)
.attr("height", 20)
.attr("width", 200)
.attr("title", "resourceDef")
.style("fill", "#CB4B19")
d3.select("svg")
.append("text")
.attr("x", 55)
.attr("y", 37)
.attr("font-size", 11)
.attr("font-family", "sans-serif")
.text("resourceDef")
.on("mouseover", function(d) {
tempText = this.text;
alert(tempText);
d3.select(this)
.attr("font-size", 15)})
.on("mouseout", function(d) {
d3.select(this)
.attr("font-size", 11)})
I can grab style info by using but not the title and I can't find that info anywhere. Thanks for your help, I know it's a long question with probably a simple answer.
You can attach a mouse over event on the rectangle DOM by doing something like this:
d3.select("svg")
.append("rect").attr("x", 50)
.attr("y", 25)
.attr("height", 20)
.attr("width", 200)
.attr("title", "resourceDef")
.style("fill", "#CB4B19")
.on("click", function (d) {
var t = d3.select(this).attr("title");
//pushing the title into the array.
clickedTitles.push(t);
console.log(t);
});
You can get the attribute of a DOM(in your case tite) by doing something like this:
.on("click", function (d) {
var t = d3.select(this).attr("title");
clickedTitles.push(t);
console.log(t)
})
You can store the clicked rectangles title in an array like this:
//create an array
var clickedTitles = [];
//in your click function push the title into the array
clickedTitles.push(t);
//use the clickedTitles where ever you need in the code
Full code is here.
Inspired by Mike Bostock's Wealth of Nations, I'm trying to illustrate infection rates over time. I'm trying to group by Month and transition() a bubble along the x-axis (Month).
I'm stuck on grouping by Month...
I've edited this post significantly following helpful feedback from Lars and Christopher below.
A jsFiddle example here - hhttp://jsfiddle.net/Nyquist212/JSsHL/1/
<div id="chart"></div>
<script type="text/javascript">
var json =
[
{
"Month":1,
"VisitCount":894,
"DiagnosisName":"ACUTE PHARYNGITIS"
},
{
"Month":1,
"VisitCount":807,
"DiagnosisName":"PNEUMONIA ORGANISM NOS"
},
{
"Month":2,
"VisitCount":566,
"DiagnosisName":"ACUTE PHARYNGITIS"
},
{
"Month":2,
"VisitCount":456,
"DiagnosisName":"PNEUMONIA ORGANISM NOS"
},
{
"Month":3,
"VisitCount":273,
"DiagnosisName":"ACUTE PHARYNGITIS"
},
{
"Month":3,
"VisitCount":189,
"DiagnosisName":"PNEUMONIA ORGANISM NOS"
}
]
var svgContainer = d3.select("#chart")
.append("svg")
.attr("height", 250)
.attr("width",750);
var bubbleGroup = svgContainer.append("g");
var bubble = bubbleGroup.selectAll("circle")
.data(json)
.enter()
.append("circle");
var bubbleAttributes = bubble
.style("stroke", "blue")
.style("fill", "white")
.attr("r", function(d){return (d.VisitCount/10);})
.attr("cy", 150)
.attr("cx", function(d){return (d.Month * 100);});
d3.select("Body").selectAll("p")
.data(json)
.enter()
.append("p")
.text(function(d){return d.Month + " " + d.DiagnosisName + " " + d.VisitCount;})
</script>
EDIT: Updated with corrections from Christopher Chiche
EDIT: Updated with partially working example as suggested by Lars Kotthoff
I would use a combination of d3.nest and a transition loop for this. Best illustrated by an example:
svg.selectAll("circle")
.data(d3.nest()
.key(function(d) { return d.DiagnosisName; })
.entries(json))
.enter().append("circle")
.style("stroke", "blue")
.style("fill", "white")
.attr("cy", 150)
.attr("cx", 0)
.attr("r", 0)
.each(function(d) {
for(var i = 0; i < d.values.length; i++) {
d3.select(this).transition().delay(1000 * i).duration(1000)
.attr("r", function(d){return (d.values[i].VisitCount/10);})
.attr("cx", function(d){return (d.values[i].Month * 100);});
}
});
Complete jsfiddle here.
your problem is that dataset does not contain any data. It is a call to a d3 function that does not return anything. However, you have this csv variable that you pass as an argument to the drawChart function.
You should thus write:
var circleGroup = svgContainer.append("g")
.selectAll("circles")
.data(csv)
Same for every time you use 'dataset' in a data() call.
If you have no data, then d3 does not plot anything. So looking at the data you attach when you have this kind of problem helps most of the times.
Also, interpolateData won't work for the same reason, you should probably pass data as an argument.
I've brought some of my code to match D3 standards. But now I'm having issues with my update function working. I'm trying to follow the General Update Pattern.
When I run the function, I get an error in the console: "TypeError: gbars.enter(...).attr is not a function"
function updateBars()
{
xScale.domain(d3.range(dataset.length)).rangeRoundBands([0, w], 0.05);
yScale.domain([0, d3.max(dataset, function(d) { return (d.local > d.global) ? d.local : d.global;})]);
var gbars = svg.selectAll("rect.global")
.data(dataset);
gbars.enter()
.attr("x", w)
.attr("y", function(d) {return h - yScale(d.global); })
.attr("width", xScale.rangeBand())
.attr("height", function(d) {return yScale(d.global);});
gbars.transition()
.duration(500)
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return h - yScale(d.global);
})
.attr("width", xScale.rangeBand())
.attr("height", function(d) {
return yScale(d.global);
});
gbars.exit()
.transition()
.duration(500)
.attr("x", -xScale.rangeBand())
.remove();
}
I understand, I probably have a lot of errors with the update function in general, but it's hard to troubleshoot when hung-up at the beginning.
My goal for the update will be to remove/add series from the chart. They can choose to display Global, Local, or both (default). I'd actually prefer that when they hover over the legend it shows only that series. On mouseout, it would go back to default.
Working Fiddle here.
You need to say what you want to do for new nodes. Yes it's almost always append but you still have to tell it:
gbars.enter()
.append("rect")
.attr("class", "global")
.attr("x", w)
Other than that error, your structure for general update pattern looks correct on the surface.