Custom easing in d3 - d3.js

I am trying to figure out how can I create custom ease for d3 animation.
const width = 1536;
const height = 720;
const svgns = "http://www.w3.org/2000/svg";
const svg = d3.select("svg");
/*create base svg*/
svg.attr("xmlns", svgns).attr("viewBox", `0 0 ${width} ${height}`);
/*create style*/
const style = d3.select("body").append("style").attr("type", "text/css");
//cubic-bezier(x1, y1, x2, y2) ->x1 and x2 must be between 0 and 1; with negative x1 and x2 provided css reverts to default->0.25, 0.1, 0.25, 1.0
//cubic-bezier --please change the value here to move red and green rect with the exact same velocity
//default-0.42, 0, 1, 1
//easeInSine - 0.12, 0, 0.39, 0; easeOutSine-0.61, 1, 0.88, 1; easeInOutSine-0.37, 0, 0.63, 1 etc.
//0.61, -0.25, 0.88, 1; 0.61, -0.15, 0.88, -1.25 etc.
const cubicBezCurvVal = "0.42, 0, 1, 1";
/*background rect*/
svg
.append("rect")
.attr("class", "vBoxRect")
.attr("width", `${width}`)
.attr("height", `${height}`)
.attr("fill", "#EFEFEF")
.attr("stroke", "black");
/*red rect - css animation+animation timing=> explicit cubic-bezier value*/
svg
.append("rect")
.attr("class", "red")
.attr("x", "0")
.attr("y", `${height / 2 - 40}`)
.attr("height", "50")
.attr("width", "50")
.attr("fill", "red")
.style("--dist", `${width - 50 + "px"}`);
svg
.append("line")
.attr("class", "path1")
.attr("x1", "0")
.attr("y1", () => {
return document.querySelector(".red").y.baseVal.value;
})
.attr("x2", `${width}`)
.attr("y2", () => {
return document.querySelector(".red").y.baseVal.value;
})
.attr("stroke", "red")
.attr("stroke-dasharray", "10");
const keyFrame1 = `
.red {
transform-box: fill-box;
animation-name: move1;
animation-duration: 5s;
animation-iteration-count: 1;
animation-timing-function: cubic-bezier(${cubicBezCurvVal});
animation-direction: normal;
animation-fill-mode: both;
}
#keyframes move1 {
0% {
transform: translateX(0px);
}
100% {
transform: translateX(var(--dist));
}
}
`;
style["_groups"][0][0].innerHTML = keyFrame1;
/*green rect - css animation+animation timing=> calculated progress of cubic-bezier value with javascript*/
svg
.append("rect")
.attr("class", "green")
.attr("x", "0")
.attr("y", `${height / 2 + 40}`)
.attr("height", "50")
.attr("width", "50")
.attr("fill", "green")
.style("--dist", `${width - 50 + "px"}`);
svg
.append("line")
.attr("class", "path2")
.attr("x1", "0")
.attr("y1", () => {
return document.querySelector(".green").y.baseVal.value;
})
.attr("x2", `${width}`)
.attr("y2", () => {
return document.querySelector(".green").y.baseVal.value;
})
.attr("stroke", "green")
.attr("stroke-dasharray", "10");
/*credit - https://blog.maximeheckel.com/posts/cubic-bezier-from-math-to-motion/*/
// create percentage container
const pct = [];
for (let i = 0; i <= 100; i++) {
pct.push(i / 100);
}
//cubic-bezier
//const cubicBezCurvVal = "0.42, 0, 1, 1"
//split bezier curve value
var cleanVal = cubicBezCurvVal.split(",");
//clean space with map -retunrns new array with the function, original array unchnaged
var cleanVal = cleanVal.map((x) => parseFloat(x.replace(/ /g, "")));
//p0
const p0 = {
x: 0,
y: 0
};
//p3
const p3 = {
x: 1,
y: 1
};
//p1
const p1 = {
x: cleanVal[0],
y: cleanVal[1]
};
//p2
const p2 = {
x: cleanVal[2],
y: cleanVal[3]
};
const x0 = p0.x; //=0
const y0 = p0.y; //=0
const x1 = p1.x;
const y1 = p1.y;
const x2 = p2.x;
const y2 = p2.y;
const x3 = p3.x; //=1
const y3 = p3.y; //=1
/*given a time percentage, calculates the x-axis of the cubic bezier graph, i.e. time elpased% */
const x = (t) =>
Math.pow(1 - t, 3) * x0 +
3 * Math.pow(1 - t, 2) * t * x1 +
3 * (1 - t) * Math.pow(t, 2) * x2 +
Math.pow(t, 3) * x3;
/*given a time percentage, calculates the y-axis of the cubic bezier graph, i.e. progres% */
const y = (t) =>
Math.pow(1 - t, 3) * y0 +
3 * Math.pow(1 - t, 2) * t * y1 +
3 * (1 - t) * Math.pow(t, 2) * y2 +
Math.pow(t, 3) * y3;
//penner's easing equation p=f(t)
const c = width - 50; // c of t,b,c,d of penner's equation
const b = 0; // b of t,b,c,d of penner's equation
//create container
const time = []; //to collect values of x(t), i.e. time elapsed %
const progress = []; //to collect values of y(t), i.e. progress %
//get the time first --- goes into keyframe---not dependent on progress,i.e. y(t)
pct.forEach((a) => {
time.push(x(a));
});
//get the progress for each time --- goes into progress --- not dependent on time x(t)
pct.forEach((a) => {
progress.push(y(a) * c + b);
});
//generate keyFrame string
var str = "#keyframes move{";
for (let i = 0; i < time.length; i++) {
var styleStr = `${time[i] * 100}%{ transform:translateX(${
progress[i]
}px);}`;
str += styleStr;
}
const keyFrame2 = `.green {
transform-box: fill-box;
animation-name: move;
animation-duration: 5s;
animation-iteration-count: 1;
animation-timing-function: linear;
animation-direction: normal;
animation-fill-mode: both;
}
${str}}
`;
style["_groups"][0][0].innerHTML += keyFrame2;
/*blue rect for d3*/
function progress1(t) {
//p0
const p0 = {
x: 0,
y: 0
};
//p3
const p3 = {
x: 1,
y: 1
};
//p1
const p1 = {
x: cleanVal[0],
y: cleanVal[1]
};
//p2
const p2 = {
x: cleanVal[2],
y: cleanVal[3]
};
const x0 = p0.x; ///0
const y0 = p0.y; ///0
const x1 = p1.x;
const y1 = p1.y;
const x2 = p2.x;
const y2 = p2.y;
const x3 = p3.x; ////1
const y3 = p3.y; ////1
const progress =
Math.pow(1 - t, 3) * y0 +
3 * Math.pow(1 - t, 2) * t * y1 +
3 * (1 - t) * Math.pow(t, 2) * y2 +
Math.pow(t, 3) * y3;
return t >= 1 ? 1 : progress;
}
svg
.append("rect")
.attr("class", "blue")
.attr("x", "0")
.attr("y", `${height / 2 + 120}`)
.attr("height", "50")
.attr("width", "50")
.attr("fill", "blue")
.each(function (d, i) {
d3.select(this)
.transition()
.duration(5000)
// .ease(progress1(d))
.attr("x", `${width - 50}`);
});
svg
.append("line")
.attr("class", "path3")
.attr("x1", "0")
.attr("y1", () => {
return document.querySelector(".blue").y.baseVal.value;
})
.attr("x2", `${width}`)
.attr("y2", () => {
return document.querySelector(".blue").y.baseVal.value;
})
.attr("stroke", "blue")
.attr("stroke-dasharray", "10");
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>
<body>
<svg>
</svg>
</body>
<script src="prod.js"></script>
</html>
To elaborate, in CSS, animation-timing function is controlled by cubic-bezier value.
In my element, I have three rect- red, green, and blue.
red rect is moved with an explicit cubic-bezier value animation-timing-function: cubic-bezier(0.42, 0, 1, 1);.
For green rect I have explicitly calculated the keyFrame time% and progress using the following, given a cubic-bezier value. By doing this, I am explicitly telling the green rect to move as per the calculation.
// create percentage container
const pct = [];
for (let i = 0; i <= 100; i++) {
pct.push(i / 100);
}
//cubic-bezier
//const cubicBezCurvVal = "0.42, 0, 1, 1"
//split bezier curve value
var cleanVal = cubicBezCurvVal.split(",");
//clean space with map -retunrns new array with the function, original array unchnaged
var cleanVal = cleanVal.map((x) => parseFloat(x.replace(/ /g, "")));
//p0
const p0 = {
x: 0,
y: 0
};
//p3
const p3 = {
x: 1,
y: 1
};
//p1
const p1 = {
x: cleanVal[0],
y: cleanVal[1]
};
//p2
const p2 = {
x: cleanVal[2],
y: cleanVal[3]
};
const x0 = p0.x; //=0
const y0 = p0.y; //=0
const x1 = p1.x;
const y1 = p1.y;
const x2 = p2.x;
const y2 = p2.y;
const x3 = p3.x; //=1
const y3 = p3.y; //=1
/*given a time percentage, calculates the x-axis of the cubic bezier graph, i.e. time elpased% */
const x = (t) =>
Math.pow(1 - t, 3) * x0 +
3 * Math.pow(1 - t, 2) * t * x1 +
3 * (1 - t) * Math.pow(t, 2) * x2 +
Math.pow(t, 3) * x3;
/*given a time percentage, calculates the y-axis of the cubic bezier graph, i.e. progres% */
const y = (t) =>
Math.pow(1 - t, 3) * y0 +
3 * Math.pow(1 - t, 2) * t * y1 +
3 * (1 - t) * Math.pow(t, 2) * y2 +
Math.pow(t, 3) * y3;
//penner's easing equation p=f(t)
const c = width - 50; // c of t,b,c,d of penner's equation
const b = 0; // b of t,b,c,d of penner's equation
//create container
const time = []; //to collect values of x(t), i.e. time elapsed %
const progress = []; //to collect values of y(t), i.e. progress %
//get the time first --- goes into keyframe---not dependent on progress,i.e. y(t)
pct.forEach((a) => {
time.push(x(a));
});
//get the progress for each time --- goes into progress --- not dependent on time x(t)
pct.forEach((a) => {
progress.push(y(a) * c + b);
});
I also have blue rect and I was wondering how can translate the same cubic-bezier value into a custom -ease function for d3?
I referred to this and came up with following which did not work unfortunately.
function progress1(t) {
//p0
const p0 = {
x: 0,
y: 0
};
//p3
const p3 = {
x: 1,
y: 1
};
//p1
const p1 = {
x: cleanVal[0],
y: cleanVal[1]
};
//p2
const p2 = {
x: cleanVal[2],
y: cleanVal[3]
};
const x0 = p0.x; ///0
const y0 = p0.y; ///0
const x1 = p1.x;
const y1 = p1.y;
const x2 = p2.x;
const y2 = p2.y;
const x3 = p3.x; ////1
const y3 = p3.y; ////1
const progress =
Math.pow(1 - t, 3) * y0 +
3 * Math.pow(1 - t, 2) * t * y1 +
3 * (1 - t) * Math.pow(t, 2) * y2 +
Math.pow(t, 3) * y3;
return t >= 1 ? 1 : progress;

Related

How can I calculate distante between rects in D3?

I'm working on this project and I need to calculate the distance between rects? Some idea?
My goal is to replicate one hart about Covid from this piece from The Washington Post: https://www.washingtonpost.com/graphics/2020/health/coronavirus-herd-immunity-simulation-vaccine/
Below the picture is the code who I used to generate this chart.
Thanks!!
export function main(){
createSVG()
build()
}
let offset = {top: 20, left: 70}
let screenWidth = 800 - offset.left
let screenHeight = 400 - offset.top
function createSVG() {
let container = d3.select('#container')
svg = container.append('svg')
.attr('id', 'canvas')
.attr('width', screenWidth + offset.left)
.attr('height', screenHeight + offset.top)
}
function build() {
let rectWidth = 15
let rectHeight = 15
let randomMovementLimit = 9
let people = []
for(let i = 0; i< 100; i++){
const cellX = i % 10
const randomX = Math.random() * randomMovementLimit - 3
const x = offset.left + cellX * (rectWidth + 10) + randomX
const cellY = Math.floor(i / 10)
const randomY = Math.random() * randomMovementLimit - 3
const y = offset.top + cellY * (rectHeight + 10) + randomY
const infected = Math.random() > 0.5
people.push({x: x, y: y, cellX: cellX, cellY: cellY, infected: infected})
}
console.log(people[0])
const infectedPeople = people.filter
const healtyPeople = people.filter
let rects = svg.selectAll('rect')
.data(people)
.enter()
.append('rect')
.attr('x', function(d,i) {
return d.x
})
.attr('y', function(d,i) {
return d.y
})
.attr('height', rectHeight)
.attr('width', rectWidth)
.style('fill', (d, i) => {
return d.infected ? "red" : "blue"
})
.attr('rx', 5)
}
Calculate distance from center of each rectangle using pythagorean theorem. I think but haven't tested if I remember right that the center of each rectangle is at d.x + rectWidth/2 and d.y - rectHeight/2. Initialize rectWidth and rectHeight outside of the build function so they are within scope.
Example to get newly infected rectangles at distance less than social_dist from the previously infected:
let social_dist = 6;
rects.filter(function(d,i){
for (let p of infectedPeople){
let xDist = (p.x + rectWidth/2) - (d.x + rectWidth/2);
let yDist = (p.y - rectHeight/2) - (d.y - rectHeight/2);
let dist = Math.sqrt( xDist*xDist + yDist*yDist );
if (dist < social_dist){
return true;
}
return false;
});

Colored text and line-breaks for D3 node labels

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>

Modify Cartesian DIstortion for D3js V4

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>)

Recreate Cluster Force Layout using d3 v4.0

I was trying to recreate the cluster force layout using D3 v4 like this: https://bl.ocks.org/mbostock/1747543. I reused the cluster function from Mike's code, but the result was not good (http://codepen.io/aizizhang/pen/OXzJdK). Also, if I passed in an alpha parameter larger than 1, the cx and cy will not be calculated properly. Can someone give me a hand?
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Clustered Bubble Chart</title>
<script src="https://d3js.org/d3.v4.js"></script>
</head>
<body>
<script>
let margin = {
top: 100,
right: 100,
bottom: 100,
left: 100
},
height = window.innerHeight,
width = window.innerWidth,
padding = 1.5, // separation between same-color nodes
clusterPadding = 6, // separation between different-color nodes
maxRadius = 12,
n = 200, // total number of nodes
m = 10, // number of distinct clusters
z = d3.scaleOrdinal(d3.schemeCategory20),
clusters = new Array(m);
let svg = d3.select('body')
.append('svg')
.attr('height', height)
.attr('width', width)
.append('g').attr('transform', `translate(${margin.right}, ${margin.top})`);
let nodes = d3.range(200).map(() => {
let i = Math.floor(Math.random() * m),
radius = Math.sqrt((i + 1) / m * -Math.log(Math.random())) * maxRadius,
d = {
cluster: i,
r: radius,
x: Math.random() * 200,
y: Math.random() * 200
};
if (!clusters[i] || (radius > clusters[i].r)) clusters[i] = d;
return d;
});
let circles = svg.append('g')
.datum(nodes)
.selectAll('.circle')
.data(d => d)
.enter().append('circle')
.attr('r', (d) => d.r)
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y)
.attr('fill', (d) => z(d.cluster))
.attr('stroke', 'black')
.attr('stroke-width', 1);
let simulation = d3.forceSimulation(nodes)
.velocityDecay(0.2)
.force("x", d3.forceX().x(200).strength(.5))
.force("y", d3.forceY().y(200).strength(.5))
.force("collide", d3.forceCollide().radius(function(d) {
return d.r + 0.5;
}).strength(0.5).iterations(2))
.on("tick", ticked)
// .force("charge", d3.forceManyBody(100))
function ticked() {
// let alpha = simulation.alpha();
circles
.each(clustering(0.5))
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y);
}
// Move d to be adjacent to the cluster node.
function clustering(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.r + cluster.r;
if (l !== r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
cluster.x += x;
cluster.y += y;
}
};
}
</script>
</body>
</html>
I think part of the problem is you are using the new 4.0 collide force but you need to use the collide calculations from the original block you were copying. Here's a port of the original example:
http://bl.ocks.org/shancarter/f621ac5d93498aa1223d8d20e5d3a0f4
Hope that helps!

D3.js Gauge look like Telerik Radial Gauge

Hello I need help to add gray (Minor and Major) graduation lines inside the gauge to capture more precisely the position of the needle. Also if you have any idea how we could put text to Major graduation lines.
For my sample I would required 7 Major graduation lines divided by 10 minor separations (to look exactly like the Telerik Gauge).
Here is my implementation : http://jsfiddle.net/8svg5/
<html>
<head runat="server">
<title></title>
</head>
<body>
<div>
<a id="myTooltip" title="This is my message"></a>
<div id="svgTarget"></div>
</div>
<script>
$(function () {
var gaugeRanges = [
{
From: 1.5,
To: 2.5,
Color: "#8dcb2a"
}, {
From: 2.5,
To: 3.5,
Color: "#ffc700"
}, {
From: 3.5,
To: 4.5,
Color: "#ff7a00"
},
{
From: 4.5,
To: 6,
Color: "#c20000"
}];
$("#svgTarget").mttD3Gauge({ data: gaugeRanges });
});
</script>
</body>
</html>
(function ($) {
$.fn.mttD3Gauge = function (options) {
var settings = $.extend({
width: 300,
innerRadius: 130,
outterRadius: 145,
data: []
}, options);
this.create = function () {
this.html("<svg class='mtt-svgClock' width='" + settings.width + "' height='" + settings.width + "'></svg>");
var maxLimit = 0;
var minLimit = 9999999;
var d3DataSource = [];
var d3TickSource = [];
//Data Genration
$.each(settings.data, function (index, value) {
d3DataSource.push([value.From, value.To, value.Color]);
if (value.To > maxLimit) maxLimit = value.To;
if (value.From < minLimit) minLimit = value.From;
});
if (minLimit > 0) {
d3DataSource.push([0, minLimit, "#d7d7d7"]);
}
var pi = Math.PI;
//Control Genration
var vis = d3.select(this.selector + " .mtt-svgClock");
var translate = "translate(" + settings.width / 2 + "," + settings.width / 2 + ")";
var cScale = d3.scale.linear().domain([0, maxLimit]).range([-120 * (pi / 180), 120 * (pi / 180)]);
var arc = d3.svg.arc()
.innerRadius(settings.innerRadius)
.outerRadius(settings.outterRadius)
.startAngle(function (d) { return cScale(d[0]); })
.endAngle(function (d) { return cScale(d[1]); });
var tickArc = d3.svg.arc()
.innerRadius(settings.innerRadius - 20)
.outerRadius(settings.innerRadius - 2)
.startAngle(function (d) { return cScale(d[0]); })
.endAngle(function (d) { return cScale(d[1]); });
for (var i = 0; i < 10; i++) {
var point = (i * maxLimit) / 10.0;
d3TickSource.push([point, point +1, "#d7d7d7"]);
}
vis.selectAll("path")
.data(d3DataSource)
.enter()
.append("path")
.attr("d", arc)
.style("fill", function (d) { return d[2]; })
.attr("transform", translate);
return this;
};
return this.create();
};
}(jQuery));
enter code here
Here is the link of the page of what i'm trying to achieve with D3.js
http://demos.telerik.com/aspnet-ajax/gauge/examples/types/radialgauge/defaultcs.aspx?#qsf-demo-source
Any help will be greatly appreciated!
I finally found the way to add graduation, feel free to use this code
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<script src="Scripts/jquery-1.9.1.js"></script>
<script src="Scripts/html5shiv.js"></script>
<script src="Scripts/d3.v3.min.js"></script>
<script src="Scripts/mtt-D3Gauge.js"></script>
</head>
<body>
<div>
<a id="myTooltip" title="This is my message"></a>
<div id="svgTarget"></div>
</div>
<script>
$(function () {
var gaugeRanges = [
{
From: 1.5,
To: 2.5,
Color: "#8dcb2a"
}, {
From: 2.5,
To: 3.5,
Color: "#ffc700"
}, {
From: 3.5,
To: 4.5,
Color: "#ff7a00"
},
{
From: 4.5,
To: 6,
Color: "#c20000"
}];
$("#svgTarget").mttD3Gauge({ data: gaugeRanges });
});
</script>
</body>
</html>
(function ($) {
$.fn.mttD3Gauge = function (options) {
var vis;
var settings = $.extend({
width: 300,
innerRadius: 130,
outterRadius: 145,
majorGraduations: 6,
minorGraduations: 10,
majorGraduationLenght: 16,
minorGraduationLenght: 10,
majorGraduationMarginTop: 7,
majorGraduationColor: "rgb(234,234,234)",
minorGraduationColor:"rgb(234,234,234)",
data: []
}, options);
this.create = function () {
this.html("<svg class='mtt-svgClock' width='" + settings.width + "' height='" + settings.width + "'></svg>");
var maxLimit = 0;
var minLimit = 9999999;
var d3DataSource = [];
//Data Genration
$.each(settings.data, function (index, value) {
d3DataSource.push([value.From, value.To, value.Color]);
if (value.To > maxLimit) maxLimit = value.To;
if (value.From < minLimit) minLimit = value.From;
});
if (minLimit > 0) {
d3DataSource.push([0, minLimit, "#d7d7d7"]);
}
//Render Gauge Color Area
vis = d3.select(this.selector + " .mtt-svgClock");
var translate = "translate(" + settings.width / 2 + "," + settings.width / 2 + ")";
var cScale = d3.scale.linear().domain([0, maxLimit]).range([-120 * (Math.PI / 180), 120 * (Math.PI / 180)]);
var arc = d3.svg.arc()
.innerRadius(settings.innerRadius)
.outerRadius(settings.outterRadius)
.startAngle(function (d) { return cScale(d[0]); })
.endAngle(function (d) { return cScale(d[1]); });
vis.selectAll("path")
.data(d3DataSource)
.enter()
.append("path")
.attr("d", arc)
.style("fill", function (d) { return d[2]; })
.attr("transform", translate);
renderMajorGraduations();
return this;
};
var renderMinorGraduations = function (majorGraduationsAngles, indexMajor) {
var graduationsAngles = [];
if (indexMajor > 0) {
var minScale = majorGraduationsAngles[indexMajor - 1];
var maxScale = majorGraduationsAngles[indexMajor];
var scaleRange = maxScale - minScale;
for (var i = 1; i < settings.minorGraduations; i++) {
var scaleValue = minScale + i * scaleRange / settings.minorGraduations;
graduationsAngles.push(scaleValue);
}
var xCenter = settings.width / 2;
var yCenter = settings.width / 2;
//Render Minor Graduations
$.each(graduationsAngles, function (indexMinor, value) {
var cos1Adj = Math.round(Math.cos((90 - value) * Math.PI / 180) * (settings.innerRadius - settings.majorGraduationMarginTop - settings.minorGraduationLenght));
var sin1Adj = Math.round(Math.sin((90 - value) * Math.PI / 180) * (settings.innerRadius - settings.majorGraduationMarginTop - settings.minorGraduationLenght));
var cos2Adj = Math.round(Math.cos((90 - value) * Math.PI / 180) * (settings.innerRadius - settings.majorGraduationMarginTop));
var sin2Adj = Math.round(Math.sin((90 - value) * Math.PI / 180) * (settings.innerRadius - settings.majorGraduationMarginTop));
var x1 = xCenter + cos1Adj;
var y1 = yCenter + sin1Adj * -1;
var x2 = xCenter + cos2Adj;
var y2 = yCenter + sin2Adj * -1;
vis.append("svg:line")
.attr("x1", x1)
.attr("y1", y1)
.attr("x2", x2)
.attr("y2", y2)
.style("stroke", settings.majorGraduationColor);
});
}
};
var renderMajorGraduations = function () {
var scaleRange = 240;
var minScale = -120;
var graduationsAngles = [];
for (var i = 0; i <= settings.majorGraduations; i++) {
var scaleValue = minScale + i * scaleRange / settings.majorGraduations;
graduationsAngles.push(scaleValue);
}
var xCenter = settings.width / 2;
var yCenter = settings.width / 2;
//Render Major Graduations
$.each(graduationsAngles, function (index, value) {
var cos1Adj = Math.round(Math.cos((90 - value) * Math.PI / 180) * (settings.innerRadius - settings.majorGraduationMarginTop - settings.majorGraduationLenght));
var sin1Adj = Math.round(Math.sin((90 - value) * Math.PI / 180) * (settings.innerRadius - settings.majorGraduationMarginTop - settings.majorGraduationLenght));
var cos2Adj = Math.round(Math.cos((90 - value) * Math.PI / 180) * (settings.innerRadius - settings.majorGraduationMarginTop));
var sin2Adj = Math.round(Math.sin((90 - value) * Math.PI / 180) * (settings.innerRadius - settings.majorGraduationMarginTop));
var x1 = xCenter + cos1Adj;
var y1 = yCenter + sin1Adj * -1;
var x2 = xCenter + cos2Adj;
var y2 = yCenter + sin2Adj * -1;
vis.append("svg:line")
.attr("x1", x1)
.attr("y1", y1)
.attr("x2", x2)
.attr("y2", y2)
.style("stroke", settings.majorGraduationColor);
renderMinorGraduations(graduationsAngles, index);
});
};
return this.create();
};
}(jQuery));

Resources