I've tried this all day long and haven't got it. I would like to display a string of text in some delaying fashion. For example, at first it displays "a" then waits for a second then display "ab", and then waits for a second then display "abc", so far so on ...
I use D3 to display, function slice to generate partial text string from the alphabet. I use either setTimeout or setInterval. None works. I appreciate some help. Here is my code:
<!DOCTYPE html>
<html>
<head>
<style>
text {
font: bold 48px monospace;
}
.enter {
fill: green;
}
.update {
fill: #333;
}
</style>
</head>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var alphabet = "abcdefghijklmnopqrstuvwxyz".split("");
var width = 1000,
height = 200;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(32," + (height / 2) + ")");
function update(data) {
var text = svg.selectAll("text").data(data);
text.attr("class", "update");
text.enter().append("text")
.attr("class", "enter")
.attr("x", function(d, i) { return i * 32; })
.attr("dy", ".35em");
text.text(function(d) { return d; });
text.exit().remove();
}
// Method 1 - NOT WORKING
update(alphabet.slice(0, 1));
setTimeout(function(){},3000)
update(alphabet.slice(0, 2));
setTimeout(function(){},3000)
update(alphabet.slice(0, 3));
// ...
/*/ Method 2 - NOT WORKING
var i = 1;
setInterval(function(i) {
update(alphabet.slice(0, i));
i++;
}, 1500);
*/
</script>
</body>
</html>
The update calls need to be in your setTimeout function, like:
setTimeout(function () {
update(alphabet.slice(0, 1));
}, 3000);
setTimeout is non-blocking; after the timer is up, it executes the function passed in as an argument.
Edit: You also probably want your code to be like this, removing the update function completely (maybe you have a reason for using many separate <text> elements?):
var label = svg.append("text");
var i = 1;
setInterval(function () {
label.text(alphabet.slice(0, i++).join(""));
}, 1500);
Related
Attached is a screen shot of what shows up with the following code. I am mapping the number of pizza restaurants each state has. I am having trouble adding the state labels and a tool tip that shows me the corresponding value for each state to the map. I would like to be able to hover on a state and then have a tool tip that tells me what state it is and the value for it. This is what I have done so far.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style type="text/css">
/* Legend Font Style */
body {
font: 11px sans-serif;
background-color: #ffffff;
}
/* Legend Position Style */
.legend {
position:absolute;
left:20px;
top:30px;
}
.axis text {
font: 10px sans-serif;
}
.axis line, .axis path {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<script type="text/javascript">
//Width and height of map
var width = 960;
var height = 500;
var lowColor = '#f9f9f9'
var highColor = '#bc2a66'
// D3 Projection
var projection = d3.geoAlbersUsa()
.translate([width / 2, height / 2]) // translate to center of screen
.scale([1000]); // scale things down so see entire US
// Define path generator
var path = d3.geoPath() // path generator that will convert GeoJSON to SVG paths
.projection(projection); // tell path generator to use albersUsa projection
//Create SVG element and append map to the SVG
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
// Load in my states data!
d3.csv("statesdata.csv", function(data) {
var dataArray = [];
for (var d = 0; d < data.length; d++) {
dataArray.push(parseFloat(data[d].value))
}
var minVal = d3.min(dataArray)
var maxVal = d3.max(dataArray)
var ramp = d3.scaleLinear().domain([minVal,maxVal]).range([lowColor,highColor])
// Load GeoJSON data and merge with states data
d3.json("us-states.json", function(json) {
// Loop through each state data value in the .csv file
for (var i = 0; i < data.length; i++) {
// Grab State Name
var dataState = data[i].state;
// Grab data value
var dataValue = data[i].value;
// Find the corresponding state inside the GeoJSON
for (var j = 0; j < json.features.length; j++) {
var jsonState = json.features[j].properties.name;
if (dataState == jsonState) {
// Copy the data value into the JSON
json.features[j].properties.value = dataValue;
// Stop looking through the JSON
break;
}
}
}
// Bind the data to the SVG and create one path per GeoJSON feature
svg.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.style("stroke", "#fff")
.style("stroke-width", "1")
.style("fill", function(d) { return ramp(d.properties.value) });
g.selectAll("text")
.data(json.features)
.enter()
.append("svg:text")
.text(function(d){
return d.properties.name;
})
.attr("x", function(d){
return path.centroid(d)[0];
})
.attr("y", function(d){
return path.centroid(d)[1];
})
.attr("text-anchor","middle")
.attr('font-size','6pt');
// add a legend
var w = 140, h = 300;
var key = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h)
.attr("class", "legend");
var legend = key.append("defs")
.append("svg:linearGradient")
.attr("id", "gradient")
.attr("x1", "100%")
.attr("y1", "0%")
.attr("x2", "100%")
.attr("y2", "100%")
.attr("spreadMethod", "pad");
legend.append("stop")
.attr("offset", "0%")
.attr("stop-color", highColor)
.attr("stop-opacity", 1);
legend.append("stop")
.attr("offset", "100%")
.attr("stop-color", lowColor)
.attr("stop-opacity", 1);
key.append("rect")
.attr("width", w - 100)
.attr("height", h)
.style("fill", "url(#gradient)")
.attr("transform", "translate(0,10)");
var y = d3.scaleLinear()
.range([h, 0])
.domain([minVal, maxVal]);
var yAxis = d3.axisRight(y);
key.append("g")
.attr("class", "y axis")
.attr("transform", "translate(41,10)")
.call(yAxis)
});
});
</script>
</body>
</html>
Thank you.
I have a simple D3 simulation that looks like this
When I click the 'remove 1 and 4' button I want to remove nodes 1 and 4 from the simulation. The result, however, is the following:
Visually it seems to have removed 3 and 4 (not 1 and 4).
My code is below. Any ideas what I need to do to make this work correctly?
I'm assuming I've fundamentally misunderstood how updating nodes works in d3. Any help appreciated.
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<!DOCTYPE html>
<meta charset="utf-8">
<script type="text/javascript" src="http://d3js.org/d3.v3.js"></script>
<body>
<button>remove 1 and 4</button>
<script>
var width = 400,
height = 400;
var nodes = [1, 2, 3, 4].map(function(x) { return { name: x}});
var force = d3.layout.force()
.size([width, height])
.nodes(nodes)
.linkDistance(30)
.charge(-500)
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var content = svg.append("g");
var nodesData = force.nodes(),
nodeElement = content.selectAll(".node");
function tick() {
nodeElement.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
}
d3.selectAll('button').on('click', function() {
//remove 1 and 4
nodesData = [
nodesData[1],
nodesData[2]
]
refresh();
});
var WIDTH = 100;
//
// this logic is slightly modified from http://bl.ocks.org/tgk/6068367
//
function refresh() {
force.nodes(nodesData)
nodeElement = nodeElement.data(nodesData);
var nodeGroup = nodeElement.enter()
.append('g')
.attr("class", "node");
// node text
nodeGroup.append("text")
.attr("class", "nodetext")
.attr("dx", WIDTH/2)
.attr("dy", "14px")
.text(function(n) { return 'node '+n.name })
nodeElement.exit().remove();
force.start();
}
refresh();
</script>
You can solve your problem by adding a "key" function to the .data call inside the refresh function: nodeElement = nodeElement.data(nodesData, function(d){ return d.name });.
The problem you saw is not specific to updating nodes. Ordinarily, selections work based off of index of the data array. So if first D3 had [a,b,c,d] and now it has [a,d], it's going to take the first two elements ([a,b]) unless you tell it the key that defines each item in the array. That's what the function above does.
For more see https://github.com/d3/d3-selection/blob/master/README.md#selection_data
I tried to create a zoom event with a function that redraws my svg. It was not running smoothly, and I would like to debounce it using the underscore library. i have imported underscore, but now if I call the redraw function, nothing happens.
This works:
let zoom = d3.zoom()
.scaleExtent(scaleExtent)
.on("zoom", redraw);
function redraw(){
console.log('test') //test
}
This doesn't:
let zoom = d3.zoom()
.scaleExtent(scaleExtent)
.on("zoom", _.debounce(redraw,200));
function redraw(){
console.log('test') // --no output--
}
Any thoughts on what i am doing wrong?
Since you have not posted the full code but here is how you can use underscore debounce and zoom.
I have put in comments in the code.
//save instance of transform
var transform;
var svg = d3.select("body")
.append("svg")
.attr("width", "100%")
.attr("height", "100%")
.call(d3.zoom().on("zoom", function() {
//save instance of transform, because when debouce is called
//d3.event will be null
transform = d3.event.transform;
//call the debounse function
lazyzoom();
}))
.append("g")
svg.append("circle")
.attr("cx", 100)
.attr("cy", 100)
.attr("r", 50)
.style("fill", "red")
var lazyzoom = _.debounce(function() {
svg.attr("transform", transform)
}, 300);
body,
html {
width: 100%;
height: 100%;
margin: 0;
}
svg {
position: absolute;
top: 0;
left: 0;
}
p {
text-align: center;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore.js"></script>
Using d3.behavior.drag(), is there a way to enable a dragged image to be visible outside of its parent svg element borders.
In my app, I have a top layout based on an HTML grid (using flexBox) and several D3.js graphs located in each grid cell. Each graph is built with an SVG element and its childrens.
I need a drag and drop feature to enable copy/move of elements between these graphs. As now, the feature is working except that the drag image disappears when I cross the border of the source graph.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg {
float: left;
border-bottom: solid 1px #ccc;
border-right: solid 1px #ccc;
margin-right: -1px;
margin-bottom: -1px;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var width = 240,
height = 125,
radius = 20;
var overSVG;
var drag = d3.behavior.drag()
.origin(function (d) {
return d;
})
.on("drag", dragmove)
.on("dragend", dragend);
var svg = d3.select("body").append("div").selectAll("svg")
.data(d3.range(2).map(function (v, i) {
return {
svgElement: i,
x : width / 2,
y : height / 2
};
}))
.enter().append("svg")
.attr("width", width)
.attr("height", height)
.attr("id", function (d, i) {
return "svg_" + i
})
.on("mouseover", over)
.on("mouseout", out);
svg.append("circle")
.attr("r", radius)
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
})
.attr("svgElement", function (d, i) {
return i;
})
.call(drag);
function over(d, i) {
overSVG = d;
var selectedNodeId = "#svg_" + i;
d3.select(selectedNodeId)
.attr("fill", 'red');
}
function out(d, i) {
overSVG = "";
var selectedNodeId = "#svg_" + i;
d3.select(selectedNodeId)
.attr("fill", 'blue');
}
function dragmove(d) {
d3.select(this)
.attr("cx", d.x = d3.event.x)
.attr("cy", d.y = d3.event.y);
console.log("drag move", this, d3.event.x, ' ', d3.event.y);
}
function dragend(d) {
console.log("==>drag Ended :");
console.log(" dragged circle", this);
console.log(" from svg ", d);
console.log(" to", overSVG);
}
</script>
By default the svg elements have an overflow attribute set to hidden.
You can try setting the overflow attribute to visible
svg {
overflow: visible;
}
Again, without seeing a working example, it's hard to tell if this will work for you, but maybe it can help.
I have simple code here from Interactive Data Visualization by Scott Murray with minor changes. What I changed is the initial data's length 5 is different from the dataset1's length 25 in the click function.
However, every time I click and update, it does generate random new numbers but the length only shows 5 bars.
What is the reason for this? And how could I modify to change it?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script type="text/javascript" src="../d3/d3.v3.js"></script>
<title>D3 index01 </title>
<style type="text/css">
div.bar{
display: inline-block;
width: 20px;
height: 75px;
background-color: teal;
margin-right: 2px;
}
</style>
</head>
<body>
<p> click it </p>
<script type = "text/javascript">
var w=600;
var h = 250;
var dataset = [5,10,13,14,15];
var xScale = d3.scale.ordinal()
.domain(d3.range(dataset.length)).rangeRoundBands([0,w],0.05);
var yScale = d3.scale.linear()
.domain([0,d3.max(dataset)]).range([0,h]);
var svg = d3.select("body")
.append("svg")
.attr("width",w)
.attr("height",h);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d,i){
return xScale(i);
})
.attr("y",function(d){return (h-yScale(d));})
.attr("width",xScale.rangeBand())
.attr("height",function(d){return yScale(d);});
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d;
})
.attr("text-anchor", "middle")
.attr("x", function(d, i) {
return xScale(i)+xScale.rangeBand() / 2;
})
.attr("y", function(d) {
return h - yScale(d) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
d3.select("p")
.on("click",function(){
var dataset1 = [];
for(var i=0; i<25; i++){
var newNumber = Math.floor(Math.random()*25);
dataset1.push(newNumber);
}
svg.selectAll("rect").data(dataset1)
.transition()
.delay(function(d,i){
return i*100;
})
.duration(500)
.attr("x",function(d,i){
return xScale(i);
})
.attr("y",function(d){
return h-yScale(d);
})
.attr("x", function(d,i){
return xScale(i);
})
.attr("height",function(d){
return yScale(d);
})
.attr("fill",function(d){
return "rgb(0,0, " + (d*10) + ")";
});
svg.selectAll("text").data(dataset1).
text(function(d) {
return d;
})
.attr("x", function(d, i) {
return xScale(i) + xScale.rangeBand() / 2;
})
.attr("y", function(d) {
return h - yScale(d) + 14;
});
});
</script>
</body>
</html>
The issue, as I see it, is that when you update the rects with the new numbers using this statement
svg.selectAll("rect").data(dataset1)
the selectAll only selects the original 5 rects that were created based on dataset
var dataset = [5,10,13,14,15];
You could either create them all from the start with an equally sized starting dataset or append new rect svgs at this point.
Since you are doing a transition on the original rects I think it makes the most sense to just start with all 25. You could write a function to automatically generate this and put it where dataset is defined:
var dataset = [];
var numBars = 25; // This is the number of bars you want
var maxHeight = 25; // Seems like this is independent and needed as a ceiling
for(var i =0;i < N;i++){
var newNumber = Math.floor(Math.random()* maxHeight);
dataset.push(newNumber);
}
You could then replace 25 with numBars within your onclick function as well.