Related
I have a force directed graph using version 5 of d3.js and would like to include arrow heads for each link. I've included the code below and posted a jsfiddle. I'm seeking guidance on why the arrow heads (id=end-arrow) are not showing up in the graph. I'm referencing the end-arrow as an attribute in the declaration of link: .attr("marker-end","url(#end-arrow)"), and I don't know how to troubleshoot this.
HTML:
<svg id="viz"></svg>
Javascript with d3.js version 5:
// based on https://bl.ocks.org/mapio/53fed7d84cd1812d6a6639ed7aa83868
var width = 600;
var height = 400;
var border = 1;
var bordercolor="black";
var color = d3.scaleOrdinal(d3.schemeCategory10); // coloring of nodes
var graph = {
"nodes": [
{"id": "4718871", "group": 2, "img": "https://derivationmap.net/static/multiplybothsidesby.png", "width": 298, "height": 30, "linear index": 2},
{"id": "2131616531", "group": 0, "img": "https://derivationmap.net/static/2131616531.png", "width": 103, "height": 30, "linear index": 0},
{"id": "9565166889", "group": 0, "img": "https://derivationmap.net/static/9565166889.png", "width": 24, "height": 23, "linear index": 0},
{"id": "9040079362", "group": 0, "img": "https://derivationmap.net/static/9040079362.png", "width": 18, "height": 30, "linear index": 0},
{"id": "9278347", "group": 1, "img": "https://derivationmap.net/static/declareinitialexpr.png", "width": 270, "height": 30, "linear index": 1},
{"id": "6286448", "group": 4, "img": "https://derivationmap.net/static/declarefinalexpr.png", "width": 255, "height": 30, "linear index": 4},
{"id": "2113211456", "group": 0, "img": "https://derivationmap.net/static/2113211456.png", "width": 121, "height": 34, "linear index": 0},
{"id": "2169431", "group": 3, "img": "https://derivationmap.net/static/dividebothsidesby.png", "width": 260, "height": 30, "linear index": 3},
{"id": "3131111133", "group": 0, "img": "https://derivationmap.net/static/3131111133.png", "width": 121, "height": 34, "linear index": 0}
],
"links": [
{"source": "2169431", "target": "2113211456", "value": 1},
{"source": "2113211456", "target": "6286448", "value": 1},
{"source": "9278347", "target": "3131111133", "value": 1},
{"source": "4718871", "target": "2131616531", "value": 1},
{"source": "9040079362", "target": "4718871", "value": 1},
{"source": "2131616531", "target": "2169431", "value": 1},
{"source": "3131111133", "target": "4718871", "value": 1},
{"source": "9565166889", "target": "2169431", "value": 1}
]
};
var label = {
"nodes": [],
"links": []
};
graph.nodes.forEach(function(d, i) {
label.nodes.push({node: d});
label.nodes.push({node: d});
label.links.push({
source: i * 2,
target: i * 2 + 1
});
});
var labelLayout = d3.forceSimulation(label.nodes)
.force("charge", d3.forceManyBody().strength(-50))
.force("link", d3.forceLink(label.links).distance(0).strength(2));
var graphLayout = d3.forceSimulation(graph.nodes)
.force("charge", d3.forceManyBody().strength(-3000))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("x", d3.forceX(width / 2).strength(1))
.force("y", d3.forceY(height / 2).strength(1))
.force("link", d3.forceLink(graph.links).id(function(d) {return d.id; }).distance(50).strength(1))
.on("tick", ticked);
var adjlist = [];
graph.links.forEach(function(d) {
adjlist[d.source.index + "-" + d.target.index] = true;
adjlist[d.target.index + "-" + d.source.index] = true;
});
function neigh(a, b) {
return a == b || adjlist[a + "-" + b];
}
var svg = d3.select("#viz").attr("width", width).attr("height", height);
// define arrow markers for graph links
svg.append("svg:defs").append("svg:marker")
.attr("id", "end-arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 6)
.attr("markerWidth", 3)
.attr("markerHeight", 3)
.attr("orient", "auto")
.append("svg:line")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", "black");
// http://bl.ocks.org/AndrewStaroscik/5222370
var borderPath = svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("height", height)
.attr("width", width)
.style("stroke", bordercolor)
.style("fill", "none")
.style("stroke-width", border);
var container = svg.append("g");
svg.call(
d3.zoom()
.scaleExtent([.1, 4])
.on("zoom", function() { container.attr("transform", d3.event.transform); })
);
var link = container.append("g").attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter()
.append("line")
.attr("stroke", "#aaa")
.attr("stroke-width", "1px")
.attr("marker-end","url(#end-arrow)");
var node = container.append("g").attr("class", "nodes")
.selectAll("g")
.data(graph.nodes)
.enter()
.append("circle")
.attr("r", 5)
.attr("fill", function(d) { return color(d.group); })
node.on("mouseover", focus).on("mouseout", unfocus);
node.call(
d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
);
var labelNode = container.append("g").attr("class", "labelNodes")
.selectAll("text")
.data(label.nodes)
.enter()
.append("image")
// alternative option, unverified: https://stackoverflow.com/questions/39908583/d3-js-labeling-nodes-with-image-in-force-layout
// I have no idea why the i%2 is needed; without it I get two images per node
// switching between i%2==1 and i%2==0 produces different image locations (?)
.attr("xlink:href", function(d, i) { return i % 2 == 1 ? "" : d.node.img; } )
.attr("x", 0)
.attr("y", 0)
// the following alter the image size
.attr("width", function(d, i) { return d.node.width/2; })
.attr("height", function(d, i) { return d.node.height/2; })
// .append("text")
// .text(function(d, i) { return i % 2 == 0 ? "" : d.node.id; })
// .style("fill", "#555")
// .style("font-family", "Arial")
// .style("font-size", 12)
.style("pointer-events", "none"); // to prevent mouseover/drag capture
node.on("mouseover", focus).on("mouseout", unfocus);
function ticked() {
node.call(updateNode);
link.call(updateLink);
labelLayout.alphaTarget(0.3).restart();
labelNode.each(function(d, i) {
if(i % 2 == 0) {
d.x = d.node.x;
d.y = d.node.y;
} else {
var b = this.getBBox();
var diffX = d.x - d.node.x;
var diffY = d.y - d.node.y;
var dist = Math.sqrt(diffX * diffX + diffY * diffY);
var shiftX = b.width * (diffX - dist) / (dist * 2);
shiftX = Math.max(-b.width, Math.min(0, shiftX));
var shiftY = 16;
this.setAttribute("transform", "translate(" + shiftX + "," + shiftY + ")");
}
});
labelNode.call(updateNode);
}
function fixna(x) {
if (isFinite(x)) return x;
return 0;
}
function focus(d) {
var index = d3.select(d3.event.target).datum().index;
node.style("opacity", function(o) {
return neigh(index, o.index) ? 1 : 0.1;
});
labelNode.attr("display", function(o) {
return neigh(index, o.node.index) ? "block": "none";
});
link.style("opacity", function(o) {
return o.source.index == index || o.target.index == index ? 1 : 0.1;
});
}
function unfocus() {
labelNode.attr("display", "block");
node.style("opacity", 1);
link.style("opacity", 1);
}
function updateLink(link) {
link.attr("x1", function(d) { return fixna(d.source.x); })
.attr("y1", function(d) { return fixna(d.source.y); })
.attr("x2", function(d) { return fixna(d.target.x); })
.attr("y2", function(d) { return fixna(d.target.y); });
}
function updateNode(node) {
node.attr("transform", function(d) {
return "translate(" + fixna(d.x) + "," + fixna(d.y) + ")";
});
}
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
if (!d3.event.active) graphLayout.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) graphLayout.alphaTarget(0);
d.fx = null;
d.fy = null;
}
Based on feedback in the d3js Slack channel, there were two issues:
In the definition of the arrow, needed .append("svg:path")
With that fixed, the arrows were too small and were hidden behind the node circles. By making the arrows larger, they were visible.
I've updated http://bl.ocks.org/bhpayne/0a8ef2ae6d79aa185dcf2c3a385daf25 and the revised code is below:
HTML
<svg id="viz"></svg>
Javascript + d3js
// based on https://bl.ocks.org/mapio/53fed7d84cd1812d6a6639ed7aa83868
var width = 600;
var height = 400;
var border = 3;
var bordercolor = "black";
var color = d3.scaleOrdinal(d3.schemeCategory10); // coloring of nodes
var graph = {
"nodes": [{
"id": "4718871",
"group": 2,
"img": "https://derivationmap.net/static/multiplybothsidesby.png",
"width": 298,
"height": 30,
"linear index": 2
},
{
"id": "2131616531",
"group": 0,
"img": "https://derivationmap.net/static/2131616531.png",
"width": 103,
"height": 30,
"linear index": 0
},
{
"id": "9565166889",
"group": 0,
"img": "https://derivationmap.net/static/9565166889.png",
"width": 24,
"height": 23,
"linear index": 0
},
{
"id": "9040079362",
"group": 0,
"img": "https://derivationmap.net/static/9040079362.png",
"width": 18,
"height": 30,
"linear index": 0
},
{
"id": "9278347",
"group": 1,
"img": "https://derivationmap.net/static/declareinitialexpr.png",
"width": 270,
"height": 30,
"linear index": 1
},
{
"id": "6286448",
"group": 4,
"img": "https://derivationmap.net/static/declarefinalexpr.png",
"width": 255,
"height": 30,
"linear index": 4
},
{
"id": "2113211456",
"group": 0,
"img": "https://derivationmap.net/static/2113211456.png",
"width": 121,
"height": 34,
"linear index": 0
},
{
"id": "2169431",
"group": 3,
"img": "https://derivationmap.net/static/dividebothsidesby.png",
"width": 260,
"height": 30,
"linear index": 3
},
{
"id": "3131111133",
"group": 0,
"img": "https://derivationmap.net/static/3131111133.png",
"width": 121,
"height": 34,
"linear index": 0
}
],
"links": [{
"source": "2169431",
"target": "2113211456",
"value": 1
},
{
"source": "2113211456",
"target": "6286448",
"value": 1
},
{
"source": "9278347",
"target": "3131111133",
"value": 1
},
{
"source": "4718871",
"target": "2131616531",
"value": 1
},
{
"source": "9040079362",
"target": "4718871",
"value": 1
},
{
"source": "2131616531",
"target": "2169431",
"value": 1
},
{
"source": "3131111133",
"target": "4718871",
"value": 1
},
{
"source": "9565166889",
"target": "2169431",
"value": 1
}
]
};
var label = {
"nodes": [],
"links": []
};
graph.nodes.forEach(function(d, i) {
label.nodes.push({
node: d
});
label.nodes.push({
node: d
});
label.links.push({
source: i * 2,
target: i * 2 + 1
});
});
var labelLayout = d3.forceSimulation(label.nodes)
.force("charge", d3.forceManyBody().strength(-50))
.force("link", d3.forceLink(label.links).distance(0).strength(2));
var graphLayout = d3.forceSimulation(graph.nodes)
.force("charge", d3.forceManyBody().strength(-3000))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("x", d3.forceX(width / 2).strength(1))
.force("y", d3.forceY(height / 2).strength(1))
.force("link", d3.forceLink(graph.links).id(function(d) {
return d.id;
}).distance(50).strength(1))
.on("tick", ticked);
var adjlist = [];
graph.links.forEach(function(d) {
adjlist[d.source.index + "-" + d.target.index] = true;
adjlist[d.target.index + "-" + d.source.index] = true;
});
function neigh(a, b) {
return a == b || adjlist[a + "-" + b];
}
var svg = d3.select("#viz").attr("width", width).attr("height", height);
// define arrow markers for graph links
svg.append("svg:defs").append("svg:marker")
.attr("id", "end-arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 10)
.attr("markerWidth", 20)
.attr("markerHeight", 20)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L20,0L0,5")
.attr("fill", "#000");
// http://bl.ocks.org/AndrewStaroscik/5222370
var borderPath = svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("height", height)
.attr("width", width)
.style("stroke", bordercolor)
.style("fill", "none")
.style("stroke-width", border);
var container = svg.append("g");
svg.call(
d3.zoom()
.scaleExtent([.1, 4])
.on("zoom", function() {
container.attr("transform", d3.event.transform);
})
);
var link = container.append("g").attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter()
.append("line")
.attr("stroke", "#aaa")
.attr("marker-end", "url(#end-arrow)")
.attr("stroke-width", "1px");
var node = container.append("g").attr("class", "nodes")
.selectAll("g")
.data(graph.nodes)
.enter()
.append("circle")
.attr("r", 10)
.attr("fill", function(d) {
return color(d.group);
})
node.on("mouseover", focus).on("mouseout", unfocus);
node.call(
d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
);
var labelNode = container.append("g").attr("class", "labelNodes")
.selectAll("text")
.data(label.nodes)
.enter()
.append("image")
// alternative option, unverified: https://stackoverflow.com/questions/39908583/d3-js-labeling-nodes-with-image-in-force-layout
// I have no idea why the i%2 is needed; without it I get two images per node
// switching between i%2==1 and i%2==0 produces different image locations (?)
.attr("xlink:href", function(d, i) {
return i % 2 == 1 ? "" : d.node.img;
})
.attr("x", 0)
.attr("y", 0)
// the following alter the image size
.attr("width", function(d, i) {
return d.node.width / 2;
})
.attr("height", function(d, i) {
return d.node.height / 2;
})
// .append("text")
// .text(function(d, i) { return i % 2 == 0 ? "" : d.node.id; })
// .style("fill", "#555")
// .style("font-family", "Arial")
// .style("font-size", 12)
.style("pointer-events", "none"); // to prevent mouseover/drag capture
node.on("mouseover", focus).on("mouseout", unfocus);
function ticked() {
node.call(updateNode);
link.call(updateLink);
labelLayout.alphaTarget(0.3).restart();
labelNode.each(function(d, i) {
if (i % 2 == 0) {
d.x = d.node.x;
d.y = d.node.y;
} else {
var b = this.getBBox();
var diffX = d.x - d.node.x;
var diffY = d.y - d.node.y;
var dist = Math.sqrt(diffX * diffX + diffY * diffY);
var shiftX = b.width * (diffX - dist) / (dist * 2);
shiftX = Math.max(-b.width, Math.min(0, shiftX));
var shiftY = 16;
this.setAttribute("transform", "translate(" + shiftX + "," + shiftY + ")");
}
});
labelNode.call(updateNode);
}
function fixna(x) {
if (isFinite(x)) return x;
return 0;
}
function focus(d) {
var index = d3.select(d3.event.target).datum().index;
node.style("opacity", function(o) {
return neigh(index, o.index) ? 1 : 0.1;
});
labelNode.attr("display", function(o) {
return neigh(index, o.node.index) ? "block" : "none";
});
link.style("opacity", function(o) {
return o.source.index == index || o.target.index == index ? 1 : 0.1;
});
}
function unfocus() {
labelNode.attr("display", "block");
node.style("opacity", 1);
link.style("opacity", 1);
}
function updateLink(link) {
link.attr("x1", function(d) {
return fixna(d.source.x);
})
.attr("y1", function(d) {
return fixna(d.source.y);
})
.attr("x2", function(d) {
return fixna(d.target.x);
})
.attr("y2", function(d) {
return fixna(d.target.y);
});
}
function updateNode(node) {
node.attr("transform", function(d) {
return "translate(" + fixna(d.x) + "," + fixna(d.y) + ")";
});
}
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
if (!d3.event.active) graphLayout.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) graphLayout.alphaTarget(0);
d.fx = null;
d.fy = null;
}
hey guys i am currently trying to change the visibility of 4 lines depending on whether the slider position is at the same "year-tick" as i have in my data.
for example: i would want line 3 which has the value "1994" stored to only be visible as long as the slider is on position "1994"
i havent workder with sliders yet, which is why i am having this much trouble getting this to work.
here is a fiddle of my code: https://fiddle.jshell.net/42jdw2Lt/3/
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.2/d3.min.js">
</script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
<script>
var width = 1500;
var height = 1500;
var margin = {top: 50, left: 50, right: 50, bottom:50},
height = 650-margin.top-margin.bottom,
width = 1200-margin.left-margin.right;
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 strwi = d3.scaleLinear()
.domain([100, 400])
.range([7,35])
var group = svg.append("g")
var series = [
[{"x": 360, "y": 250, "num": 100}, {"x": 520, "y": 400, "num": 100}, {"x":
630, "y": 300, "num": 100, "year": 1991}],
[{"x": 71, "y": 45, "num": 200}, {"x": 32, "y": 39, "num": 200}, {"x": 43,
"y": 70, "num": 200, "year": 1992}],
[{"x": 100, "y": 300, "num": 300}, {"x": 200, "y": 200, "num": 300}, {"x":
300, "y": 200, "num": 300, "year": 1994}],
[{"x": 101, "y": 202, "num": 400}, {"x": 102, "y": 204, "num": 400}, {"x":
103, "y": 215, "num": 400, "year": 1995}]
];
var line = d3.line()
.curve(d3.curveBasis)
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
group.selectAll(".line")
.data(series)
.enter().append("path")
.attr("class", "line")
// .attr("visibility", "hidden")
.attr("stroke-width", function(d) {return strwi(d); })
.attr("stroke", "black")
.attr("fill", "none")
.attr("d", line);
var data = [1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999,
2000, 2001, 2002]
var x = d3.scaleLinear()
.domain(d3.extent(data))
.range([0, width])
.clamp(true);
var slider = svg.append("g")
.attr("class", "slider")
.attr("transform", "translate(" + 0 + "," + 300 + ")");
slider.append("line")
.attr("class", "track")
.attr("x1", x.range()[0])
.attr("x2", x.range()[1])
.attr("stroke", "black")
.attr("stroke-width", "4")
.select(function() { return
this.parentNode.appendChild(this.cloneNode(true)); })
.attr("class", "track-inset")
.select(function() { return
this.parentNode.appendChild(this.cloneNode(true)); })
.attr("class", "track-overlay")
.call(d3.drag()
.on("start.interrupt", function() { slider.interrupt(); })
.on("start drag", function() { hue(x.invert(d3.event.x));
}));
slider.insert("g", ".track-overlay")
.attr("class", "ticks")
.selectAll("ticks")
.data(x.ticks(data.length))
.enter().append("text")
.attr("x", x)
.attr("text-anchor", "middle")
.attr("transform", "translate(0," + 30 + ")")
.text(function(d) { return d; })
.exit()
.data(x.ticks(data.length * 2))
.enter().append("circle")
.attr("cx", x)
.attr("r", 3)
.attr("fill", "#c1c7cd");
slider.insert("g", ".track-overlay")
.attr("class", "ticks--cirlces")
.selectAll("ticks--ticks");
var handle = slider.insert("circle", ".track-overlay")
.attr("class", "handle")
.attr("r", 9);
slider.transition() // Gratuitous intro!
.duration(750)
.tween("hue", function() {
var i = d3.interpolate(0, 70);
return function(t) { hue(i(t)); };
});
function hue(h) {
handle.attr("cx", x(h));
d3.select(".text")
.text( (Math.round(h*2)/2).toFixed(1) );
}
</script>
I added the prop year to all your elements in the data selecting all the line paths in a variable called paths so that I can change the opacity when you move your slider like this
var paths = group.selectAll(".line")
.data(series)
.enter().append("path")
.attr("class", "line")
// .attr("visibility", "hidden")
.attr("stroke-width", function(d) {
return strwi(d);
})
.attr("stroke", "black")
.attr("fill", "none")
.attr("d", line);
and then in your hue function, I added this code to change the opacity based on your slider
paths.attr('opacity', function(d) {
if (((Math.round(h * 2) / 2).toFixed(1)) > d[2]['year']) {
return 0;
} else {
return 1;
}
})
Here's a fiddle for you:
var width = 1500;
var height = 1500;
var margin = {
top: 50,
left: 50,
right: 50,
bottom: 50
},
height = 650 - margin.top - margin.bottom,
width = 1200 - margin.left - margin.right;
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 strwi = d3.scaleLinear()
.domain([100, 400])
.range([7, 35])
var group = svg.append("g")
var series = [
[{
"x": 360,
"y": 250,
"num": 100,
"startYear": 1991,
"endYear": 1995
}, {
"x": 520,
"y": 400,
"num": 100,
"startYear": 1991,
"endYear": 1995
}, {
"x": 630,
"y": 300,
"num": 100,
"startYear": 1991,
"endYear": 1995
}],
[{
"x": 71,
"y": 45,
"num": 200,
"startYear": 1992,
"endYear": 1993
}, {
"x": 32,
"y": 39,
"num": 200,
"startYear": 1992,
"endYear": 1993
}, {
"x": 43,
"y": 70,
"num": 200,
"startYear": 1992,
"endYear": 1993
}],
[{
"x": 100,
"y": 300,
"num": 300,
"startYear": 1994,
"endYear": 1996
}, {
"x": 200,
"y": 200,
"num": 300,
"startYear": 1994,
"endYear": 1996
}, {
"x": 300,
"y": 200,
"num": 300,
"startYear": 1994,
"endYear": 1996
}],
[{
"x": 101,
"y": 202,
"num": 400,
"startYear": 1995,
"endYear": 1997
}, {
"x": 102,
"y": 204,
"num": 400,
"startYear": 1995,
"endYear": 1997
}, {
"x": 103,
"y": 215,
"num": 400,
"startYear": 1995,
"endYear": 1997
}]
];
var line = d3.line()
.curve(d3.curveBasis)
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
});
var paths = group.selectAll(".line")
.data(series)
.enter().append("path")
.attr("class", "line")
.attr('v1',function(d){
return d[2]['startYear'];
})
.attr('v2',function(d) {
return d[2]['endYear'];
})
.attr('opacity',0)
//.attr("visibility", "hidden")
.attr("stroke-width", function(d) {
return strwi(d);
})
.attr("stroke", "black")
.attr("fill", "none")
.attr("d", line);
var data = [1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002]
var x = d3.scaleLinear()
.domain(d3.extent(data))
.range([0, width])
.clamp(true);
var slider = svg.append("g")
.attr("class", "slider")
.attr("transform", "translate(" + 0 + "," + 300 + ")");
slider.insert("g", ".track-overlay")
.attr("class", "ticks")
.selectAll("ticks")
.data(x.ticks(data.length))
.enter().append("text")
.attr("x", x)
.attr("text-anchor", "middle")
.attr("transform", "translate(0," + 30 + ")")
.text(function(d) {
return d;
})
.exit()
.data(x.ticks(data.length * 2))
.enter().append("circle")
.attr("cx", x)
.attr("r", 3)
.attr("fill", "#c1c7cd");
slider.append("line")
.attr("class", "track")
.attr("x1", x.range()[0])
.attr("x2", x.range()[1])
.attr("stroke", "black")
.attr("stroke-width", "4")
.select(function() {
return this.parentNode.appendChild(this.cloneNode(true));
})
.attr("class", "track-inset")
.select(function() {
return this.parentNode.appendChild(this.cloneNode(true));
})
.attr("class", "track-overlay")
.call(d3.drag()
.on("start.interrupt", function() {
slider.interrupt();
})
.on("start drag", function() {
//console.log(d3.event.x)
hue(x.invert(d3.event.x));
}));
slider.insert("g", ".track-overlay")
.attr("class", "ticks--cirlces")
.selectAll("ticks--ticks");
var handle = slider.insert("circle", ".track-overlay")
.attr("class", "handle")
.attr("r", 9);
slider.transition() // Gratuitous intro!
.duration(750)
.tween("hue", function() {
var i = d3.interpolate(0, 70);
return function(t) {
//nsole.log(t)
hue(i(t));
};
});
function hue(h) {
handle.attr("cx", x(h));
d3.select(".text")
.text((Math.round(h * 2) / 2).toFixed(1));
paths
.transition().duration(300)
.attr('opacity', function(d) {
if (((Math.round(h * 2) / 2).toFixed(1)) >= d[2]['startYear'] && ((Math.round(h * 2) / 2).toFixed(1)) <= d[2]['endYear']) {
return 1;
} else {
return 0;
}
})
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.2/d3.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
Let me know if this is what you wanted or there's something else.
I've recently begun learning D3.js and I am struggling to create a transition in a scatter plot with the following data:
var data = [
{"year" : "2004", "x":100, "y":300, "size": 2, "type": "A"},
{"year" : "2005", "x":200, "y":200, "size": 2, "type": "A"},
{"year" : "2006", "x":300, "y":100, "size": 2, "type": "A"},
{"year" : "2004", "x":150, "y":250, "size": 2.382450, "type": "B"},
{"year" : "2005", "x":150, "y":250, "size": 3.078548, "type": "B"},
{"year" : "2006", "x":150, "y":250, "size": 4.265410, "type": "B"}];
Where in the scatter plot there are 2 points (type A&B) and they change location (x&y) and size by year. I've created a fiddle where I try to nest the data and plot the points, but making the next step of using transition() function is confusing. More specifically, I am still declaring the whole data, but to make transitions work I only need part of the data.
The key to understand what you want lies here:
There are 2 points and they change location (x&y) and size by year
Therefore, this is clearly a XY problem. Your problem is not "how to transition with nested data". Your problem is "how to transition by year".
My proposed solution involves, first of all, dropping that nested array. You don't need that.
Instead, get all the years in the data...
var years = [...new Set(data.map(function(d) {
return d.year
}))];
..., filter the data by year...
var dataStart = data.filter(function(d) {
return d.year === years[0]
});
... and loop trough the years. Here, I'm using d3.interval():
var counter = 1;
var timer = d3.interval(transition, 1500);
function transition() {
var newData = data.filter(function(d) {
return d.year === years[counter]
});
svg.selectAll("circle").data(newData)
.transition()
.duration(1000)
.attr("cx", function(d) {
console.log(d)
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", function(d) {
return d.size * 10;
});
counter += 1;
if (counter === 3) {
timer.stop()
}
}
Here is the demo:
var data = [{
"year": "2004",
"x": 100,
"y": 100,
"size": 2,
"type": "A"
}, {
"year": "2005",
"x": 200,
"y": 180,
"size": 2,
"type": "A"
}, {
"year": "2006",
"x": 300,
"y": 50,
"size": 2,
"type": "A"
}, {
"year": "2004",
"x": 150,
"y": 150,
"size": 2.382450,
"type": "B"
}, {
"year": "2005",
"x": 150,
"y": 50,
"size": 3.078548,
"type": "B"
}, {
"year": "2006",
"x": 150,
"y": 100,
"size": 4.265410,
"type": "B"
}];
var width = 400,
height = 200;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var years = [...new Set(data.map(function(d) {
return d.year
}))];
var dataStart = data.filter(function(d) {
return d.year === years[0]
});
var cell = svg.selectAll("circle")
.data(dataStart);
cell.enter()
.append("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", function(d) {
return d.size * 10;
});
var counter = 1;
var timer = d3.interval(transition, 1500);
function transition() {
var newData = data.filter(function(d) {
return d.year === years[counter]
});
svg.selectAll("circle").data(newData)
.transition()
.duration(1000)
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", function(d) {
return d.size * 10;
});
counter += 1;
if (counter === 3) {
timer.stop()
}
}
<script src="https://d3js.org/d3.v4.min.js"></script>
I have an array of objects which I bind to a g-element. For each g-element I generate a rect-element. It works so far but now I want to have a dynamic number of "parts" in each g-element which should generate a new rect and text element inside the existing rect.
Here you will find my example or on fiddle.net/tnnomejg/
var config = {
width: 600
};
var data = [
{
height: 150,
y: 0,
parts: [
{
title: "Hello",
x: 50,
y: 60,
},
{
title: "World",
x: 350,
y: 60,
}
],
},
{
height: 150,
y: 155,
parts: [
{
title: "Demo",
x: 280,
y: 215,
}
],
},
];
var svg = d3.select("body").append("svg").attr("width", config.width).attr("height", 500);
var g = svg.selectAll("g")
.data(data)
.enter()
.append("g");
g.append("rect")
.attr("x", 1)
.attr("y", function(d) { return d.y; })
.attr("width", config.width)
.attr("height", function(d) { return d.height; });
Thx.
You could use a subselection binding your parts' data to the corresponding elements using an accessor function like this:
var parts = g.selectAll("g.part")
.data(function(d) { return d.parts; })
.enter()
.append("g")
.attr("class", "part");
Having this subselection at your hands you may insert/append content accessing the corresponding data bound to each group/part.
var config = {
width: 600
};
var data = [
{
height: 150,
y: 0,
parts: [
{
title: "Hello",
x: 50,
y: 60
},
{
title: "World",
x: 350,
y: 60
}
]
},
{
height: 150,
y: 155,
parts: [
{
title: "Demo",
x: 280,
y: 215
}
]
}
];
var svg = d3.select("body").append("svg").attr("width", config.width).attr("height", 500);
var g = svg.selectAll("g")
.data(data)
.enter()
.append("g");
g.append("rect")
.attr("x", 1)
.attr("y", function(d) { return d.y; })
.attr("width", config.width)
.attr("height", function(d) { return d.height; });
var parts = g.selectAll("g.part")
.data(function(d) { return d.parts; })
.enter()
.append("g")
.attr("class", "part");
parts.append("rect")
.attr({
"class": "part",
"x": function(d) { return d.x; },
"y": function(d) { return d.y; },
"width": 200,
"height":80
});
parts.append("text")
.attr({
"class": "part",
"x": function(d) { return d.x; },
"y": function(d) { return d.y; }
})
.text(function(d) { return d.title; });
g.part rect {
fill:white;
}
g.part text {
fill: red;
stroke: none;
font-size:20px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Use d3 selector each method.
g.each(function(d, i) {
var rect = d3.select(this);
d.parts.forEach(function(p) {
rect.append("rect")
.style("fill", "aliceblue")
.attr("x", p.x)
.attr("y", p.y)
.attr("width", config.width / 3)
.attr("height", d.height / 2);
rect.append("text")
.style("stroke", "brown")
.attr("x", p.x)
.attr("y", p.y)
.text(p.title);
});
});
var config = {
width: 600
};
var data = [{
height: 150,
y: 0,
parts: [{
title: "Hello",
x: 50,
y: 60,
}, {
title: "World",
x: 350,
y: 60,
}],
}, {
height: 150,
y: 155,
parts: [{
title: "Demo",
x: 280,
y: 215,
}],
}, ];
var svg = d3.select("body").append("svg").attr("width", config.width).attr("height", 500);
var g = svg.selectAll("g")
.data(data)
.enter()
.append("g");
g.append("rect")
.attr("x", 1)
.attr("y", function(d) {
return d.y;
})
.attr("width", config.width)
.attr("height", function(d) {
return d.height;
});
g.each(function(d, i) {
var rect = d3.select(this);
d.parts.forEach(function(p) {
rect.append("rect")
.style("fill", "aliceblue")
.attr("x", p.x)
.attr("y", p.y)
.attr("width", config.width / 3)
.attr("height", d.height / 2);
rect.append("text")
.style("stroke", "brown")
.attr("x", p.x)
.attr("y", p.y)
.text(p.title);
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I have been searching for quite a while but I haven't been able to figure it out.
I want to have a transition between two interpolations "basis" and "step-after" for a single line.
var time_scale, kpi_scale;
var line;
var data = [
{"kpi": 100, "time": 1317448800000},
{"kpi": 200, "time": 1320127200000},
{"kpi": 250, "time": 1322719200000},
{"kpi": 180, "time": 1325397600000},
{"kpi": 230, "time": 1328076000000},
{"kpi": 360, "time": 1330581600000},
{"kpi": 140, "time": 1333260000000},
{"kpi": 120, "time": 1335852000000},
{"kpi": 240, "time": 1338530400000},
{"kpi": 190, "time": 1341122400000},
{"kpi": 185, "time": 1343800800000},
{"kpi": 130, "time": 1346479200000},
{"kpi": 340, "time": 1349071200000},
{"kpi": 320, "time": 1351749600000},
{"kpi": 250, "time": 1354341600000}
];
// set up the viewport, the scales, and axis generators
var container_dimensions = {width: 900, height: 400},
margins = {top: 10, right: 20, bottom: 30, left: 60},
chart_dimensions = {
width: container_dimensions.width - margins.left - margins.right,
height: container_dimensions.height - margins.top - margins.bottom
};
var time_extent = d3.extent(
data,
function(d){return d.time;}
);
time_scale = d3.time.scale()
.domain(time_extent)
.range([0, chart_dimensions.width]);
var kpi_extent = d3.extent(
data,
function(d){return d.kpi;}
);
kpi_scale = d3.scale.linear()
.domain(kpi_extent)
.range([chart_dimensions.height, 0]);
var container = d3.select('body')
.append('svg')
.attr("width", container_dimensions.width)
.attr("height", container_dimensions.height)
.append("g")
.attr("transform", "translate(" + margins.left + "," + margins.top + ")")
.attr("id","chart");
draw_timeseries (data);
transitionToStepped (data);
function draw_timeseries (data) {
line = d3.svg.line()
.x(function(d){return time_scale(d.time);})
.y(function(d){return kpi_scale(d.kpi);})
.interpolate("basis");
d3.select('#chart').append('path')
.attr('d', line(data))
.attr('id', 'line')
.attr('class', 'line');
}
function transitionToStepped (data) {
line.interpolate("step-after");
d3.select("#chart").selectAll(".line")
.transition()
.duration(2000)
.attr("d", line(data))
.delay(2000);
}
I have saved a small jsfiddle containing my code.
jsFiddle Interpolation Transition
I wanted to "morph" between the basis line and the step-after line. But d3 is sliding in the step-after line from the left instead of "bending" the basis line into a step-after one.
Thanks for your help.
Christoph
There has got to be a way to do this, and certainly one better than the offering below (I am looking forward to seeing other answers), but I have seen no answers here as of yet, and this will at least do a "morph" for you, although it also makes the line parallel to the x-axis during the transition. Not a terrible thing, but I believe not precisely what you are after.
Here's a fiddle for the full code and demo,
modified from your original:
http://jsfiddle.net/n5P6z/2/
The meat of the approach is here:
function xLineReduce() {
line.x(function (d) {
return time_scale(d.time);
}).y(function (d) {
return chart_dimensions.height;
});
}
function resetInterpolator() {
line.interpolate("cubic-in-out");
}
function stepItUp() {
line.x(function (d, i) {
return time_scale(d.time);
})
.y(function (d, i) {
return kpi_scale(d.kpi);
})
.interpolate("step-after");
}
function transitionToStepped(myData) {
path.data(myData)
.call(xLineReduce)
.transition()
.delay(1000)
.duration(500)
.attr('d', line(myData));
path.data(myData)
.call(resetInterpolator)
.transition()
.delay(1500)
.duration(0)
.attr('d', line(myData));
path.data(myData)
.call(stepItUp)
.transition()
.delay(1500)
.duration(500)
.attr('d', line(myData));
}