Javascript button onclick event to call function inside function in module - javascript

I'm drawing and coloring lines based on data with D3.js and want to update their colors when clicking a button. My question is: how can I call colorP1() and colorP2(), declared in function drawLines in drawLines.js, from the onclick event of one of the buttons in index.html?
I have tried:
using the window.drawLines = drawLines trick and have the onclick event refer to window.drawLines.colorP2(), but I get Uncaught TypeError: colorP2 is not a function
using window.colorP2 = colorP2, but I don't know how the import would work in this case
Any ideas to enlighten the mind of this humble beginner? As I understand it, colorP1() and colorP2() have to stay inside drawLines() because they need the data and lines variables from drawLines()--feel free to prove me wrong here.
index.html
<html>
<head>
<style>
.line {
stroke-width: 4px;
fill: none;
}
</style>
</head>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script type="module">
import {drawLines} from './drawLines.js';
d3.json("test.geojson").then(drawLines);
</script>
<body>
<svg id='map'></svg>
<button onclick="colorP1()">colorP1</button>
<button onclick="colorP2()">colorP2</button>
</body>
</html>
drawLines.js
function colorInterpolate(data, property) {
let max_d = d3.max(data.features.map(d => d.properties[property]));
let range = [max_d, 1];
return d3.scaleSequential().domain(range).interpolator(d3.interpolateViridis);
}
export function drawLines(data) {
let width = 900,
height = 500,
initialScale = 1 << 23,
initialCenter = [-74.200698022608137, 40.034504451003734]
let svg = d3.select('#map')
.attr('height', height)
.attr('width', width)
let projection = d3.geoMercator()
.scale(initialScale)
.center(initialCenter)
.translate([width / 2, height / 2])
let path = d3.geoPath(projection)
let myColor = colorInterpolate(data, 'p1');
let lines = svg.append('g')
lines.selectAll('path')
.data(data.features)
.join('path')
.attr('class', 'line')
.attr('d', path)
.attr("stroke", function(d) {
return myColor(d.properties.p1);
})
function colorP2() {
let myColor = colorInterpolate(data, 'p2');
lines.selectAll('path')
.attr("stroke", d => myColor(d.properties.p2))
}
function colorP1() {
let myColor = colorInterpolate(data, 'p1');
lines.selectAll('path')
.attr("stroke", d => myColor(d.properties.p1))
}
}
test.geojson
{
"type": "FeatureCollection",
"name": "lines",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
{ "type": "Feature", "properties": { "id": 3, "p1": 1, "p2": 3}, "geometry": { "type": "LineString", "coordinates": [ [ -74.201304101157845, 40.033790926216739 ], [ -74.201226425025339, 40.033761910802717 ], [ -74.201164135201353, 40.033738641825124 ] ] } },
{ "type": "Feature", "properties": { "id": 4, "p1": 2, "p2": 2}, "geometry": { "type": "LineString", "coordinates": [ [ -74.200521185229846, 40.034804885753857 ], [ -74.200535458528648, 40.034780636493231 ], [ -74.200698022608137, 40.034504451003734 ], [ -74.200932444446437, 40.034106179618831 ], [ -74.201017665586349, 40.033961391736824 ] ] } }
]
}

Your assumption is wrong:
As I understand it, colorP1() and colorP2() have to stay inside drawLines() because they need the data and lines variables from drawLines()
D3 binds data to the elements entered with .data(data).join() or .data(data).enter(). The datum is attached to the node. When using .attr("something",function(d) { the d refers to the bound datum, not the original data array. So, you don't need the original data array, it is part of the DOM element.
Also, you don't need lines because you can remake that selection: d3.selectAll("paths") or d3.selectAll(".line").
So, you can move the p1/p2 functions outside of your drawLines function.
As I wanted to simplify for the snippet below, I've got a function that is passed data to draw some circles. I then assign event listeners to the buttons (I could also use onclick="" attributes on the buttons directly) with D3 to call functions that recolor the circles:
function color1() {
d3.selectAll("circle")
.attr("fill",d=>d.color1);
}
The function access the bound datum and a given property (d=>d.color1) and by using d3.selectAll() we can select all the circles that exist at the time of the click:
function draw(data) {
var svg = d3.select("body")
.append("svg")
.attr("width", 300)
.attr("height", 200);
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx",d=>d.x)
.attr("cy",d=>d.y)
.attr("fill",d=>d.color2)
.attr("r", 20);
}
draw([{x: 100,y:50, color1: "steelblue",color2:"crimson"},{x:200,y:50,color1:"steelblue",color2:"crimson"}])
d3.selectAll("button")
.data([0,1])
.on("click", function(event,d) {
if (d) color2();
else color1();
})
function color1() {
d3.selectAll("circle")
.attr("fill",d=>d.color1);
}
function color2() {
d3.selectAll("circle")
.attr("fill",d=>d.color2);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<div>
<button> Blue </button>
<button> Red </button>
</div>
If you need the data array itself, you can extract that with d3.selectAll("elements").data()
Of course, we could also append the buttons in your drawLines function, which would potentially make a cleaner outcome, especially if the buttons are dependent on the data in any form. This way if you ever wanted to change the buttons or the functions, everything is in one place, for example:
var geojson = { "type": "FeatureCollection","name": "lines","crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },"features": [{ "type": "Feature", "properties": { "id": 3, "p1": 1, "p2": 3}, "geometry": { "type": "LineString", "coordinates": [ [ -74.201304101157845, 40.033790926216739 ], [ -74.201226425025339, 40.033761910802717 ], [ -74.201164135201353, 40.033738641825124 ] ] } },{ "type": "Feature", "properties": { "id": 4, "p1": 2, "p2": 2}, "geometry": { "type": "LineString", "coordinates": [ [ -74.200521185229846, 40.034804885753857 ], [ -74.200535458528648, 40.034780636493231 ], [ -74.200698022608137, 40.034504451003734 ], [ -74.200932444446437, 40.034106179618831 ], [ -74.201017665586349, 40.033961391736824 ] ] } }]};
function drawLines(data) {
let width = 500,
height = 400,
initialScale = 1 << 23,
initialCenter = [-74.200698022608137, 40.034504451003734]
let svg = d3.select('#map')
.attr('height', height)
.attr('width', width)
let projection = d3.geoMercator()
.fitSize([width,height],data)
let path = d3.geoPath(projection)
let myColor = colorInterpolate(data, 'p1');
let lines = svg.append('g')
lines.selectAll('path')
.data(data.features)
.join('path')
.attr('class', 'line')
.attr('d', path)
colorBy("p1");
function colorBy(property) {
let myColor = colorInterpolate(property);
lines.selectAll('path')
.attr("stroke", d => myColor(d.properties[property]))
}
function colorInterpolate(property) {
let max_d = d3.max(data.features.map(d => d.properties[property]));
let range = [max_d, 1];
return d3.scaleSequential().domain(range).interpolator(d3.interpolateViridis);
}
d3.selectAll(".property")
.data(["p1","p2"])
.enter()
.append("button")
.attr("class","property")
.text(d=>d)
.on("click", function(_,d) {
colorBy(d);
})
.lower();
}
drawLines(geojson);
.line {
stroke-width: 4px;
fill: none;
}
<script src="https://d3js.org/d3.v6.min.js"></script>
<svg id='map'></svg>

You can call like this to call inner functions:
<button onclick="(new drawLines().colorP1())">colorP1</button>
<button onclick="(new drawLines().colorP2())">colorP2</button>

and work example for continue working...
var json1 ='{ "type" : "FeatureCollection", "name":"lines", "crs": { "type": "name", "properties":{ "name":"urn:ogc:def:crs:OGC:1.3:CRS84" }}, "features" : [{ "type" : "Feature", "properties" : { "id" : 3, "p1" : 1, "p2": 3}, "geometry" : {"type" : "LineString","coordinates":[[ -74.201304101157845, 40.033790926216739],[-74.201226425025339,40.033761910802717 ],[-74.201164135201353,40.033738641825124]]}},{"type": "Feature","properties":{ "id" : 4, "p1" : 2, "p2" :2 },"geometry" : { "type": "LineString", "coordinates" : [[ -74.200521185229846, 40.034804885753857 ],[ -74.200535458528648, 40.034780636493231 ],[ -74.200698022608137, 40.034504451003734 ],[ -74.200932444446437, 40.034106179618831 ],[ -74.201017665586349, 40.033961391736824 ]]}}]}';
var width = 900,
height = 500,
initialScale = 1 << 23,
initialCenter = [-74.198698022608137, 40.034504451003734]
var svg = d3.select('#map')
.attr('height', height)
.attr('width', width);
var lines = svg.append('g');
var projection = d3.geoMercator()
.scale(initialScale)
.center(initialCenter)
.translate([width / 2, height / 2])
var path = d3.geoPath(projection)
function colorInterpolate(data, property) {
let max_d = d3
.max(data.features.map(d => d.properties[property]));
let range = [max_d, 1];
return d3.scaleSequential()
.domain(range)
.interpolator(d3.interpolateViridis);
}
function drawLines(data) {
let myColor = colorInterpolate(data, 'p1');
lines.selectAll('path')
.data(data.features)
.join('path')
.attr('class', 'line')
.attr('d', path)
.attr("stroke", function(d) {
return myColor(d.properties.p1);
});
}
function colorP2(data){
let myColor = colorInterpolate(data, 'p2');
lines.selectAll('path')
.attr("stroke", d=>myColor(d.properties.p2));
}
function colorP1(data){
let myColor = colorInterpolate(data, 'p1');
lines.selectAll('path')
.attr("stroke", d=>myColor(d.properties.p1));
}
<html>
<head>
<style>
.line {
stroke-width: 4px;
fill: none;
}
</style>
</head>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script type="module">
//import {drawLines} from './drawLines.js';
//d3.json("test.geojson").then(drawLines);
drawLines(JSON.parse(json1));
</script>
<body>
<svg id='map'></svg>
<button onclick="colorP1(JSON.parse(json1))">colorP1</button>
<button onclick="colorP2(JSON.parse(json1))">colorP2</button>
</body>
</html>

Related

d3js Force Layout: null values for node and link positions

I am currently working through this d3js force layout tutorial, with some custom modifications. Mostly, my interest is to introduce an abstraction layer between my back-end and d3.js visualization, which I am doing using JS classes. Right now I am mostly throwing away some attributes I have in my mock .json file, but this logic could grow to be more complex.
Here is a MWE that I cannot seem to get to work:
index.html:
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<head>
</head>
<body>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="js/model.js"></script>
<script src="js/viz.js"></script>
</body>
</html>
model.js:
class Node {
constructor(id, name) {
this.id = id;
this.name = name;
this.outEdges = {};
this.inEdges = {};
}
}
class Edge {
constructor(node1, node2) {
this.source = node1;
this.target = node2;
}
}
class Graph {
constructor(nodes, edges) {
this.nodes = {}
for (let node of nodes) {
this.nodes[node.id] = node;
}
for (let edge of edges) {
this.nodes[edge.source].outEdges[edge.target] = edge;
this.nodes[edge.target].inEdges[edge.source] = edge;
}
this.edges = edges;
}
}
viz.js:
function loadJson() {
path = "mockup2.json";
$.getJSON(path, function(json) {
var nodes = [];
var edges = [];
for (let node of json.nodes) {
var temp = new Node(node.index, node.name);
nodes.push(temp);
}
for (let edge of json.edges) {
var temp = new Edge(edge.node1, edge.node2);
edges.push(temp);
}
var graph = new Graph(nodes, edges);
drawGraph(graph);
});
}
function drawGraph(graph) {
width = "90%";
height = "90%";
// set up svg and layout
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.gravity(.1)
.distance(20)
.charge(-30)
.size([width, height]);
// prep nodes and links for d3 viz
var d3nodes = [];
var temp = {};
for (var key in graph.nodes) {
node = graph.nodes[key]
temp = {}
temp.x = Math.random() * 100;
temp.y = Math.random() * 100;
temp.index = node.id;
d3nodes.push(temp);
}
d3edges = [];
var temp = {}
for (var key in graph.edges) {
edge = graph.edges[key];
temp = {}
temp.source = edge.source;
temp.target = edge.target;
temp.timediff = edge.timediff;
d3edges.push(temp);
}
// draw links and nodes
var svglinks = svg.selectAll('.link')
.data(d3edges)
.enter().append('line')
.attr('class', 'link');
var svgnodes = svg.selectAll('.node')
.data(d3nodes)
.enter().append('g')
.attr('class', 'node')
.call(force.drag);
force.nodes(d3nodes)
.links(d3edges)
.linkDistance(width/3)
.start();
// test svglinks values
svglinks.forEach(function(d) {
console.log(JSON.stringify(d));
});
force.on("tick", function() {
svglinks.attr("x1", function(d) {
console.log(JSON.stringify(d));
return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
svgnodes.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
// avoid console spam
force.stop();
});
}
$(document).ready(function() {
loadJson();
});
mockup2.json:
{
"nodes": [
{"index": 0, "name": 0, "timestamp": 1, "group": 1},
{"index": 1, "name": 0, "timestamp": 0, "group": 2},
{"index": 2, "name": 0, "timestamp": 0, "group": 3},
{"index": 3, "name": 0, "timestamp": 2, "group": 1},
{"index": 4, "name": 0, "timestamp": 0, "group": 2},
{"index": 5, "name": 0, "timestamp": 3, "group": 3},
{"index": 6, "name": 0, "timestamp": 2, "group": 3},
{"index": 7, "name": 0, "timestamp": 5, "group": 2},
{"index": 8, "name": 0, "timestamp": 8, "group": 2},
{"index": 9, "name": 0, "timestamp": 3, "group": 1}
],
"edges": [
{"node1": 1, "node2": 2},
{"node1": 2, "node2": 4},
{"node1": 4, "node2": 8},
{"node1": 4, "node2": 8},
{"node1": 8, "node2": 2},
{"node1": 5, "node2": 9}
]
}
The most damning thing to me is that the svglinks variable seems to be fine before outside of the force.on('tick') stuff, displaying sane values for x, y, px and py - I have included a console.log() call for you to check this too. However, printing the very same object inside of that function shows me null values for all these attributes, which make it possible for d3.js to render the graph correctly (at least in my intuition).
I also have a more "standard" example which uses the more standard d3.json() call to load a .json file which is preformatted with source and target attributes; this works perfectly. So my intuition is that I am not properly building the d3nodes and d3links variables above; however, comparing them across examples has shown me that they look exactly the same. I am a little dumbfounded here, I will admit!
Thanks in advance to everyone who is going to take the time to give this a go.

Are directed graphs only possible with files that contains a "source" and "target" value?

I got one question about the force-directed layout in d3.js. I try to understand if directed graphs are only possible with an JSON file, which contains a "source" and "target" value? I ask this, because i am using the hierarchy-layout for force-directed graphs and i could not find any examples with directed graphs AND hierarchy-layout, so i try to understand other examples and write my own.
Usually, hierarchy has this form (no "source" and "target"):
"name": "frank",
"children": [
{
"name": "tim"
},
{
"name": "max",
"children": [
{
"name": "alex"
},
{
"name": "martin"
},
.....
Based on this example: http://bl.ocks.org/jhb/5955887
i would like to know, where is the line that makes clear how to set the arrows right (from source to value and not the other way). I copied and pasted the most important parts of the code and deleted some unimportant parts:
var dataset = {
nodes: [
{name: "Adam"},
{name: "Bob"},
{name: "Carrie"},
{name: "Donovan"},
{name: "Edward"},
...
],
edges: [
{source: 0, target: 1},
{source: 0, target: 2},
{source: 0, target: 3},
{source: 0, target: 4},
{source: 1, target: 5},
{source: 2, target: 5},
.....
]
};
var svg = d3.select("body").append("svg").attr({"width":w,"height":h});
var force = d3.layout.force()
.nodes(dataset.nodes)
.links(dataset.edges)
.size([w,h])
.linkDistance([linkDistance])
.charge([-500])
.theta(0.1)
.gravity(0.05)
.start();
var edges = svg.selectAll("line")
.data(dataset.edges)
.enter()
.append("line")
.attr("id",function(d,i) {return 'edge'+i})
.attr('marker-end','url(#arrowhead)')
.style("stroke","#ccc")
.style("pointer-events", "none");
var nodes = svg.selectAll("circle")
.data(dataset.nodes)
.enter()
.append("circle")
.attr({"r":15})
.style("fill",function(d,i){return colors(i);})
.call(force.drag)
var edgepaths = svg.selectAll(".edgepath")
.data(dataset.edges)
.enter()
.append('path')
.attr({'d': function(d) {return 'M '+d.source.x+' '+d.source.y+' L '+ d.target.x +' '+d.target.y},
'class':'edgepath',
'fill-opacity':0,
'stroke-opacity':0,
'fill':'blue',
'stroke':'red',
'id':function(d,i) {return 'edgepath'+i}})
.style("pointer-events", "none");
svg.append('defs').append('marker')
.attr({'id':'arrowhead',
'viewBox':'-0 -5 10 10',
'refX':25,
'refY':0,
//'markerUnits':'strokeWidth',
'orient':'auto',
'markerWidth':10,
'markerHeight':10,
'xoverflow':'visible'})
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#ccc')
.attr('stroke','#ccc');
You can use your hierarchical data array to create a force directed chart, but first you'll have to modify that array to populate your nodes and links array.
So, given this data array:
var data = {
"name": "Frank",
"children": [{
"name": "Tim"
}, {
"name": "Max",
"children": [{
"name": "Alex"
}, {
"name": "Martin"
}]
}, {
"name": "Tom",
"children": [{
"name": "Murphy"
}, {
"name": "Amanda"
}]
}]
};
You can use this function to populate the nodes:
function flatten(hierarchyArray) {
var nodes = [],
i = 0;
function recurse(node) {
if (node.children) node.children.forEach(recurse);
if (!node.id) node.id = ++i;
nodes.push(node);
}
recurse(hierarchyArray);
return nodes;
};
And to populate the links:
links = d3.layout.tree().links(nodes);
That way, you can have a force directed chart without explicitly setting "target" and "source".
Check the demo (the first console.log is for the nodes, the second one is for the links):
var data = {
"name": "Frank",
"children": [{
"name": "Tim"
}, {
"name": "Max",
"children": [{
"name": "Alex"
}, {
"name": "Martin"
}]
}, {
"name": "Tom",
"children": [{
"name": "Murphy"
}, {
"name": "Amanda"
}]
}]
};
var nodes = flatten(data);
var links = d3.layout.tree().links(nodes);
console.log(nodes);
console.log(links);
function flatten(hierarchyArray) {
var nodes = [], i = 0;
function recurse(node) {
if (node.children) node.children.forEach(recurse);
if (!node.id) node.id = ++i;
nodes.push(node);
}
recurse(hierarchyArray);
return nodes;
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
(source of the flatten function: this bl.ocks from Bostock)

d3 - dougnut bubble pie chart

I am trying to create a merged pie/bubble chart.
-- looking for a starting bubble chart base - maybe this one.
http://jsfiddle.net/xsafy/
^ need to cluster these bubbles - maybe sub bubbles per slice.
//bubble chart base.
http://jsfiddle.net/NYEaX/1450/
(function() {
var diameter = 250;
var svg = d3.select('#graph').append('svg')
.attr('width', diameter)
.attr('height', diameter);
var bubble = d3.layout.pack()
.size([diameter, diameter])
.value(function(d) {
return d.size;
})
.padding(3);
var color = d3.scale.ordinal()
.domain(["Lorem ipsum", "dolor sit", "amet", "consectetur", "adipisicing"])
.range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56"]);
function randomData() {
var data1 = {
"children": [
{
name: "AA",
className: "aa",
size: 170
},
{
name: "BB",
className: "bb",
size: 393
},
{
name: "CC",
className: "cc",
size: 293
},
{
name: "DD",
className: "dd",
size: 89
}
]
};
var data2 = {
"children": [
{
name: "AA",
className: "aa",
size: 120
},
{
name: "BB",
className: "bb",
size: 123
},
{
name: "CC",
className: "cc",
size: 193
},
{
name: "DD",
className: "dd",
size: 289
}
]
};
var j = Math.floor((Math.random() * 2) + 1);
console.log("j", j);
if (j == 1) {
return data1;
} else {
return data2;
}
}
change(randomData());
d3.select(".randomize")
.on("click", function() {
change(randomData());
});
function change(data) {
console.log("data", data);
// generate data with calculated layout values
var nodes = bubble.nodes(data)
.filter(function(d) {
return !d.children;
}); // filter out the outer bubble
var vis = svg.selectAll('circle')
.data(nodes);
vis.enter()
.insert("circle")
.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
})
.attr('r', function(d) {
return d.r;
})
.style("fill", function(d) {
return color(d.name);
})
.attr('class', function(d) {
return d.className;
});
vis
.transition().duration(1000)
vis.exit()
.remove();
};
})();
//doughnut chart base.
derived from these examples.
http://bl.ocks.org/dbuezas/9306799
https://bl.ocks.org/mbostock/1346410
http://jsfiddle.net/NYEaX/1452/
var svg = d3.select("#graph")
.append("svg")
.append("g")
svg.append("g")
.attr("class", "slices");
svg.append("g")
.attr("class", "labels");
svg.append("g")
.attr("class", "lines");
var width = 560,
height = 450,
radius = Math.min(width, height) / 2;
var pie = d3.layout.pie()
.sort(null)
.value(function(d) {
return d.value;
});
var arc = d3.svg.arc()
.outerRadius(radius * 0.85)
.innerRadius(radius * 0.83);
var outerArc = d3.svg.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9);
svg.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var key = function(d) {
return d.data.label;
};
var color = d3.scale.ordinal()
.domain(["Lorem ipsum", "dolor sit", "amet", "consectetur", "adipisicing"])
.range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56"]);
function randomData() {
var data1 = [{
"label": "AA",
"value": 0.911035425558026
}, {
"label": "BB",
"value": 0.08175111844879179
}, {
"label": "CC",
"value": 0.25262439557273275
}, {
"label": "DD",
"value": 0.8301366989535612
}, {
"label": "EE",
"value": 0.0517762265780517
}];
var data2 = [{
"label": "AA",
"value": 0.243879179
}, {
"label": "BB",
"value": 0.243879179
}, {
"label": "CC",
"value": 0.2342439557273275
}, {
"label": "DD",
"value": 0.2349535612
}, {
"label": "EE",
"value": 0.2345780517
}];
var j = Math.floor((Math.random() * 2) + 1);
if (j == 1) {
return data1;
} else {
return data2;
}
}
change(randomData());
d3.select(".randomize")
.on("click", function() {
change(randomData());
});
function change(data) {
/* ------- PIE SLICES -------*/
var slice = svg.select(".slices").selectAll("path.slice")
.data(pie(data), key);
slice.enter()
.insert("path")
.style("fill", function(d) {
return color(d.data.label);
})
.attr("class", "slice");
slice
.transition().duration(1000)
.attrTween("d", function(d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc(interpolate(t));
};
})
slice.exit()
.remove();
};
This is as close I have got so far.. I've merged the two charts together as such - although there is a bug still trying to update the bubbles -- at least get them to scale/morph/move/animate. Ideally I want them to stick close to their group segments..
here is the fiddle.
https://jsfiddle.net/tk5xog0g/1/
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>doughpie</title>
<link rel="stylesheet" href="css/generic.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<button class="randomize">randomize</button>
<div data-role="doughpie" data-width="450" data-height="450" id="graph"></div>
<script>
//__invoke pie chart
$('[data-role="doughpie"]').each(function(index) {
createDoughnut(this);
});
function bubbledata(data){
//loop through data -- and MERGE children
var childs = [];
$.each(data, function( index, value ) {
childs.push(value.children);
});
var merged = [].concat.apply([], childs);//flatterns multidimensional array
return $.extend(true, {}, {"children": merged});// return deep clone
}
function createDoughnut(el){
var width = $(el).data("width"),
height = $(el).data("height"),
radius = Math.min(width, height) / 2;
var svg = d3.select($(el)[0])
.append("svg")
.attr("width", width)
.attr("height", height)
//_create doughpie shell
var doughpie = svg.append("g")
.attr("class", "doughpie");
doughpie.append("g")
.attr("class", "slices");
doughpie.append("g")
.attr("class", "labels");
doughpie.append("g")
.attr("class", "lines");
var pie = d3.layout.pie()
.sort(null)
.value(function(d) {
return d.value;
});
var arc = d3.svg.arc()
.outerRadius(radius * 0.85)
.innerRadius(radius * 0.83);
var outerArc = d3.svg.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9);
doughpie.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var key = function(d) {
return d.data.label;
};
var color = d3.scale.ordinal()
.range(["#46a2de", "#7b3cce", "#31d99c", "#de5942", "#ffa618"]);
//_create doughpie shell
//_create bubble
var diameter = width/2;//take half/width
var bubs = svg.append("g")
.attr("class", "bubs");
bubs.attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")");
var bubble = d3.layout.pack()
.size([diameter, diameter])
.value(function(d) {
return d.size;
})
.padding(3);
//_create bubble
function randomData() {
var data1 = [{
"label": "AA",
"value": 0.911035425558026,
"children": [
{
name: "some text aa",
group: "AA",
size: 120
}
]
}, {
"label": "BB",
"value": 0.08175111844879179,
"children": [
{
name: "some text bb",
group: "BB",
size: 123
}
]
}, {
"label": "CC",
"value": 0.25262439557273275,
"children": [
{
name: "some text cc",
group: "CC",
size: 193
}
]
}, {
"label": "DD",
"value": 0.8301366989535612,
"children": [
{
name: "some text dd",
group: "DD",
size: 29
},
{
name: "some text dd",
group: "DD",
size: 289
}
]
}, {
"label": "EE",
"value": 0.0517762265780517,
"children": [
{
name: "some text ee",
group: "EE",
size: 389
},
{
name: "some text ee",
group: "EE",
size: 89
}
]
}];
var data2 = [{
"label": "AA",
"value": 0.243879179,
"children": [
{
name: "some text aa",
group: "AA",
size: 120
}
]
}, {
"label": "BB",
"value": 0.243879179,
"children": [
{
name: "some text bb",
group: "BB",
size: 123
}
]
}, {
"label": "CC",
"value": 0.2342439557273275,
"children": [
{
name: "some text cc",
group: "CC",
size: 193
}
]
}, {
"label": "DD",
"value": 0.2349535612,
"children": [
{
name: "some text dd",
group: "DD",
size: 29
},
{
name: "some text dd",
group: "DD",
size: 289
}
]
}, {
"label": "EE",
"value": 0.2345780517,
"children": [
{
name: "some text ee",
group: "EE",
size: 389
},
{
name: "some text ee",
group: "EE",
size: 89
}
]
}];
var j = Math.floor((Math.random() * 2) + 1);
if (j == 1) {
return data1;
} else {
return data2;
}
}
change(randomData());
d3.select(".randomize")
.on("click", function() {
change(randomData());
});
function change(data) {
/* ------- ANIMATE PIE SLICES -------*/
var slice = doughpie.select(".slices").selectAll("path.slice")
.data(pie(data), key);
slice.enter()
.insert("path")
.style("fill", function(d) {
return color(d.data.label);
})
.style("transform", function(d, i){
//return "translate(0, 0)";
})
.attr("class", "slice");
slice
.transition().duration(1000)
.attrTween("d", function(d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc(interpolate(t));
};
})
slice.exit()
.remove();
/* ------- ANIMATE PIE SLICES -------*/
/* ------- ANIMATE BUBBLES -------*/
// generate data with calculated layout values
var data = bubbledata(data);
var nodes = bubble.nodes(data)
.filter(function(d) {
return !d.children;
}); // filter out the outer bubble
var bubbles = bubs.selectAll('circle')
.data(nodes);
bubbles.enter()
.insert("circle")
.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
})
.attr('r', function(d) {
return d.r;
})
.style("fill", function(d) {
return color(d.group);
});
bubbles
.transition().duration(1000)
bubbles.exit()
.remove();
/* ------- ANIMATE BUBBLES -------*/
};
}
</script>
</body>
</html>
I thought I will try and first plot mid dot points on the arcs -- and maybe from THOSE -- start to get a batch of zone coordinates to master control the coloured bubbles.
I've been trying to follow this -- How to get coordinates of slices along the edge of a pie chart?
https://jsfiddle.net/tk5xog0g/28/
/*placing mid dots*/
var midDotsArrayCooridnates = [];
slice
.each(function(d) {
x = 0 + (radius * 0.85) * Math.cos(d.startAngle);
y = 0 + (radius * 0.85) * Math.sin(d.startAngle);
var obj = {
"x": x,
"y": y
}
midDotsArrayCooridnates.push(obj);
});
$.each(midDotsArrayCooridnates, function(index, value) {
var dot = doughpie.select(".slicedots").append('circle')
.attr('cx', value.x)
.attr('cy', value.y)
.attr('r', 5)
.style("fill", "red")
.attr('class', "ccc")
});
/*placing mid dots*/

d3.js - Text position based on text width

I'm working with d3.js to generate a visualization that represents different hypotheses. Since the hypotheses are made of different parts , each word / part gets its own text element.
I want to base the x-position of each text element on the text width of the previous word including an offset. Having a hypothesis "IF x THEN y" i would need 4 text elements with "IF" having x=0, and since "IF" has a width of 10 and i use an offset of 5 "x" will get x=15 and so on.
I'm using json data that could look like this:
{[
{"id" : "id0",
"elements" : [
{
"text" : "IF",
"type" : "conditional"
},
{
"text" : "X",
"type" : "variable"
},
{
"text" : "THEN",
"type" : "conditional"},
{
"text" : "Y",
"type" : "variable"
}
]},
{"id" : "id1",
"elements" : [
{
"text" : "IF",
"type" : "conditional"
},
{
"text" : "abc",
"type" : "variable"
},
{
"text" : "THEN",
"type" : "conditional"},
{
"text" : "xyz",
"type" : "variable"
}
]}
]}
The code i am using to generate the text elements so far (each hypothesis is in a g-element is
var svg = d3.select("#viewport")
.append("svg")
.attr("width", 1200)
.attr("height", 800);
var content = svg.append("g").attr("id", "drawing");
var groups = content.selectAll().data(arr)
.enter()
.append("g")
.attr("class", function (d) {
return "hypothesis " + d["id"];
})
.each(function (d, i) {
d3.select(this).selectAll("text")
.data(d["elements"])
.enter()
.append("text")
.attr("class", function (d) {
return d.type;
})
.text(function (d) {
return d.text;
})
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("x", function (d, j) {
return j++ * 100;
})
.attr("y", 50 * (i + 1));
});
When setting the x position i want to get the width of the current text element and push it onto a variable so i can get the next new x-coordinate instead of just using a currently random offset of 100 px per word.
So the question is how can i get the calculated text width (have seen things on getBBox or similar, but it didn't work for me since i don't know where to use them) and how to apply it to the text elements. Or if there is a better way to create the elements, maybe not in a single run.
The different elements need to be styled in different colors and have to react so mouse-over later, that's why they have to be single text elements.
Thanks in advance.
I always use getComputedTextLength for these sorts of things, although getBBox would also work:
.each(function(d, i) {
var runningWidth = 0; //<-- keep a running total
...
.attr("x", function(d, j) {
var w = this.getComputedTextLength(), //<-- length of this node
x = runningWidth; //<-- previous length to return
runningWidth += w; //<-- total
return x;
})
...
Full code:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.5.3" data-semver="3.5.3" src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body>
<div id="viewport"></div>
<script>
var arr =
[{
"id": "id0",
"elements": [{
"text": "IF",
"type": "conditional"
}, {
"text": "X",
"type": "variable"
}, {
"text": "THEN",
"type": "conditional"
}, {
"text": "Y",
"type": "variable"
}]
}, {
"id": "id1",
"elements": [{
"text": "IF",
"type": "conditional"
}, {
"text": "abc",
"type": "variable"
}, {
"text": "THEN",
"type": "conditional"
}, {
"text": "xyz",
"type": "variable"
}]
}];
var svg = d3.select("#viewport")
.append("svg")
.attr("width", 1200)
.attr("height", 800);
var content = svg.append("g").attr("id", "drawing");
var groups = content.selectAll().data(arr)
.enter()
.append("g")
.attr("class", function(d) {
return "hypothesis " + d["id"];
})
.each(function(d, i) {
var runningWidth = 0;
d3.select(this).selectAll("text")
.data(d["elements"])
.enter()
.append("text")
.attr("class", function(d) {
return d.type;
})
.text(function(d) {
return d.text;
})
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("x", function(d, j) {
var w = this.getComputedTextLength(),
x = runningWidth;
runningWidth += w;
return x;
})
.attr("y", 50 * (i + 1));
});
</script>
</body>
</html>

D3JS Force Layout new nodes disconnects existing nodes

When I add new nodes to D3's Force Layout, the new nodes ignore the previous nodes when positioning itself and the previous nodes becomes un-draggable. I feel I've followed the logic of:
Add elements to arrays nodes and links
Updated force.nodes(nodes) and force.links(links)
Ran through .data().enter() with new data
Called force.start()
But still results in previous nodes disconnects. The new nodes are draggable and appears to take into consideration the LAST SET of added nodes position and avoids collision, all other previous nodes are clickable still, but their positioning are ignored and not updated.
Here is a the code in PLNKR: http://plnkr.co/edit/5fXZf63s73cTO37zLjNQ?p=preview
var width = 1000;
var height = 600;
var node_w = 30;
var node_h = 30;
var text_dx = -20;
var text_dy = 20;
var new_id = 9;
var nodes = [],
links = [],
links_line,
node_circles;
var svg = d3.select("body").append("svg")
.attr("width",width)
.attr("height",height);
var nodes = [
{ "name": "Nucleus" , "size" : 25, "id" : 0 , "color":"#ac0000"},
{ "name": "one" , "size" : 5 , "id": 1 , "color": "#ac0"},
{ "name": "two" , "size" : 15 , "id": 2 , "color": "#ac0"},
{ "name": "three" , "size" : 25 , "id": 3 , "color": "#ac0"},
{ "name": "four" , "size" : 9 , "id": 4 , "color": "#ac0"},
{ "name": "five" , "size" : 12 , "id": 5 , "color": "#ac0"},
{ "name": "six" , "size" : 15 , "id": 6 , "color": "#ac0"},
{ "name": "seven" , "size" : 41 , "id": 7 , "color": "#ac0"},
{ "name": "eight" , "size" : 5 , "id": 8 , "color": "#ac0"}
];
var links = [
{ "source": 0 , "target": 1 , "link_info":"r01" },
{ "source": 1 , "target": 2 , "link_info":"r31" },
{ "source": 1 , "target": 3 , "link_info":"r02" },
{ "source": 1 , "target": 4 , "link_info":"r04" },
{ "source": 0 , "target": 5 , "link_info":"r05" },
{ "source": 0 , "target": 6 , "link_info":"r06" },
{ "source": 0 , "target": 7 , "link_info":"r87" },
{ "source": 0 , "target": 8 , "link_info":"r87" }
];
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([width, height])
.linkDistance(150)
.charge(-1400);
var drag = force.drag();
init();
function init() {
force.start();
links_line = svg.selectAll("line")
.data(links)
.enter()
.append("line")
.style("stroke", "#ac0")
.style("stroke-width", 1);
node_circles = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.style("fill", function(d) {return d.color;})
.on("dblclick", function(d, i) {
addNodes(i);
})
.call(drag);
draw();
}
function addNodes(i) {
for (c=0; c < Math.floor(Math.random() * 20) + 4; c++) {
nodes.push({"name": "new " + new_id,"size": (Math.floor(Math.random() * 20) + 10),"id": new_id,"color": "#333"})
links.push({"source": i,"target": new_id,"link_info": "r"+i+new_id});
new_id++;
}
// Update force.nodes
force.nodes(nodes);
// Update force.links
force.links(links);
// exec init()
init();
}
function draw() {
var ticksPerRender = 1;
requestAnimationFrame(function render() {
force.tick();
//Update nodes
node_circles.attr("cx", function(d) {return d.x - d.size / 6;});
node_circles.attr("cy", function(d) {return d.y - d.size / 6;});
node_circles.attr("r", function(d) {return d.size});
//Update Location line
links_line.attr("x1", function(d) {return d.source.x;});
links_line.attr("y1", function(d) {return d.source.y;});
links_line.attr("x2", function(d) {return d.target.x;});
links_line.attr("y2", function(d) {return d.target.y;});
requestAnimationFrame(render)
});
} // draw();
Updating a d3 visualization follows an enter, update, and exit workflow (start your reading here and here).
Try this instead:
function init() {
force.start();
links_line = svg.selectAll("line")
.data(links);
links_line
.enter()
.append("line")
.style("stroke", "#ac0")
.style("stroke-width", 1);
links_line.exit().remove();
node_circles = svg.selectAll("circle")
.data(nodes);
node_circles
.enter()
.append("circle")
.style("fill", function(d) {return d.color;})
.on("dblclick", function(d, i) {
addNodes(i);
})
.call(drag);
draw();
}
Updated example.

Categories