I have code that should move a circle that's plotted on a graph in a svg using D3.js v6. The circle should be dragged to where the cursor is relative to the graph but I think the cursor position that is given is relative to the whole window and not the graph/svg. I'm not sure how to modify the mouse position to be relative to the svg. I have also tried using the suggestion from this answer here as well:
d3 v6 pointer function not adjusting for scale and translate
Edit:
If I were to start the circle at a position such as (0.5, 0.5) how would I go about making it so the circle only moves in a path along the circumference of a circle centered at (0, 0) with radius 0.5? I tried scaling the position of the mouse to be of magnitude 0.5 using:
x = x*(0.5/(Math.sqrt(x**2 + y**2)));
y = y*(0.5/(Math.sqrt(x**2 + y**2)));
As this is how you scale a vector to be a certain length while keeping the same direction as shown here:
https://math.stackexchange.com/questions/897723/how-to-resize-a-vector-to-a-specific-length
However this doesn't seem to work even though it should scale any point the mouse is at to be on the circle centered at the origin with radius 0.5.
var margin = {top: -20, right: 30, bottom: 40, left: 40};
//just setup
var svg = d3.select("#mysvg")
.attr("width", 300)
.attr("height", 300)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
var xAxis = d3.scaleLinear()
.domain([-1.5, 1.5])
.range([0, 300]);
svg.append("g")
.attr("transform", "translate(0," + 300 + ")")
.call(d3.axisBottom(xAxis));
var yAxis = d3.scaleLinear()
.domain([-1.5, 1.5])
.range([300, 0]);
svg.append("g")
.call(d3.axisLeft(yAxis));
var circle1 = svg.append('circle')
.attr('id', 'circle1')
.attr('cx', xAxis(0))
.attr('cy', yAxis(0))
.attr('r', 10)
.style('fill', '#000000')
.call( d3.drag().on('drag', dragCircle) ); //add drag listener
function dragCircle(event) {
let x = d3.pointer(event, svg.node())[0];
let y = d3.pointer(event, svg.node())[1];
console.log("x: " + x + " y: " + y);
d3.select("#circle1")
.attr("cx", xAxis(x))
.attr("cy", yAxis(y));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.0.0/d3.min.js"></script>
<svg id="mysvg"></svg>
Two things,
D3.pointer returns units in pixels, these do not need to be scaled - the purpopse of the scale is to take an arbitrary unit and convert it to pixels:
So instead of:
d3.select("#circle1")
.attr("cx", xAxis(x))
.attr("cy", yAxis(y));
Try:
d3.select("#circle1")
.attr("cx", x)
.attr("cy", y);
Also, we want the drag to be relative to the g which holds the plot and has a transform applied to it. This part is detailed in question you link to. We can specify that we want the drag to be relative to the parent g with:
let x = d3.pointer(event,svg.node())[0];
let y = d3.pointer(event,svg.node())[1];
We can then use some trigonometry to constrain the point to a ring and have the drag be based on the angle to any arbitrary point:
let x = d3.pointer(event,svg.node())[0];
let y = d3.pointer(event,svg.node())[1];
let cx = xAxis(0);
let cy = yAxis(0);
let r = xAxis(0.5)-xAxis(0);
let dx = x- cx;
let dy = y-cy;
var angle = Math.atan2(dy,dx);
d3.select("#circle1")
.attr("cx", cx + Math.cos(angle)*r)
.attr("cy", cy + Math.sin(angle)*r);
Which gives us:
var margin = {top: -20, right: 30, bottom: 40, left: 40};
//just setup
var svg = d3.select("#mysvg")
.attr("width", 300)
.attr("height", 300)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
var xAxis = d3.scaleLinear()
.domain([-1.5, 1.5])
.range([0, 300]);
svg.append("g")
.attr("transform", "translate(0," + 300 + ")")
.call(d3.axisBottom(xAxis));
var yAxis = d3.scaleLinear()
.domain([-1.5, 1.5])
.range([300, 0]);
svg.append("g")
.call(d3.axisLeft(yAxis));
var circle1 = svg.append('circle')
.attr('id', 'circle1')
.attr('cx', xAxis(0))
.attr('cy', yAxis(0))
.attr('r', 10)
.style('fill', '#000000')
.call( d3.drag().on('drag', dragCircle) ); //add drag listener
var dragCircle = svg.append('circle')
.attr('id', 'circle1')
.attr('cx', xAxis(0))
.attr('cy', yAxis(0))
.attr('r', xAxis(0.5)-xAxis(0))
.style('fill', 'none')
.style('stroke', 'black')
.style('stroke-line',1)
.call( d3.drag().on('drag', dragCircle) ); //add drag listener
function dragCircle(event) {
let x = d3.pointer(event,svg.node())[0];
let y = d3.pointer(event,svg.node())[1];
let cx = xAxis(0);
let cy = yAxis(0);
let r = xAxis(0.5)-xAxis(0);
let dx = x- cx;
let dy = y-cy;
var angle = Math.atan2(dy,dx);
d3.select("#circle1")
.attr("cx", cx + Math.cos(angle)*r)
.attr("cy", cy + Math.sin(angle)*r);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.0.0/d3.min.js"></script>
<svg id="mysvg"></svg>
I want to make a visual that shows ordinal data (ratings). There are 12 rating dimensions, and each rating will have its own dedicated line appended to a circle. The polar orientation of the line designates a category (i.e. lines pointing to 1 o'clock = category 1, 2 o'clock = category 2, and so forth). The length of the line indicates the ratings value (short = bad, long = good). The result should resemble a snow flake or a sun burst.
The name is stored in a string. The ratings for each company are stored in an array. Here are two slices of my data variable:
{'fmc':'fmc1', 'ratings':[10,10,10,10,10,10,10,10,10,10,10,10]},
{'fmc':'fmc2', 'ratings':[8,10,10,5,10,10,10,10,10,7,10,5]},
I have the grid-system placement for the companies functioning, but there seems to be an issue with the way I'm aligning the lines about the circle. Relevant code:
var rotationDegree = d3.scalePoint().domain([0,12]).range([0, 2*Math.PI - Math.PI/6]);
fmcG.append('line')
.data([10,10,10,10,10,10,10,10,10,10,10,10])
.attr("x1", r)
.attr("y1", r)
.attr("x2", function(d,i) { return length(10) * Math.cos(rotationDegree(i) - Math.PI/2) + (width/2); })
.attr("y2", function(d,i) { return length(10) * Math.sin(rotationDegree(i) - Math.PI/2) + (height/2); })
.style("stroke", function(d) { return "#003366" });
It would seem that I have the trig mapped out correctly, but in implementation I am proven wrong: the lines are not being appended about the circle like a snow flake / sun burst / clock.
Snippet:
var margins = {top:20, bottom:300, left:30, right:100};
var height = 600;
var width = 900;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");
var data = [
//{'fmc':'fmc1', 'ratings':[{'r1':10,'r2':10,'r3':10,'r4':10,'r5':10}]}
{'fmc':'fmc1', 'ratings':[10,10,10,10,10,10,10,10,10,10,10,10]},
{'fmc':'fmc2', 'ratings':[8,10,10,5,10,10,10,10,10,7,10,5]},
{'fmc':'fmc3', 'ratings':[10,10,10,10,10,10,10,10,10,10,10,10]},
];
var r = 30;
var length = d3.scaleLinear().domain([0, 10]).range([0, 50]);
var rotationDegree = d3.scalePoint().domain([0,12]).range([0, 2*Math.PI - Math.PI/6]);
var columns = 5;
var spacing = 220;
var vSpacing = 250;
var fmcG = graphGroup.selectAll('.fmc')
.data(data)
.enter()
.append('g')
.attr('class', 'fmc')
.attr('id', (d,i) => 'fmc' + i)
.attr('transform', (d,k) => {
var horSpace = (k % columns) * spacing;
var vertSpace = ~~((k / columns)) * vSpacing;
return "translate("+horSpace+","+vertSpace+")";
});
fmcG.append('circle')
.attr('cx',100)
.attr('cy',100)
.attr('r', r)
.style('fill','none')
.style('stroke','#003366');
fmcG.append('text')
.attr('x',100)
.attr('y',105)
.style('text-anchor','middle')
.text(function(d) {return d.fmc});
fmcG.append('line')
//.data(function(d) {return d.ratings}) why doesnt it workk??????
.data([10,10,10,10,10,10,10,10,10,10,10,10])
.attr("x1", r)
.attr("y1", r)
.attr("x2", function(d,i) { return length(10) * Math.cos(rotationDegree(i) - Math.PI/2) + (width/2); })
.attr("y2", function(d,i) { return length(10) * Math.sin(rotationDegree(i) - Math.PI/2) + (height/2); })
.style("stroke", function(d) { return "#003366" });
<script src="https://d3js.org/d3.v5.min.js"></script>
Question
How can I take an 12-item array and append lines about the circle in 30 degree increments (360 divided by 12) while using the value of each item in the array to determine the line's length?
The main issue is that, right now, you're appending a single line. For appending as many lines as data points you have to set up a proper enter selection:
fmcG.selectAll(null)
.data(function(d) {
return d.ratings
})
.enter()
.append('line')
//etc...
And that, by the way, is the reason your data is not working (as you ask in your comment "why doesnt it workk??????")
Other issues:
A point scale needs to have a discrete domain, for instance d3.range(12)
For whatever reason you're moving the circles 100px right and down. I'm moving the lines by the same amount.
Here is the snippet with those changes:
var margins = {
top: 20,
bottom: 300,
left: 30,
right: 100
};
var height = 600;
var width = 900;
var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate(" + margins.left + "," + margins.top + ")");
var data = [
//{'fmc':'fmc1', 'ratings':[{'r1':10,'r2':10,'r3':10,'r4':10,'r5':10}]}
{
'fmc': 'fmc1',
'ratings': [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10]
},
{
'fmc': 'fmc2',
'ratings': [8, 10, 10, 5, 10, 10, 10, 10, 10, 7, 10, 5]
},
{
'fmc': 'fmc3',
'ratings': [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10]
},
];
var r = 30;
var length = d3.scaleLinear().domain([0, 10]).range([0, 50]);
var rotationDegree = d3.scalePoint().domain(d3.range(12)).range([0, 2 * Math.PI]);
var columns = 5;
var spacing = 220;
var vSpacing = 250;
var fmcG = graphGroup.selectAll('.fmc')
.data(data)
.enter()
.append('g')
.attr('class', 'fmc')
.attr('id', (d, i) => 'fmc' + i)
.attr('transform', (d, k) => {
var horSpace = (k % columns) * spacing;
var vertSpace = ~~((k / columns)) * vSpacing;
return "translate(" + horSpace + "," + vertSpace + ")";
});
fmcG.append('circle')
.attr('cx', 100)
.attr('cy', 100)
.attr('r', r)
.style('fill', 'none')
.style('stroke', '#003366');
fmcG.append('text')
.attr('x', 100)
.attr('y', 105)
.style('text-anchor', 'middle')
.text(function(d) {
return d.fmc
});
fmcG.selectAll(null)
.data(function(d) {
return d.ratings
})
.enter()
.append('line')
.attr("x1", 100)
.attr("y1", 100)
.attr("x2", function(d, i) {
return 100 + length(d) * Math.cos(rotationDegree(i));
})
.attr("y2", function(d, i) {
return 100 + length(d) * Math.sin(rotationDegree(i));
})
.style("stroke", function(d) {
return "#003366"
});
<script src="https://d3js.org/d3.v5.min.js"></script>
I have been looking to create a moving gauge and struggled to find an off the shelf solution. I then stumbled across the gauge posted in the below link. Currently it runs random numbers in the chart. I would like it to change value based on an array of numerical values (not %ages) that I have, over a timeframe that I specify. The numbers are currently a simple Excel column.
So the gauge would go through the thousand or so numbers I have across, say, a minute.
I'll be frank, I'm a novice at coding and have limited experience in NetLogo and R. Hence why I'm here asking for pointers.
Any advice would be greatly appreciated. Thank you.
https://codepen.io/leomarquine/pen/xGzMjZ
var size = 150,
thickness = 60;
var color = d3.scale.linear()
.domain([0, 50, 100])
.range(['#db2828', '#fbbd08', '#21ba45']);
// .domain([0, 17, 33, 50, 67, 83, 100])
// .range(['#db4639', '#db7f29', '#d1bf1f', '#92c51b', '#48ba17', '#12ab24', '#0f9f59']);
var arc = d3.svg.arc()
.innerRadius(size - thickness)
.outerRadius(size)
.startAngle(-Math.PI / 2);
var svg = d3.select('#chart').append('svg')
.attr('width', size * 2)
.attr('height', size + 20)
.attr('class', 'gauge');
var chart = svg.append('g')
.attr('transform', 'translate(' + size + ',' + size + ')')
var background = chart.append('path')
.datum({
endAngle: Math.PI / 2
})
.attr('class', 'background')
.attr('d', arc);
var foreground = chart.append('path')
.datum({
endAngle: -Math.PI / 2
})
.style('fill', '#db2828')
.attr('d', arc);
var value = svg.append('g')
.attr('transform', 'translate(' + size + ',' + (size * .9) + ')')
.append('text')
.text(0)
.attr('text-anchor', 'middle')
.attr('class', 'value');
var scale = svg.append('g')
.attr('transform', 'translate(' + size + ',' + (size + 15) + ')')
.attr('class', 'scale');
scale.append('text')
.text(100)
.attr('text-anchor', 'middle')
.attr('x', (size - thickness / 2));
scale.append('text')
.text(0)
.attr('text-anchor', 'middle')
.attr('x', -(size - thickness / 2));
setInterval(function() {
update(Math.random() * 100);
}, 1500);
function update(v) {
v = d3.format('.1f')(v);
foreground.transition()
.duration(750)
.style('fill', function() {
return color(v);
})
.call(arcTween, v);
value.transition()
.duration(750)
.call(textTween, v);
}
function arcTween(transition, v) {
var newAngle = v / 100 * Math.PI - Math.PI / 2;
transition.attrTween('d', function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return arc(d);
};
});
}
function textTween(transition, v) {
transition.tween('text', function() {
var interpolate = d3.interpolate(this.innerHTML, v),
split = (v + '').split('.'),
round = (split.length > 1) ? Math.pow(10, split[1].length) : 1;
return function(t) {
this.innerHTML = d3.format('.1f')(Math.round(interpolate(t) * round) / round) + '<tspan>%</tspan>';
};
});
}
I use http://d3pie.org/#docs-settings
But there is no such parameter as the distance from the center to the internal labels.
Can someone tried to do it?
I want to move the internal labels closer to the outer edge of the circle.
Thank you so much.
now so:
need:
You can position the labels by defining a new arc as suggested in https://stackoverflow.com/a/8270668/2314737 and then applying the centroid function.
I defined a new arc newarc with an inner radius equal to 2/3 of the outer radius.
var newarc = d3.svg.arc()
.innerRadius(2 * radius / 3)
.outerRadius(radius);
Here's the JS code:
var width = 300;
var height = 300;
var svg = d3.select("body").append("svg");
svg.attr("width", width)
.attr("height", height);
var dataset = [11, 13, 18, 25, 31];
var radius = width / 2;
var innerRadius = 0;
var arc = d3.svg.arc()
.innerRadius(0)
.outerRadius(radius);
var pie = d3.layout.pie();
var arcs = svg.selectAll("g.arc")
.data(pie(dataset))
.enter()
.append("g")
.attr("class", "arc")
.attr("transform", "translate(" + radius + ", " + radius + ")");
//Draw arc paths
var color = d3.scale.category10();
arcs.append("path")
.attr("fill", function (d, i) {
console.log(d);
return color(i);
})
.attr("stroke", "white")
.attr("d", arc);
var newarc = d3.svg.arc()
.innerRadius(2 * radius / 3)
.outerRadius(radius);
// Place labels
arcs.append("text")
.attr("transform", function (d) {
return "translate(" + newarc.centroid(d) + ")";
})
.attr("text-anchor", "middle")
.attr("fill", "white")
.text(function (d) {
return d.value + "%";
});
Here is a working demo: http://jsfiddle.net/user2314737/kvz8uev8/2/
I decided to enroll in another way.
I added my property in the object and function of positioning inner labels in D3pie file d3pie.js
This function is located on the line - 996 d3pie.js
positionLabelGroups: function(pie, section) {
d3.selectAll("." + pie.cssPrefix + "labelGroup-" + section)
.style("opacity", 0)
.attr("transform", function(d, i) {
var x, y;
if (section === "outer") {
x = pie.outerLabelGroupData[i].x;
y = pie.outerLabelGroupData[i].y;
} else {
var pieCenterCopy = extend(true, {}, pie.pieCenter);
// now recompute the "center" based on the current _innerRadius
if (pie.innerRadius > 0) {
var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true });
var newCoords = math.translate(pie.pieCenter.x, pie.pieCenter.y, pie.innerRadius, angle);
pieCenterCopy.x = newCoords.x;
pieCenterCopy.y = newCoords.y;
//console.log('i ='+i , 'angle='+angle, 'pieCenterCopy.x='+pieCenterCopy.x, 'pieCenterCopy.y='+pieCenterCopy.y);
}
var dims = helpers.getDimensions(pie.cssPrefix + "labelGroup" + i + "-inner");
var xOffset = dims.w / 2;
var yOffset = dims.h / 4; // confusing! Why 4? should be 2, but it doesn't look right
// ADD VARAIBLE HERE !!! =)
var divisor = pie.options.labels.inner.pieDistanceOfEnd;
x = pieCenterCopy.x + (pie.lineCoordGroups[i][0].x - pieCenterCopy.x) / divisor;
y = pieCenterCopy.y + (pie.lineCoordGroups[i][0].y - pieCenterCopy.y) / divisor;
x = x - xOffset;
y = y + yOffset;
}
return "translate(" + x + "," + y + ")";
});
},
I add var divisor = pie.options.labels.inner.pieDistanceOfEnd;
Then I spotted this property devoltnyh the configuration file bhp and passed for plotting parameters.
inner: {
format: "percentage",
hideWhenLessThanPercentage: null,
pieDistanceOfEnd : 1.8
},
Meaning pieDistanceOfEnd: 1 hang tag on the outer radius of the chart
value pieDistanceOfEnd: 1.25 turn them slightly inward ....
You can play these parameters and to achieve the desired option.
In d3pie.js look for the function positionLabelGroups. In this function both labels (outer and inner) are positioned.
To modify the distance from the center you can play with the x,y here:
x = pieCenterCopy.x + (pie.lineCoordGroups[i][0].x - pieCenterCopy.x) / 1.8;
y = pieCenterCopy.y + (pie.lineCoordGroups[i][0].y - pieCenterCopy.y) / 1.8;
What I did was decreasing the 1.8 to 1.2 and obtained what youre looking for. Dont know what the other vars do, but you can study the code to figure it out
I'm starting playing with d3 and I want to achieve this result:
I've done this arc thing, but I don't know how to calculate position and rotation of triangle? This is my current code: (http://jsfiddle.net/spbGh/1/)
var width = 220,
height = 120;
var convertValue = function (value) {
return value * .75 * 2 * Math.PI;
}
var arc = d3.svg.arc()
.innerRadius(45)
.outerRadius(60)
.startAngle(0);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
var big_background = svg.append("path")
.datum({ endAngle: convertValue(1)})
.style("fill", "#f3f4f5")
.attr("d", arc);
var big_gain = svg.append("path")
.datum({ endAngle: convertValue(.75) })
.style("fill", "orange")
.attr("d", arc);
// HELP with this thing!!!
var triangle = svg.append('path')
.style("fill", "orange")
.attr('d', 'M 0 0 l 4 4 l -8 0 z')
You need to set the transform attribute accordingly:
.attr("transform",
"translate(" + Math.cos(convertValue(.127)-Math.PI/2)*70 + "," +
Math.sin(convertValue(.127)-Math.PI/2)*70 + ")" +
"rotate(" + ((convertValue(.127)*180/Math.PI)+180) + ")")
It becomes a bit easier if you draw the triangle the other way round. Updated jsfiddle here.