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>
Related
I'm trying to build a doughnut chart with rounded edges only on one side. My problem is that I have both sided rounded and not just on the one side. Also can't figure out how to do more foreground arcs not just one.
const tau = 2 * Math.PI; // http://tauday.com/tau-manifesto
const arc = d3.arc()
.innerRadius(80)
.outerRadius(100)
.startAngle(0)
.cornerRadius(15);
const svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
Background arc, but I'm not sure if this is even needed?
const background = g.append("path")
.datum({endAngle: tau})
.style("fill", "#ddd")
.attr("d", arc);
const data = [ .51];
const c = d3.scaleThreshold()
.domain([.200,.205,.300,.310, .501, 1])
.range(["green","#ddd", "orange","#ddd", "red"]);
Const pie = d3.pie()
.sort(null)
.value(function(d) {
return d;
});
Only have one foreground, but need to be able to have multiple:
const foreground = g.selectAll('.arc')
.data(pie(data))
.enter()
.append("path")
.attr("class", "arc")
.datum({endAngle: 3.8})
.style("fill", function(d) {
return c(d.value);
})
.attr("d", arc)
What am I doing wrong?
var tau = 2 * Math.PI; // http://tauday.com/tau-manifesto
// An arc function with all values bound except the endAngle. So, to compute an
// SVG path string for a given angle, we pass an object with an endAngle
// property to the `arc` function, and it will return the corresponding string.
var arc = d3.arc()
.innerRadius(80)
.outerRadius(100)
.startAngle(0)
.cornerRadius(15);
// Get the SVG container, and apply a transform such that the origin is the
// center of the canvas. This way, we don’t need to position arcs individually.
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// Add the background arc, from 0 to 100% (tau).
var background = g.append("path")
.datum({endAngle: tau})
.style("fill", "#ddd")
.attr("d", arc);
var data = [ .51];
var c = d3.scaleThreshold()
.domain([.200,.205,.300,.310, .501, 1])
.range(["green","#ddd", "orange","#ddd", "red"]);
var pie = d3.pie()
.sort(null)
.value(function(d) {
return d;
});
// Add the foreground arc in orange, currently showing 12.7%.
var foreground = g.selectAll('.arc')
.data(pie(data))
.enter()
.append("path")
.attr("class", "arc")
.datum({endAngle: 3.8})
.style("fill", function(d) {
return c(d.value);
})
.attr("d", arc)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="960" height="500"></svg>
The documentation states, that the corner radius is applied to both ends of the arc. Additionally, you want the arcs to overlap, which is also not the case.
You can add the one-sided rounded corners the following way:
Use arcs arc with no corner radius for the data.
Add additional path objects corner just for the rounded corner. These need to be shifted to the end of each arc.
Since corner has rounded corners on both sides, add a clipPath that clips half of this arc. The clipPath contains a path for every corner. This is essential for arcs smaller than two times the length of the rounded corners.
raise all elements of corner to the front and then sort them descending by index, so that they overlap the right way.
const arc = d3.arc()
.innerRadius(50)
.outerRadius(70);
const arc_corner = d3.arc()
.innerRadius(50)
.outerRadius(70)
.cornerRadius(10);
const svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
const clipPath = g.append("clipPath")
.attr("id", "clip_corners");
const c = d3.scaleQuantile()
.range(["#f7fcf0","#e0f3db","#ccebc5","#a8ddb5","#7bccc4","#4eb3d3","#2b8cbe","#08589e"]);
const pie = d3.pie().value(d => d);
function render(values) {
c.domain(values);
const arcs = pie(values);
const corners = pie(values).map(d => {
d.startAngle = d.endAngle - 0.2;
d.endAngle = d.endAngle + 0.2;
return d;
});
const clip = pie(values).map(d => {
d.startAngle = d.endAngle - 0.01;
d.endAngle = d.endAngle + 0.2;
return d;
});
g.selectAll(".arc")
.data(arcs)
.join("path")
.attr("class", "arc")
.style("fill", d => c(d.value))
.attr("d", arc);
clipPath.selectAll("path")
.data(clip)
.join("path")
.attr("d", arc);
g.selectAll(".corner")
.data(corners)
.join("path")
.raise()
.attr("class", "corner")
.attr("clip-path", "url(#clip_corner)")
.style("fill", d => c(d.value))
.attr("d", arc_corner)
.sort((a, b) => b.index - a.index);
}
function randomData() {
const num = Math.ceil(8 * Math.random()) + 2;
const values = Array(num).fill(0).map(d => Math.random());
render(values);
}
d3.select("#random_data")
.on("click", randomData);
randomData();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.3.0/d3.min.js"></script>
<button id="random_data">
Random data
</button>
<svg width="150" height="150"></svg>
I changed the dependency to the current version of d3.
I want to calculate the distance while drawing the line on the vertical axis or horizontal axis. It could show next to the line of how much distance drawn from mousedraw or any other ways. Can someone help me?
var width = 400,
height = 500;
var data = [10, 15, 20, 25, 30];
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.on("mousedown", mousedown)
.on("mouseup", mouseup);
var xscale = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, width - 100]);
var yscale = d3.scale.linear()
.domain([0, d3.max(data)])
.range([height / 2, 0]);
svg.on("mousemove", function() {
console.log("x - using invert", xscale.invert(d3.mouse(this)[0] - 50));
console.log("y- using invert", yscale.invert(d3.mouse(this)[1] - 10));
});
var x_axis = d3.svg.axis()
.scale(xscale).orient("bottom");
var y_axis = d3.svg.axis()
.scale(yscale).orient("left");;
svg.append("g")
.attr("transform", "translate(50, 10)")
.call(y_axis);
var xAxisTranslate = height / 2 + 10;
svg.append("g")
.attr("transform", "translate(50, " + xAxisTranslate + ")")
.call(x_axis)
function mousedown() {
var m = d3.mouse(this);
line = svg.append("line")
.attr("x1", m[0])
.attr("y1", m[1])
.attr("x2", m[0])
.attr("y2", m[1]);
svg.on("mousemove", mousemove);
}
function mousemove() {
var m = d3.mouse(this);
line.attr("x2", m[0])
.attr("y2", m[1]);
}
function mouseup() {
svg.on("mousemove", null);
}
Please check the live output I have now: https://jsfiddle.net/anojansith/m38b5fnp/1/
Image expected output
You might already know the Pythagorean theorem for determining the distance of a straight line; you could implement it like this:
Math.sqrt((m[0] - line.attr("x1")) ** 2 + (m[1] - line.attr("y1")) ** 2)
I'm referencing your global variable line and the local variable m for the mouse position. To display this next to the mouse cursor? Well, in mousedown you can add a text variable in addition to the line:
text = svg.append("text")
.attr("x", m[0])
.attr("y", m[1])
.style("user-select", "none")
Then you update the text in mousemove, shoving it over to the right of the cursor a little. Also in this case the distance is expressed in the same units as the data, by inverting the scale functions and thereby converted from pixels back into the data units.
var dist = Math.sqrt(
(xscale.invert(m[0]) - xscale.invert(line.attr("x1"))) ** 2 +
(yscale.invert(m[1]) - yscale.invert(line.attr("y1"))) ** 2)
text
.text(dist.toFixed(2))
.attr("x", m[0] + 10)
.attr("y", m[1])
and remove it in mouseup
text.remove()
I'm tying to implement semantic zoom with d3.js v4. Most examples and questions on Stackoverflow are for v3. So i tried to alter one of them, like from this answer. Example from the answer: bl.ocks.org example
I tried to adept the example for d3 v4:
var xOld, yOld;
var width = document.querySelector('body').clientWidth,
height = document.querySelector('body').clientHeight;
var randomX = d3.randomNormal(width / 2, 80),
randomY = d3.randomNormal(height / 2, 80);
var data = d3.range(2000).map(function() {
return [
randomX(),
randomY()
];
});
var xScale = d3.scaleLinear()
.domain(d3.extent(data, function (d) {
return d[0];
}))
.range([0, width]);
var yScale = d3.scaleLinear()
.domain(d3.extent(data, function (d) {
return d[1];
}))
.range([0, height]);
var xExtent = xScale.domain();
var yExtent = yScale.domain();
var zoomer = d3.zoom().scaleExtent([1, 8]).on("zoom", zoom);
var svg0 = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
var svg = svg0.append('g')
.attr("width", width)
.attr("height", height)
var circle = svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", 2.5)
.attr("transform", transform_);
svg0
.call(zoomer)
.call(zoomer.transform, d3.zoomIdentity);
function zoom(e) {
var transform = d3.zoomTransform(this);
var x = 0;
var y = 0;
if(d3.event.sourceEvent) {
var x = d3.event.sourceEvent.layerX;
var y = d3.event.sourceEvent.layerY;
}
var scale = Math.pow(transform.k, .8);
xScale = d3.scaleLinear()
.domain([xExtent[0], xExtent[1] / scale])
.range([0, width]);
yScale = d3.scaleLinear()
.domain([yExtent[0], yExtent[1] / scale])
.range([0, height]);
circle.attr('transform', transform_)
svg.attr("transform", "translate(" + d3.event.transform.x + "," + d3.event.transform.y + ")");
}
function transform_(d) {
var x = xScale(d[0]);
var y = yScale(d[1]);
return "translate(" + x + "," + y + ")";
}
The zoom itself works - basically. Like the normal zoom it should zoom to the position of the mouse pointer, which it doesn't. Also the panning looks a little bit unsmooth.
I tried to use the mouse position from the d3.event.sourceEvent as offset for the translation, but it didn't work.
So, how could the zoom use the mouse position? It would be also great to get smoother panning gesture.
The zoom on mouse pointer can be added using pointer-events attribute.
Also, I have an example for a semantic zoom for d3 version 4 with the mouse pointer and click controls and also displaying the scale value for reference.[enter link description here][1]
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
var randomX = d3.randomNormal(width / 2, 80),
randomY = d3.randomNormal(height / 2, 80),
data = d3.range(20).map(function() {
return [randomX(), randomY()];
});
var scale;
console.log(data);
var circle;
var _zoom = d3.zoom()
.scaleExtent([1, 8])
.on("zoom", zoom);
circle = svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", 5)
.attr("transform", transform(d3.zoomIdentity));
svg.append("rect")
.attr("fill", "none")
.attr("pointer-events", "all")
.attr("width", width)
.attr("height", height)
.call(_zoom);
function zoom() {
circle.attr("transform", transform(d3.event.transform));
scale = d3.event.transform.k;
console.log(scale);
document.getElementById('scale').value = scale;
}
function transform(t) {
return function(d) {
return "translate(" + t.apply(d) + ")";
}
}
var gui = d3.select("#gui");
gui.append("span")
.classed("zoom-in", true)
.text("+")
.on("click", function() {
_zoom.scaleBy(circle, 1.2);
});
gui.append("span")
.classed("zoom-out", true)
.text("-")
.on("click", function() {
_zoom.scaleBy(circle, 0.8);
});
please find the link to fiddle:
[1]: https://jsfiddle.net/sagarbhanu/5jLbLpac/3/
I have a bar chart see plunker the problem is that I would like to move the y-axis ticks to be at the middle left side of the rects but they appear on the top and end. and I cannot seem to move them without destroying the chart.
my code
var info = [{
name: "Walnuts",
value: 546546
}, {
name: "Almonds",
value: 456455
}
];
/* Set chart dimensions */
var width = 960,
height = 500,
margin = {
top: 10,
right: 10,
bottom: 20,
left: 60
};
//subtract margins
width = width - margin.left - margin.right;
height = height - margin.top - margin.bottom;
//sort data from highest to lowest
info = info.sort(function(a, b) {
return b.value - a.value;
});
//Sets the y scale from 0 to the maximum data element
var max_n = 0;
var category = []
for (var d in info) {
max_n = Math.max(info[d].value, max_n);
category.push(info[d].name)
}
var dx = width / max_n;
var dy = height / info.length;
var y = d3.scale.ordinal()
.domain(category)
.range([0, height]);
var yAxis = d3.svg.axis()
.scale(y)
.orient('left')
var svg = d3.select("#chart")
.append("svg")
.attr("width", "100%")
.attr("height", "100%")
.attr('preserveAspectRatio', 'xMidYMin')
.attr("viewBox", '0 0 ' + parseInt(width + margin.left + margin.right) + ' ' + parseInt(height + margin.top + margin.bottom))
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.selectAll(".bar")
.data(info)
.enter()
.append("rect")
.attr("class", function(d, i) {
return "bar" + d.name;
})
.attr("x", function(d, i) {
return 0;
})
.attr("y", function(d, i) {
return dy * i;
})
.attr("width", function(d, i) {
return dx * d.value
})
.attr("height", dy)
.attr("fill", function(d, i) {
if (d.name == 'Walnuts') {
return 'red'
} else {
return 'green'
}
});
var y_xis = svg.append('g')
.attr('id', 'yaxis')
.call(yAxis);
You are using range in y axis like this:
var y = d3.scale.ordinal()
.domain(category)
.range([0, height]);
You should be using 'rangeRoundBands' since the y scale is ordinal
var y = d3.scale.ordinal()
.domain(category)
.rangeRoundBands([0, height], .1);
working code here
For d3 versions like v4/v5.
Defining height as the graph/plot height, and max as the maximum value of y.
import { parseSvg } from 'd3-interpolate/src/transform/parse'
const yScale = d3
.scaleLinear()
.domain([0, max])
.rangeRound([height, 0])
const yAxis = d3.axisLeft(yScale)
svg
.append('g')
.call(yAxis)
.selectAll('.tick')
.each(function(data) {
const tick = d3.select(this)
const { translateX, translateY } = parseSvg(tick.attr('transform'))
tick.attr(
'transform',
translate(translateX, translateY + height / (2 * max))
)
})
Recently I needed something very very similar and I solved this with a call with selecting all text elements in the selection and moving their dy upwards. I will give an example with OP's code:
var y_xis = svg.append('g')
.attr('id','yaxis')
.call(yAxis)
.call(selection => selection
.selectAll('text')
.attr('dy', '-110') // this moves the text labels upwards
.attr('x', '110')); // this does the same job but horizontally
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.