I am setting the nodes to be fixed with
let link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line")
.attr("stroke-width", () => 4)
let node = svg.append('g')
.attr("class", "nodes")
.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.call(
d3.drag()
.on("start", Node.dragstarted)
.on("drag", Node.dragged)
.on("end", Node.dragended))
node.append("title")
.text(d => d.country)
node.append('image')
.attr('xlink:href', d => 'https://rawgit.com/hjnilsson/country-flags/master/svg/' + d.code + '.svg')
.attr('height', d => 2.5 * (area[d.code]||5))
.attr('width', d => 4 * (area[d.code])||5)
simulation
.nodes(graph.nodes.map(c => {
if(latlong[c.code] === undefined) {
console.log(c,'missing lat/long data')
return c
}
c.x = (+latlong[c.code][0])
c.y = (+latlong[c.code][1])
c.fixed = true
return c
}))
.on("tick", ticked)
This does correctly display images in apparently different locations than without the x and y values, but .. the fixed property isn't working.
Here's my code: codepen
If anyone also knows how I can set the canvas up so that nothing escapes out the top or bottom I'd appreciate that as well.
d3.js v4 does not have a fixed property. Instead, you need to set the nodes fx and fy attributes. Note, your drag functions are already doing this.
.nodes(graph.nodes.map(c => {
if(latlong[c.code] === undefined) {
console.log(c,'missing lat/long data')
return c
}
c.fx = c.x = (+latlong[c.code][0])
c.fy = c.y = (+latlong[c.code][1])
return c
}))
https://codepen.io/anon/pen/ZKXmVe
Related
Maybe I bit off more than I can chew as someone who has a vague memory of only seeing D3... but here I go. I am using R shiny and r2d3. I've copy pasted some very basic examples of r2d3 from here (https://rstudio.github.io/r2d3/) to kind of feel out how to actually incorporate any d3 into r/ 'kicking the tires'. For what I'm currently working with and what I actually want to modify for r2d3 is this here ->(https://observablehq.com/#d3/tree). I've made a few modifications just playing around with it, as pasted below. Now it comes to actually modifying it to be used for r2d3.
I know that the .js file already includes the svg, data which was passed into the function, width, height, options, theme. So I did my best to remove all those things, but it could be I missed it because I don't exactly know what Im looking at for each of these lines. Now I'm at a point where something is displaying; however, it is just what I believe to be only the root node circle with no labels. When the node is grey, it means it has no children, so this lonely node appearing has no children. Also when I hover my mouse it does recognize a mouseover and change in color. This makes me believe I'm missing something really small and dumb - At least I hope that is the case.
I inspected the element. I see an error that says
Uncaught TypeError: d3.select(...).append(...).duration is not a function
r2d3-script-454:192 at SVGCircleElement.
So I look this up and I come across this is stack overflow (Functions not recognised when using R2D3). I realize I don't actually know which of these functions need a dependency or how to find that out. If it is a dependency that I need to resolve this, I don't know what the URL would be or how to figure that out.
Server.R
library(shiny)
library(r2d3)
# download.file("https://d19vzq90twjlae.cloudfront.net/leaflet-0.7/leaflet.js", "leaflet-0.7.js")
# download.file("https://cdn.jsdelivr.net/gh/holtzy/D3-graph-gallery#master/LIB/d3-scale-radial.js", "d3-scale-radial.js")
# download.file("https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.7.1/d3-tip.min.js", "d3-tip.min.js")
server <- function(input, output) {
output$d3 <- renderD3({
r2d3(
runif(5, 0, input$bar_max),
script = system.file("examples/baranims.js", package = "r2d3")
)
})
output$d3dendrogram <- renderD3({
r2d3(
data = read.csv("flare.csv"),
d3_version = 4,
script = "r2d3dendrogram.js",
dependencies = list("leaflet-0.7.js",
"d3-scale-radial.js",
"d3-tip.min.js"))
})
}
ui.R
ui <- fluidPage(
inputPanel(
sliderInput("bar_max", label = "Max:",
min = 0, max = 1, value = 1, step = 0.05)
),
# d3Output("d3"),
d3Output("d3dendrogram")
)
r2d3dendrogram.js
//hardcoded variables
dy = 192
dy = width/6
dx = 30
tree = d3.tree().nodeSize([dx, dy])
margin = ({top: 10, right: 120, bottom: 10, left: 40})
diagonal = d3.linkHorizontal().x(d => d.y).y(d => d.x)
const root = d3.hierarchy(data);
root.x0 = dy / 2;
root.y0 = 0;
root.descendants().forEach((d, i) => {
d.id = i;
d._children = d.children;
if (d.depth && d.data.name.length !== 7) d.children = null;
});
const gLink = svg.append("g")
.attr("fill", "none")
.attr("stroke", "#555") //color of the tree link branches
.attr("stroke-opacity", 0.4)
.attr("stroke-width", 1.5);
const gNode = svg.append("g")
.attr("cursor", "pointer") //changes mouse into the pointing hand
.attr("pointer-events", "all");
//creates the div element to appear on hovering on a node
var div = d3.select("body").append("div")
//.attr("class", "tooltip")
.style("position", "absolute")
.style("text-align", "center")
.style("background", "black")
.style("border-radius", "8px")
.style("border", "solid 1px green")
.style("opacity", 0);
//controls the display that shows up on hovering on a node
function mouseover(d){
div.html("hello" + "<br/>"+"Everyone")
.style("left", (d3.event.pageX + 10) + "px")
.style("top", (d3.event.pageY - 15) + "px")
.style("opacity",1);
}
//controls how the div element from the hover acts when mouse goes away
function mouseout(){
div.style("opacity", 1e-6);
}
d3.selectAll("circle")
.on("mouseover", mouseover)
.on("mouseover", mouseout);
function update(source) {
const duration = d3.event && d3.event.altKey ? 2500 : 550;
const nodes = root.descendants().reverse();
const links = root.links();
// Compute the new tree layout.
tree(root);
let left = root;
let right = root;
root.eachBefore(node => {
if (node.x < left.x) left = node;
if (node.x > right.x) right = node;
});
const height = right.x - left.x + margin.top + margin.bottom;
const transition = svg.transition()
//.duration(duration)
.attr("viewBox", [-margin.left, left.x - margin.top, width, height])
.tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle"));
// Update the nodes…
const node = gNode.selectAll("g")
.data(nodes, d => d.id);
// Enter any new nodes at the parent's previous position.
const nodeEnter = node.enter().append("g")
.attr("transform", d => `translate(${source.y0},${source.x0})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0)
.on("click", (event, d) => {
d.children = d.children ? null : d._children;
update(d)
})
//adds on the tooltip for the info that shows up on the hover
//content inside
nodeEnter.append("circle")
.attr("r", 6.5)
.attr("fill", d => d._children ? "#64B4FF" : "#999") /*creates the blue color or the grey color*/
.attr("stroke-width", 10)
.attr('transform', 'translate(0, 0)')
.on('mouseover', function (d, n, i) {
//Below is the hover feature for the nodes to change light blue
d3.select(this).transition()
.duration('10')
.attr('fill', '#E2F1FF') //fill color to be light blue
//Makes the new div appear on hover:
d3.select(this).append("div")
. duration('50')
.style("opacity", 1)
})
.on('mouseout', function (d,i) {
d3.select(this).transition()
//.duration('500')
.attr("fill", d => d._children ? "#64B4FF" : "#999"); /*creates the blue color || grey color*/
//Makes the new div disappear:
div.transition()
.duration('50')
.style("opacity", 1)
});
nodeEnter.append("text")
.attr("dy", "0.31em")
.attr("x", d => d._children ? -6 : 6)
.attr("text-anchor", d => d._children ? "end" : "start")
.text(d => d.data.name)
.clone(true).lower()
.attr("stroke-linejoin", "round")
.attr("stroke-width", 3)
.attr("stroke", "white");
// Transition nodes to their new position.
const nodeUpdate = node.merge(nodeEnter).transition(transition)
.attr("transform", d => `translate(${d.y},${d.x})`)
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1);
// Transition exiting nodes to the parent's new position.
const nodeExit = node.exit().transition(transition).remove()
.attr("transform", d => `translate(${source.y},${source.x})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0);
// Update the links…
const link = gLink.selectAll("path")
.data(links, d => d.target.id);
// Enter any new links at the parent's previous position.
const linkEnter = link.enter().append("path")
.attr("d", d => {
const o = {x: source.x0, y: source.y0}
return diagonal({source: o, target: o});
});
// Transition links to their new position.
link.merge(linkEnter).transition(transition)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition(transition).remove()
.attr("d", d => {
const o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
});
// Stash the old positions for transition.
root.eachBefore(d => {
d.x0 = d.x;
d.y0 = d.y;
});
}
update(root);
return svg.node();
Sidenote: I know d3 is huge. If anyone can offer any tips or anything else to learn about all of this (free would be a nice plus), let me know. I've watched some sections of a 19hr video online with Curran. It was somewhat useful, but this is some learning curve for me; I had to re-watch a few things over and over again.
Sidenote: Sorry, please ignore the slider bar. It shouldn't do anything.
edit - adding the javascript tag in case this is requires a javascript related solution
I'm having issues where my D3 Force Graph is showing with nodes but not connecting the links.
I'm not sure what the issue is because my strokes are defined.
I'm not sure if it's an issue with the JSON Data format or what it could be. Where could the issue be?
I am using Angular D3 with D3.Js & what I am trying to build is a Force Directed Network Graph.
JSON Data I'm using:
https://gist.github.com/KoryJCampbell/f18f8a11030269739eabc7de05b38b11
graph.ts
loadForceDirectedGraph(nodes: Node[], links: Link[]) {
const svg = d3.select('svg');
const width = +svg.attr('width');
const height = +svg.attr('height');
const color = d3.scaleOrdinal(d3.schemeTableau10);
const simulation = d3.forceSimulation()
.force('link', d3.forceLink().id((d: Node) => d.name))// the id of the node
.force("charge", d3.forceManyBody().strength(-5).distanceMax(0.1 * Math.min(width, height)))
.force('center', d3.forceCenter(width / 2, height / 2));
console.log(nodes, links);
const link = svg.append('g')
.attr('class', 'links')
.selectAll('line')
.data(links)
.enter()
.append('line')
.attr('stroke-width', d => Math.sqrt(d.index))
.attr('stroke', 'black');
const node = svg.append('g')
.attr('class', 'nodes')
.selectAll('circle')
.data(nodes)
.enter()
.append('circle')
.attr('r', 5)
.attr("fill", function(d) { return color(d.company); })
.call(d3.drag()
.on('start', dragStarted)
.on('drag', dragged)
.on('end', dragEnded)
);
node.append('title').text((d) => d.name);
simulation
.nodes(nodes)
.on('tick', ticked);
simulation.force<d3.ForceLink<any, any>>('link')
.links(links);
function ticked() {
node
.attr('cx', d => d.x)
.attr('cy', d => d.y);
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
}
function dragStarted(event) {
if (!event.active) { simulation.alphaTarget(0.3).restart(); }
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragEnded(event) {
if (!event.active) { simulation.alphaTarget(0); }
event.subject.fx = null;
event.subject.fy = null;
}
}
I think your links format in json file is wrong.
change links to this format.
links: [
{
source: "<paste source's name property>",
target: "<paste target's name property>",
index: 0
},
]
because you are doing:
.force('link', d3.forceLink().id((d: Node) => d.name))// the name of the node
I need to display the total value on top of the stacked chart, I am using D3 Stacked bar chart with Angular 7
I have tried implementing but getting an error saying ERROR TypeError: Cannot read property 'domain' of undefined
Let me know whats wrong with the current implementing or appreciate you all to provide some refrences
// Get Stacked chart data
let stackedJsonData = this.getStackChartData.data;
var data = Array();
// Loop to iterate the JSON to fetch stacked chart data
for (let k = 0; k < stackedJsonData.length; k++) {
var objItem = {};
var key_name = Object.keys(stackedJsonData[k])[0];
objItem["State"] = key_name;
var objArray = stackedJsonData[k][key_name];
for (var i = 0; i < objArray.length; i++) {
var keyNm = "id" + (i + 1);
objItem[keyNm.toString()] = objArray[i];
}
data.push(objItem);
}
let keys = Object.getOwnPropertyNames(data[0]).slice(1);
data = data.map(v => {
v.total = keys.map(key => v[key]).reduce((a, b) => a + b, 0);
return v;
});
data.sort((a: any, b: any) => b.total - a.total);
this.x.domain(data.map((d: any) => d.State));
this.y.domain([0, d3Array.max(data, (d: any) => d.total)]).nice();
this.z.domain(keys);
this.g
.append("g")
.selectAll("g")
.data(d3Shape.stack().keys(keys)(data))
.enter()
.append("g")
.attr("fill", d => this.z(d.key))
.selectAll("rect")
.data(d => d)
.enter()
.append("rect")
.attr("x", d => this.x(d.data.State))
.attr("y", d => this.y(d[1]))
.attr("height", d => this.y(d[0]) - this.y(d[1]))
.attr("width", this.x.bandwidth());
// Draw stacked chart x-axis
this.g
.append("g")
.attr("class", "axis")
.attr("transform", "translate(18," + this.height + ")")
.attr("color", "#ebecf5")
.call(d3Axis.axisBottom(this.x));
//Draw stacked chart y-axis
this.g
.append("g")
.attr("class", "axis")
.attr("color", "#ebecf5")
.call(d3Axis.axisLeft(this.y).ticks(null, "s"))
.append("text")
.attr("x", 2)
.attr("y", this.y(this.y.ticks().pop()) + 0.5);
// Display total value on top of stacked bar
this.g
.selectAll("g")
.data(d3Shape.stack().keys(keys)(data))
.enter()
.attr("fill", d => this.z(d.key))
.append("text")
.data(d => d)
.attr("class", "yAxis-label")
.attr("fill", "#70747a")
.attr("text-anchor", "middle")
.attr("x", d => this.x(d.data.State))
.attr("y", d => this.y(d[1]) - 5)
.text(d => d.data.State);
As the error message says, either this.x, this.y, or this.z is undefined. Do you instantiate them somewhere, like in a constructor?
I'm trying to make a force directed graph, where nodes and links are added and removed as needed. However, while the chart correctly updates with added/removed links and nodes, all the old nodes are still visible.
Here is the update function. I've tried various tutorials, re-arranged the code, double checked the data being update is correct (i.e. this.dataNodes is being mutated, but replacing the object completely doesn't work either), etc. Honestly don't know what I should be looking for anymore.
// ADDING LINKS
this.link = this.linkGroup.selectAll("path")
.data(this.dataLinks, (link) => {
return link.target.id + link.source.id
});
this.link.exit().remove();
const linkEnter = this.link.enter()
.append("path")
.attr("stroke-width", 2)
.style("stroke", "#ccc")
.style('marker-start', (d) => d.sync ? 'url(#start-arrow)' : '')
.style('marker-end', (d) => 'url(#end-arrow)');
this.link = linkEnter.merge(this.link);
// ADDING NODES
this.node = this.nodeGroup.selectAll(".nodes")
.data(this.dataNodes, function (node) { return node.id });
this.node.exit().remove();
const nodeEnter = this.node.enter()
.append("g")
.call(this.dragAndDrop);
// Main circle
nodeEnter.append("circle")
.attr("r", 10)
.attr("fill", "grey")
// ADDING CHARACTER NAMES
nodeEnter.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function (d) {return d.title;});
this.node = nodeEnter.merge(this.node);
this.simulation.nodes(this.dataNodes).on("tick", this.tickActions );
this.simulation.force('link').links(this.dataLinks);
this.simulation.alphaTarget(1).restart();
EDIT:
This code is called when the force graph is first created. this.updateSimulation is the function above and renders with no problems. Calling it again, all previously created nodes remain in the graph.
this.svg = d3.select('#relationship-chart')
.append('svg')
.attr('width', this.width)
.attr('height', this.height);
// GROUPS
this.linkGroup = this.svg.append("g").attr("class", "links");
this.nodeGroup = this.svg.append("g").attr("class", "nodes");
// MAIN SIMULATION
this.link_force = d3.forceLink()
.id(function(d) { return d.id; })
.distance(100);
this.simulation = d3.forceSimulation()
.force("link", this.link_force)
.force("charge", d3.forceManyBody().strength(-200))
.force('center', d3.forceCenter(this.width / 2, this.height / 2))
//.force('collide', d3.forceCollide(25))
.force("x", d3.forceX())
.force("y", d3.forceY())
.alphaTarget(1);
// MISC DEFINTIONS
this.dragAndDrop = d3.drag()
.on("start", this.dragstarted)
.on("drag", this.dragged)
.on("end", this.dragended);
// ADDING ARROWS
this.svg.append('svg:defs').append('svg:marker')
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 7)
.attr('markerWidth', 4)
.attr('markerHeight', 4)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#ccc');
this.svg.append('svg:defs').append('svg:marker')
.attr('id', 'start-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 1)
.attr('markerWidth', 4)
.attr('markerHeight', 4)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M10,-5L0,0L10,5')
.attr('fill', '#ccc');
this.updateSimulation();
Turns out I was selecting the parent elements class and not the children. I added a class to the nodes I created and this cleared up the problem.
Before:
this.node = this.nodeGroup.selectAll(".nodes")
.data(this.dataNodes, function (node) { return node.id });
this.node.exit().remove();
const nodeEnter = this.node.enter()
.append("g")
.call(this.dragAndDrop);
After:
this.node = this.nodeGroup.selectAll(".onenode")
.data(this.dataNodes, (node) => { return node.id });
this.node.exit().remove();
const nodeEnter = this.node.enter()
.append("g")
.attr("class", "onenode")
.call(this.dragAndDrop);
I'm trying to make a map exactly like this example ( http://bost.ocks.org/mike/map/ ) except focused on Australia and New Zealand.
I've followed the instructions but the dots for the places don't render on my map.
This is how I'm generating my data:
ogr2ogr -f GeoJSON -where "adm0_a3 IN ('AUS', 'NZL')" subunits.json ne_10m_admin_0_map_subunits.shp
ogr2ogr -f GeoJSON -where "(iso_a2 = 'AU' OR iso_a2 = 'NZ') AND SCALERANK < 8" places.json ne_10m_populated_places.shp
topojson --id-property su_a3 -p name=NAME -p name -o aus.json subunits.json places.json
Here is the code I've got so far: http://bashsolutions.com.au/australia.html
The map shows up but the dots for the cities are not displaying. What am I doing wrong?
EDIT: So this isn't very clear just with the big long error so here's the actual code:
<script>
var width = 960,
height = 1160;
//var subunits = topojson.object(aus, aus.objects.subunitsAUS);
var projection = d3.geo.mercator()
//.center([0,0])
.center([180,-40])
.scale(400)
//.translate([width / 2, height / 2])
.precision(.1);
var path = d3.geo.path()
.projection(projection)
.pointRadius(2);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("aus.json", function(error, aus) {
svg.selectAll(".subunit")
.data(topojson.object(aus, aus.objects.subunits).geometries)
.enter().append("path")
.attr("class", function(d) { return "subunit " + d.id; })
.attr("d", path);
svg.append("path")
.datum(topojson.mesh(aus, aus.objects.subunits, function(a,b) { return a !== b; }))
.attr("d", path)
.attr("class", "subunit-boundary");
svg.append("path")
.datum(topojson.mesh(aus, aus.objects.subunits, function(a,b) { return a == b; }))
.attr("d", path)
.attr("class", "subunit-boundary External");
/* This is the failing bit */
svg.append("path")
.datum(topojson.object(aus, aus.objects.places))
.attr("class", "place")
.attr("d", path);
/* End of failing bit */
/*
svg.selectAll(".place-label")
.data(topojson.object(aus, aus.objects.places).geometries)
.enter().append("text")
.attr("class", "place-label")
.attr("transform", function(d) { return "translate(" + projection(d.coordinates) + ")"; })
.attr("x", function(d) { return d.coordinates[0] > -1 ? 6 : -6; })
.attr("dy", ".35em")
.style("text-anchor", function(d) { return d.coordinates[0] > -1 ? "start" : "end"; })
.text(function(d) { return d.properties.name; });
*/
});
When you plot the outline you need to remove the TKL (Tokelau) data points.
svg.append("path")
.datum(topojson.mesh(aus, aus.objects.subunits, function(a, b) {
return a === b && a.id !=="TKL" }))
.attr("d", path)
.attr("class", "subunit-boundary External");
I'm still researching why this creates the error, but adding that condition to the mesh function filter seems to fix things.
I found away around this issue that solves the problem but it still doesn't explain why it was failing in the first place.
Here is the fix: http://bashsolutions.com.au/australia2.html
This chunk of code replaces the failing bit above:
svg.selectAll(".place")
.data(topojson.object(aus, aus.objects.places).geometries)
.enter().append("circle")
.attr("d", path)
.attr("transform", function(d) { return "translate(" + projection(d.coordinates) + ")"; })
.attr("r", 2)
.attr("class", "place");
So this gets around it by doing something similar to the labels bit (which is commented out above) but drawing a circle instead of text.
But I'm still not sure what was wrong with the above bit considering it's the same as Mike Bostock's example (apart from the data).