Related
I am trying to implement this gauge to show target and actual values.
Here the position of the target value '45%' is given by a fixed number, so that it always stays at the top of the gauge as shown in below image:
How do I make this label stick to the beginning of second arc dynamically, similar to this:
Here is a snippet of current code I am using with hardcoded translate values:
var barWidth, chart, chartInset, degToRad, repaintGauge,
height, margin, numSections, padRad, percToDeg, percToRad,
percent, radius, sectionIndx, svg, totalPercent, width;
percent = percentValue;
numSections = 1;
sectionPerc = 1 / numSections / 2;
padRad = 0.025;
chartInset = 10;
// Orientation of gauge:
totalPercent = .75;
el = d3.select('#HSFO');
margin = {
top: 12,
right: 12,
bottom: 0,
left: 12
};
width = el[0][0].offsetWidth - margin.left - margin.right;
height = width;
radius = Math.min(width, height) / 2;
barWidth = 40 * width / 300;
//Utility methods
percToDeg = function(perc) {
return perc * 360;
};
percToRad = function(perc) {
return degToRad(percToDeg(perc));
};
degToRad = function(deg) {
return deg * Math.PI / 180;
};
// Create SVG element
svg = el.append('svg').attr('width', width + margin.left + margin.right).attr('height', height + margin.top + margin.bottom);
// Add layer for the panel
chart = svg.append('g').attr('transform', "translate(" + ((width + margin.left) / 2) + ", " + ((height + margin.top) / 2) + ")");
chart.append('path').attr('class', "arc chart-first");
chart.append('path').attr('class', "arc chart-second");
chart.append('path').attr('class', "arc chart-third");
formatValue = d3.format('1%');
arc3 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
arc2 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
arc1 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
repaintGauge = function() {
perc = 17 / 20;
var next_start = totalPercent;
arcStartRad = percToRad(next_start);
arcEndRad = arcStartRad + percToRad(perc / 2);
next_start += perc / 2;
arc1.startAngle(arcStartRad).endAngle(arcEndRad);
perc = 1 - perc;
arcStartRad = percToRad(next_start);
arcEndRad = arcStartRad + percToRad(perc / 2);
next_start += perc / 2;
arc2.startAngle(arcStartRad + padRad).endAngle(arcEndRad);
chart.select(".chart-first").attr('d', arc1);
chart.select(".chart-second").attr('d', arc2);
svg.append("text").attr("transform", "translate("+(width + margin.left-35) +","+ (radius - chartInset - barWidth/4.5) +")" + 'rotate('+'70'+')')
.attr("text-anchor", "middle").style("font-size", "12").style("font-family", "Helvetica").text('17')
}
I would use a text path for the percent text and use an arc as the template so that you don't have to worry about manually transforming the text and calculating the angle. This means reorganising your elements slightly and using arc3 (currently unused) as the path for the text.
The general format for text on a path is:
<path id="path_for_text" d="M-150,1.8369701987210297e-14A150,150 0 0,1 18.799985034645633,-148.8172051971717L13.45243373590199,-106.4869779410873A107.33333333333334,107.33333333333334 0 0,0 -107.33333333333334,1.3144542310848258e-14Z"></path>
<text>
<textPath xlink:href="#path_for_text">my text here</textPath>
</text>
so the basic alterations that we'll need to do on your code are adding the new arc for the text to go along, and adding in the text path element. So, let's create an appropriate arc generator:
// we want the text to be offset slightly from the outer edge of the arc, and the arc
// itself can have identical inner and outer radius measurements
var arc3 = d3.svg.arc()
.outerRadius(radius - chartInset + 10)
.innerRadius(radius - chartInset + 10)
// add the text element and give it a `textPath` element as a child
var arc_text = chart.append('text')
.attr('id', 'scale10')
.attr("font-size", 15)
.style("fill", "#000000")
// the textPath element will use an element with ID `text_arc` to provide its shape
arc_text.append('textPath')
.attr('startOffset','0%')
.attr('xlink:href', '#text_arc' )
// add the path with the ID `text_arc`
chart.append('path').attr('class', "arc chart-third")
.attr('id', 'text_arc')
In repaintGauge, calculate the appropriate arc:
// append the path to the chart, using the arc3 constructor to generate the arc
// these numbers will be the same as those for arc2, although I would add a little
// padding to both start and end angles to ensure that the text doesn't wrap if it
// is at 0% or 100%
arc3.startAngle(arcStartRad - 0.15).endAngle(arcEndRad + 0.15);
chart.select('id', 'text_arc')
.attr('d', arc3)
and update the text:
arc_text.select('textPath')
.text( percent + '%')
You can refactor your repaintGauge function to make it significantly simpler as some of the arc figures don't change; arc1's startAngle will always be at 1.5 Pi radians, and arc2's endAngle will always be 2.5 Pi radians. That means you only need to work out what your percent is in terms of radians, which is pretty simple: if 0% is 1.5 Pi and 100% is 2.5 Pi, and you want to represent perc percent, it will be p / 100 * Math.PI + 1.5 * Math.PI.
repaintGauge = function(perc) {
var arcOffset = Math.PI * 1.5
var current = Math.PI * perc / 100 + arcOffset
// arc1's endAngle and arc2, arc3's endAngle can be set to `current`
arc1.startAngle(arcOffset).endAngle(current)
arc2.startAngle(current + padRad).endAngle(arcOffset + Math.PI)
arc3.startAngle(current - 0.15).endAngle(arcOffset + Math.PI + 0.15)
chart.select(".chart-first").attr('d', arc1);
chart.select(".chart-second").attr('d', arc2);
chart.select(".chart-third").attr('d', arc3);
arc_text.select('textPath').text(perc + '%');
};
Here's a demo showing the text at different positions and with different values:
var name = "Value";
var value = 17;
var gaugeMaxValue = 100;
// data to calculate
var percentValue = value / gaugeMaxValue;
////////////////////////
var needleClient;
(function() {
var barWidth, chart, chartInset, degToRad, repaintGauge,
height, margin, numSections, padRad, percToDeg, percToRad,
percent, radius, sectionIndx, svg, totalPercent, width;
percent = percentValue;
numSections = 1;
sectionPerc = 1 / numSections / 2;
padRad = 0.025;
chartInset = 10;
var percStart = 0;
var arcOffset = Math.PI * 1.5
// Orientation of gauge:
totalPercent = .75;
el = d3.select('.chart-gauge');
margin = {
top: 40,
right: 20,
bottom: 30,
left: 60
};
width = el[0][0].offsetWidth - margin.left - margin.right;
height = width;
radius = Math.min(width, height) / 2;
barWidth = 40 * width / 300;
//Utility methods
percToDeg = function(perc) {
return perc * 360;
};
percToRad = function(perc) {
return degToRad(percToDeg(perc));
};
degToRad = function(deg) {
return deg * Math.PI / 180;
};
// Create SVG element
svg = el.append('svg').attr('width', width + margin.left + margin.right).attr('height', height + margin.top + margin.bottom);
// Add layer for the panel
chart = svg.append('g').attr('transform', "translate(" + ((width + margin.left) / 2) + ", " + ((height + margin.top) / 2) + ")");
formatValue = d3.format('1%');
var arc3 = d3.svg.arc().outerRadius(radius - chartInset + 10).innerRadius(radius - chartInset + 10),
arc2 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth),
arc1 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
// bind angle data directly to the chart elements
chart.append('path').attr('class', "arc chart-first")
.datum({ startAngle: arcOffset, endAngle: arcOffset })
.attr('d', arc1)
chart.append('path').attr('class', "arc chart-second")
.datum({ startAngle: arcOffset, endAngle: arcOffset + padRad + Math.PI })
.attr('d', arc2)
chart.append('path').attr('class', "arc chart-third")
.attr('id', 'text_arc')
.datum({ startAngle: arcOffset - 0.15, endAngle: arcOffset + Math.PI + 0.15 })
.attr('d', arc3)
var arc_text = chart.append('text')
.attr('id', 'scale10')
.attr("font-size", 15)
.style("fill", "#000000")
.attr('text-anchor', 'start')
arc_text.append('textPath')
.attr('startOffset','0%')
.attr('xlink:href', '#text_arc' )
var dataset = [{
metric: name,
value: value
}]
var texts = svg.selectAll("text")
.data(dataset)
.enter();
texts.append("text")
.text(function() {
return dataset[0].metric;
})
.attr('id', "Name")
.attr('transform', "translate(" + ((width + margin.left) / 2) + ", " + ((height + margin.top) / 1.5) + ")")
.attr("font-size", 25)
.style("fill", "#000000");
texts.append("text")
.text(function() {
return dataset[0].value + "%";
})
.attr('id', "Value")
.attr('transform', "translate(" + ((width + margin.left) / 1.4) + ", " + ((height + margin.top) / 1.5) + ")")
.attr("font-size", 25)
.style("fill", "#000000");
texts.append("text")
.text(function() {
return 0 + "%";
})
.attr('id', 'scale0')
.attr('transform', "translate(" + ((width + margin.left) / 100) + ", " + ((height + margin.top) / 2) + ")")
.attr("font-size", 15)
.style("fill", "#000000");
texts.append("text")
.text(function() {
return gaugeMaxValue + "%";
})
.attr('id', 'scale20')
.attr('transform', "translate(" + ((width + margin.left) / 1.08) + ", " + ((height + margin.top) / 2) + ")")
.attr("font-size", 15)
.style("fill", "#000000");
repaintGauge = function(perc) {
var current = Math.PI * perc / 100 + arcOffset
var t = d3.transition().duration(500)
chart.select(".chart-first")
.transition(t)
.attrTween('d', arcEndTween(current, arc1));
chart.select(".chart-second")
.transition(t)
.attrTween('d', arcStartTween(current, arc2));
chart.select(".chart-third")
.transition(t)
.attrTween('d', arcStartTween(current, arc3) );
arc_text.select('textPath')
.text( perc.toFixed(1) + '%')
}
function arcStartTween(newAngle, arc) {
return function(d) {
var interpolate = d3.interpolate(d.startAngle, newAngle);
return function(t) {
d.startAngle = interpolate(t);
return arc(d);
};
};
}
function arcEndTween(newAngle, arc) {
return function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return arc(d);
};
};
}
/////////
var Needle = (function() {
//Helper function that returns the `d` value for moving the needle
var recalcPointerPos = function(perc) {
var centerX, centerY, leftX, leftY, rightX, rightY, thetaRad, topX, topY;
thetaRad = percToRad(perc / 2);
centerX = 0;
centerY = 0;
topX = centerX - this.len * Math.cos(thetaRad);
topY = centerY - this.len * Math.sin(thetaRad);
leftX = centerX - this.radius * Math.cos(thetaRad - Math.PI / 2);
leftY = centerY - this.radius * Math.sin(thetaRad - Math.PI / 2);
rightX = centerX - this.radius * Math.cos(thetaRad + Math.PI / 2);
rightY = centerY - this.radius * Math.sin(thetaRad + Math.PI / 2);
return "M " + leftX + " " + leftY + " L " + topX + " " + topY + " L " + rightX + " " + rightY;
};
function Needle(el) {
this.el = el;
this.len = width / 2.5;
this.radius = this.len / 8;
}
Needle.prototype.render = function() {
this.el.append('circle').attr('class', 'needle-center').attr('cx', 0).attr('cy', 0).attr('r', this.radius);
return this.el.append('path').attr('class', 'needle').attr('id', 'client-needle').attr('d', recalcPointerPos.call(this, 0));
};
Needle.prototype.moveTo = function(perc) {
var self,
oldValue = this.perc || 0;
this.perc = perc;
self = this;
// Reset pointer position
this.el.transition().delay(100).ease('quad').duration(200).select('.needle').tween('reset-progress', function() {
return function(percentOfPercent) {
var progress = (1 - percentOfPercent) * oldValue;
return d3.select(this).attr('d', recalcPointerPos.call(self, progress));
};
});
this.el.transition().delay(300).ease('bounce').duration(1500).select('.needle').tween('progress', function() {
return function(percentOfPercent) {
var progress = percentOfPercent * perc;
return d3.select(this).attr('d', recalcPointerPos.call(self, progress));
};
});
};
return Needle;
})();
setInterval(function() {
repaintGauge( Math.floor(Math.random() * 100) )
}, 1500);
needle = new Needle(chart);
needle.render();
needle.moveTo(percent);
})();
.chart-gauge
{
width: 400px;
margin: 100px auto
}
.chart-first
{
fill: #66AB8C;
}
.chart-second
{
fill: #ff533d;
}
.needle, .needle-center
{
fill: #000000;
}
.text {
color: "#112864";
font-size: 16px;
}
svg {
font: 10px sans-serif;
}
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.0.0/d3.min.js"></script>
<div class="chart-gauge"></div>
</body>
</html>
I am working on a D3 chart, but can't get the node labeling right (I would like to put the "size" on a second line, and change all the text to white. For instance, the "Toyota" node would say "Toyota Motor" and on a new line just below, "61.84").
Here is my starting point. I tried to add a pre block with the CSV data so it could run on JSfiddle, but I got stuck.
I know I need to change this code in order to get the data from the pre block instead of the external CSV:
d3.text("./car_companies.csv", function(error, text) {
After that, I need to add a new "node append", something like this:
node.append("revenue")
.style("text-anchor", "middle")
.attr("dy", "1.5em")
.text(function(d) { return d.revenue.substring(0, d.radius / 3); });
http://jsfiddle.net/nick2ny/pqo1x670/4/
Thank you for any ideas.
Not directly related to the question, but to use the <pre> element to hold your data, you have to use:
var text = d3.select("pre").text();
Instead of d3.text().
Back to the question:
For printing those values, you just need:
node.append("text")
.attr("dy", "1.3em")
.style("text-anchor", "middle")
.style("fill", "white")
.text(function(d) {
return d.size;
});
Adjusting dy the way you want. However, there is an additional problem: you're not populating size in the data array. Therefore, add this in the create_nodes function:
size: data[node_counter].size,
Here is the code with those changes:
<!DOCTYPE html>
<meta charset="utf-8">
<style type="text/css">
text {
font: 10px sans-serif;
}
pre {
display: none;
}
circle {
stroke: #565352;
stroke-width: 1;
}
</style>
<body>
<pre id="data">
Toyota Motor,61.84,Asia,239
Volkswagen,44.54,Europe,124
Daimler,40.79,Europe,104
BMW,35.78,Europe,80
Ford Motor,31.75,America,63
General Motors,30.98,America,60
</pre>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
Array.prototype.contains = function(v) {
for (var i = 0; i < this.length; i++) {
if (this[i] === v) return true;
}
return false;
};
var width = 500,
height = 500,
padding = 1.5, // separation between same-color nodes
clusterPadding = 6, // separation between different-color nodes
maxRadius = 12;
var color = d3.scale.ordinal()
.range(["#0033cc", "#33cc66", "#990033"]);
var text = d3.select("pre").text();
var colNames = "text,size,group,revenue\n" + text;
var data = d3.csv.parse(colNames);
data.forEach(function(d) {
d.size = +d.size;
});
//unique cluster/group id's
var cs = [];
data.forEach(function(d) {
if (!cs.contains(d.group)) {
cs.push(d.group);
}
});
var n = data.length, // total number of nodes
m = cs.length; // number of distinct clusters
//create clusters and nodes
var clusters = new Array(m);
var nodes = [];
for (var i = 0; i < n; i++) {
nodes.push(create_nodes(data, i));
}
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(.02)
.charge(0)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var node = svg.selectAll("circle")
.data(nodes)
.enter().append("g").call(force.drag);
node.append("circle")
.style("fill", function(d) {
return color(d.cluster);
})
.attr("r", function(d) {
return d.radius
})
node.append("text")
.attr("dy", ".3em")
.style("text-anchor", "middle")
.style("fill", "white")
.text(function(d) {
return d.text;
});
node.append("text")
.attr("dy", "1.3em")
.style("text-anchor", "middle")
.style("fill", "white")
.text(function(d) {
return d.size;
});
function create_nodes(data, node_counter) {
var i = cs.indexOf(data[node_counter].group),
r = Math.sqrt((i + 1) / m * -Math.log(Math.random())) * maxRadius,
d = {
cluster: i,
radius: data[node_counter].size * 1.5,
text: data[node_counter].text,
size: data[node_counter].size,
revenue: data[node_counter].revenue,
x: Math.cos(i / m * 2 * Math.PI) * 200 + width / 2 + Math.random(),
y: Math.sin(i / m * 2 * Math.PI) * 200 + height / 2 + Math.random()
};
if (!clusters[i] || (r > clusters[i].radius)) clusters[i] = d;
return d;
};
function tick(e) {
node.each(cluster(10 * e.alpha * e.alpha))
.each(collide(.5))
.attr("transform", function(d) {
var k = "translate(" + d.x + "," + d.y + ")";
return k;
})
}
// Move d to be adjacent to the cluster node.
function cluster(alpha) {
return function(d) {
var cluster = clusters[d.cluster];
if (cluster === d) return;
var x = d.x - cluster.x,
y = d.y - cluster.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + cluster.radius;
if (l != r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
cluster.x += x;
cluster.y += y;
}
};
}
// Resolves collisions between d and all other circles.
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + maxRadius + Math.max(padding, clusterPadding),
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + (d.cluster === quad.point.cluster ? padding : clusterPadding);
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
</script>
All,
I know there are many examples for D3.js collision detection for circles and rectangles.
I'm trying to do force simulation of ellipse nodes.
I tried following snippet which is originally for rectangles, but it's not perfect.
var width = 960,
height = 500,
minSize = 10,
maxSize = 30;
var n = 20,
m = 10;
var color = d3.scaleOrdinal(d3.schemeCategory10)
.domain(d3.range(m));
var nodes = d3.range(n).map(function() {
var c = Math.floor(Math.random() * m),
rx = Math.sqrt((c + 1) / m * -Math.log(Math.random())) * (maxSize - minSize) + minSize,
ry = Math.sqrt((c + 1) / m * -Math.log(Math.random())) * (maxSize - minSize) + minSize,
d = {color: c, rx: rx, ry: ry};
return d;
});
var collide = function(alpha) {
var quadtree = d3.quadtree()
.x((d) => d.x)
.y((d) => d.y)
.addAll(nodes);
nodes.forEach((d) => {
quadtree.visit((quad, x0, y0, x1, y1) => {
let updated = false;
if (quad.data && (quad.data !== d)) {
let x = d.x - quad.data.x,
y = d.y - quad.data.y,
xSpacing = (quad.data.rx + d.rx),
ySpacing = (quad.data.ry + d.ry),
absX = Math.abs(x),
absY = Math.abs(y),
l, lx, ly;
if (absX < xSpacing && absY < ySpacing) {
l = Math.sqrt(x * x + y * y);
lx = (absX - xSpacing) / l * alpha;
ly = (absY - ySpacing) / l * alpha;
if (Math.abs(lx) > Math.abs(ly)) {
lx = 0;
} else {
ly = 0;
}
d.x -= x *= lx;
d.y -= y *= ly;
quad.data.x += x;
quad.data.y += y;
updated = true;
}
}
return updated;
});
});
};
var force = d3.forceSimulation()
.nodes(nodes)
.force("center", d3.forceCenter())
.force("collide", (alpha) => collide(alpha))
.force("x", d3.forceX().strength(.01))
.force("y", d3.forceY().strength(.01))
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
var ellipse = svg.selectAll("ellipse")
.data(nodes)
.enter().append("ellipse")
.attr("rx", function(d) { return d.rx; })
.attr("ry", function(d) { return d.ry; })
.style("fill", function(d) { return color(d.color); })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
function tick() {
ellipse
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
function dragstarted(d) {
if (!d3.event.active) force.alphaTarget(0.3).restart();
d.x = d.x;
d.y = d.y;
}
function dragged(d) {
d.x = d3.event.x;
d.y = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) force.alphaTarget(0);
d.x = d3.event.x;
d.y = d3.event.y;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
There are too many gaps between nodes, and I know it's because ellipses are treated as rectangles in the collision detection.
Anybody who's got a good solution for this?
Thanks, in advance.
I've figured this out by myself.
Here's the collision detection library for d3.
ellipse-collision-detection
I've attached working example in the above repository.
Thanks!
I am trying to achieve this effect. The closest I could find an working example of that effect is Cartesian distortion effect which doesn't seem to work with D3 V4. I don't really understand which all lines need to be changed or how to change to make this example compatible with d3js version 4.
jsfiddle
(function chart3() {
console.clear()
var width = 960,
height = 180,
xSteps = d3.range(10, width, 16),
ySteps = d3.range(10, height, 16);
var xFisheye = d3.fisheye.scale(d3.scale.identity).domain([0, width]).focus(360),
yFisheye = d3.scale.linear().domain([0, height]);
var svg = d3.select("#chart3").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(-.5,-.5)");
svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);
var xLine = svg.selectAll(".x")
.data(xSteps)
.enter().append("line")
.attr("class", "x")
.attr("y2", height);
redraw();
svg.on("mousemove", function() {
var mouse = d3.mouse(this);
// HACK ( only for left-side )
xFisheye.focus(mouse[0] - 32); // HACK 1
yFisheye(mouse[1]);
if(mouse[0] > 26) // HACK 2
redraw();
});
function redraw() {
xLine.attr("x1", xFisheye).attr("x2", xFisheye);
}
})();
For what it's worth I just tweaked the d3-fisheye plugin to work with d3 v4. I've also added the fisheye.invert which might be useful.
import * as d3 from 'd3'
const fisheye = {
scale: function (scaleType) {
return d3FisheyeScale(scaleType(), 3, 0)
},
circular: function () {
let radius = 200
let distortion = 2
let k0
let k1
let focus = [0, 0]
function fisheye (d) {
let dx = d.x - focus[0]
let dy = d.y - focus[1]
let dd = Math.sqrt(dx * dx + dy * dy)
if (!dd || dd >= radius) return {x: d.x, y: d.y, z: dd >= radius ? 1 : 10}
let k = k0 * (1 - Math.exp(-dd * k1)) / dd * 0.75 + 0.25
return {x: focus[0] + dx * k, y: focus[1] + dy * k, z: Math.min(k, 10)}
}
function rescale () {
k0 = Math.exp(distortion)
k0 = k0 / (k0 - 1) * radius
k1 = distortion / radius
return fisheye
}
fisheye.radius = function (_) {
if (!arguments.length) return radius
radius = +_
return rescale()
}
fisheye.distortion = function (_) {
if (!arguments.length) return distortion
distortion = +_
return rescale()
}
fisheye.focus = function (_) {
if (!arguments.length) return focus
focus = _
return fisheye
}
return rescale()
}
}
function d3FisheyeScale (scale, d, a) {
function fisheye (_) {
let x = scale(_)
let left = x < a
let range = d3.extent(scale.range())
let min = range[0]
let max = range[1]
let m = left ? a - min : max - a
if (m === 0) m = max - min
return (left ? -1 : 1) * m * (d + 1) / (d + (m / Math.abs(x - a))) + a
}
fisheye.invert = function (xf) {
let left = xf < a
let range = d3.extent(scale.range())
let min = range[0]
let max = range[1]
let m = left ? a - min : max - a
if (m === 0) m = max - min
return scale.invert(a + m * (xf - a) / ((d + 1) * m - (left ? -1 : 1) * d * (xf - a)))
}
fisheye.distortion = function (_) {
if (!arguments.length) return d
d = +_
return fisheye
}
fisheye.focus = function (_) {
if (!arguments.length) return a
a = +_
return fisheye
}
fisheye.copy = function () {
return d3FisheyeScale(scale.copy(), d, a)
}
fisheye.nice = scale.nice
fisheye.ticks = scale.ticks
fisheye.tickFormat = scale.tickFormat
const rebind = function (target, source) {
let i = 1
const n = arguments.length
let method
while (++i < n) {
method = arguments[i]
target[method] = d3Rebind(target, source, source[method])
};
return target
}
function d3Rebind (target, source, method) {
return function () {
var value = method.apply(source, arguments)
return value === source ? target : value
}
}
return rebind(fisheye, scale, 'domain', 'range')
}
export default fisheye
Now to use it:
import fisheye from './fisheye'
import { scaleLinear, scalePow } from 'd3-scale'
const fisheyeLinearScale = fisheye.scale(scaleLinear)
const fisheyePowScale = fisheye.scale(scalePow().exponent(1.1).copy)
const myFisheyeScale = fisheyePowScale.domain(<domain>)
.range(<range>)
.focus(<mouseX>)
.distortion(<deformation>)
I am trying to modify the d3 force layout from SVG to DIV's. It seems the collision detection doesnt work as well with DIV's. You can see the working examples below.
(Another quick question, anyone know why css transform:translate isnt used for hardware acceleration)
DIV Version
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body,
html {
margin: 0;
width: 100%;
height: 100%
}
.divs div {
border-radius: 50%;
background: red;
position: absolute;
}
</style>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js"></script>
<script>
var width = $('body').width(),
height = $('body').height(),
padding = 10, // separation between nodes
maxRadius = 30;
var n = 20, // total number of nodes
m = 1; // number of distinct clusters
var color = d3.scale.category10()
.domain(d3.range(m));
var xPos = d3.scale.ordinal()
.domain(d3.range(m))
.rangePoints([width, width], 1);
var x =
d3.scale.linear()
.domain([0, width])
.range([0, width]);
var y =
d3.scale.linear()
.domain([0, height])
.range([0, height]);
var nodes = d3.range(n).map(function() {
var i = Math.floor(Math.random() * m),
v = (i + 1) / m * -Math.log(Math.random());
return {
radius: Math.random() * maxRadius + 20,
color: color(i),
cx: xPos(i),
cy: height
};
});
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(0)
.charge(0)
.on("tick", tick)
.start();
var $body = d3.select("body")
.append("div").attr('class', 'divs')
.attr('style', function(d) {
return 'width: ' + width + 'px; height: ' + height + 'px;';
});
var $div = $body.selectAll("div")
.data(nodes)
.enter()
.append("div")
.attr('style', function(d) {
return 'width: ' + (d.radius * 2) + 'px; height: ' + (d.radius * 2) + 'px; margin-left: -' + d.radius + 'px; margin-top: -' + d.radius + 'px;';
})
.call(force.drag);
function tick(e) {
$div
.each(gravity(.2 * e.alpha))
.each(collide(.5))
.style('left', function(d) {
return x(Math.max(d.radius, Math.min(width - d.radius, d.x))) + 'px';
})
.style('top', function(d) {
return y(Math.max(d.radius, Math.min(height - d.radius, d.y))) + 'px';
});
}
// Move nodes toward cluster focus.
function gravity(alpha) {
return function(d) {
d.y += (d.cy - d.y) * alpha;
d.x += (d.cx - d.x) * alpha;
};
}
// Resolve collisions between nodes.
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + maxRadius + padding,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + (d.color !== quad.point.color) * padding;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
</script>
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body, html { margin: 0;width: 100%; height: 100%}
circle {
}
</style>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js"></script>
<script>
var width = $('body').width(),
height = $('body').height(),
padding = 10, // separation between nodes
maxRadius = 40;
var n = 10, // total number of nodes
m = 1; // number of distinct clusters
var color = d3.scale.category10()
.domain(d3.range(m));
var x = d3.scale.ordinal()
.domain(d3.range(m))
.rangePoints([width - 200, width], 1);
var nodes = d3.range(n).map(function() {
var i = Math.floor(Math.random() * m),
v = (i + 1) / m * -Math.log(Math.random());
return {
radius: Math.random() * maxRadius + 30,
color: color(i),
cx: x(i),
cy: height
};
});
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(0)
.charge(0)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var circle = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", function(d) { return d.radius; })
.style("fill", function(d) { return d.color; })
.call(force.drag);
function tick(e) {
circle
.each(gravity(.2 * e.alpha))
.each(collide(.5))
//.attr("cx", function(d) { return d.x; })
//.attr("cy", function(d) { return d.y; });
.attr("cx", function(d) { return d.x = Math.max(d.radius, Math.min(width - d.radius, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(d.radius, Math.min(height - d.radius, d.y)); });
}
// Move nodes toward cluster focus.
function gravity(alpha) {
return function(d) {
d.y += (d.cy - d.y) * alpha;
d.x += (d.cx - d.x) * alpha;
};
}
// Resolve collisions between nodes.
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + maxRadius + padding,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + (d.color !== quad.point.color) * padding;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
</script>
The reason is you are not updating the d.x and d.y in the tick so the layout will never come to know if they are colliding or not.
.style('left', function(d) {
//update the d.x
d.x = x(Math.max(d.radius, Math.min(width - d.radius, d.x)))
return d.x + 'px';
})
.style('top', function(d) {
//update the d.y
d.y=y(Math.max(d.radius, Math.min(height - d.radius, d.y)));
return d.y + "px"
});
Working code:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body,
html {
margin: 0;
width: 100%;
height: 100%
}
.divs div {
border-radius: 50%;
background: red;
position: absolute;
}
</style>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js"></script>
<script>
var width = $('body').width(),
height = $('body').height(),
padding = 10, // separation between nodes
maxRadius = 30;
var n = 20, // total number of nodes
m = 1; // number of distinct clusters
var color = d3.scale.category10()
.domain(d3.range(m));
var xPos = d3.scale.ordinal()
.domain(d3.range(m))
.rangePoints([width, width], 1);
var x =
d3.scale.linear()
.domain([0, width])
.range([0, width]);
var y =
d3.scale.linear()
.domain([0, height])
.range([0, height]);
var nodes = d3.range(n).map(function() {
var i = Math.floor(Math.random() * m),
v = (i + 1) / m * -Math.log(Math.random());
return {
radius: Math.random() * maxRadius + 20,
color: color(i),
cx: xPos(i),
cy: height
};
});
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(0)
.charge(0)
.on("tick", tick)
.start();
var $body = d3.select("body")
.append("div").attr('class', 'divs')
.attr('style', function(d) {
return 'width: ' + width + 'px; height: ' + height + 'px;';
});
var $div = $body.selectAll("div")
.data(nodes)
.enter()
.append("div")
.attr('style', function(d) {
return 'width: ' + (d.radius * 2) + 'px; height: ' + (d.radius * 2) + 'px; margin-left: -' + d.radius + 'px; margin-top: -' + d.radius + 'px;';
})
.call(force.drag);
function tick(e) {
$div
.each(gravity(.2 * e.alpha))
.each(collide(.5))
.style('left', function(d) {
d.x = x(Math.max(d.radius, Math.min(width - d.radius, d.x)))
return d.x + 'px';
})
.style('top', function(d) {
d.y=y(Math.max(d.radius, Math.min(height - d.radius, d.y)));
return d.y + "px"
});
}
// Move nodes toward cluster focus.
function gravity(alpha) {
return function(d) {
d.y += (d.cy - d.y) * alpha;
d.x += (d.cx - d.x) * alpha;
};
}
// Resolve collisions between nodes.
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + maxRadius + padding,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + (d.color !== quad.point.color) * padding;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
</script>
Hope this helps!