Related
I would like to create a rounded edge for a corner where the user can specify the corner's radius in D3js.
I found a post that has potential solutions, but the examples are in Observable notebook.
I tried converting to plain Javascript. But it didn't work for me.
https://observablehq.com/#carpiediem/svg-paths-with-circular-corners
Any help is much appreciated, thanks.
I think the post you shared may be overly complicated. Assuming you are using d3.line() or d3.area(), I would suggest looking into the different curve interpolators available in D3. Many of them allow an extra parameter to specify, for example, a tension that can be manipulated.
Here it is:
const drag = () => {
function dragstarted(d) {
d3.select(this).raise().attr("stroke", "black");
}
function dragged(d) {
d3.select(this)
.attr("cx", d.x = d3.event.x)
.attr("cy", d.y = d3.event.y);
d3.select('path.angled')
.attr('d', 'M' + points.map(d => `${d.x} ${d.y}`).join(','));
const angle = Math.atan2(points[1].y-points[0].y, points[1].x-points[0].x)
- Math.atan2(points[1].y-points[2].y, points[1].x-points[2].x);
const acuteAngle = Math.min(Math.abs(angle), 2*Math.PI-Math.abs(angle));
const shortestRay = Math.min(
Math.sqrt(Math.pow(points[1].x-points[0].x, 2) + Math.pow(points[1].y-points[0].y, 2)),
Math.sqrt(Math.pow(points[1].x-points[2].x, 2) + Math.pow(points[1].y-points[2].y, 2))
);
const radiusToUse = Math.min( cornerRadius, shortestRay * Math.tan(acuteAngle/2) );
const distanceToTangentPoint = Math.abs(radiusToUse / Math.tan(acuteAngle/2));
const determinant = (points[1].x-points[0].x)*(points[1].y-points[2].y) - (points[1].x-points[2].x)*(points[1].y-points[0].y);
const sweepFlag = determinant < 0 ? 1 : 0;
const anchorIn = alongSegment(points[1], points[0], distanceToTangentPoint);
const anchorOut = alongSegment(points[1], points[2], distanceToTangentPoint);
const manualPathDesc = `
M${points[0].x} ${points[0].y}
L${anchorIn.x} ${anchorIn.y}
A${radiusToUse} ${radiusToUse} 0 0 ${sweepFlag} ${anchorOut.x} ${anchorOut.y}
L${points[2].x} ${points[2].y}
`;
d3.select('path.arced').attr('d', manualPathDesc);
d3.select('rect.anchor.in')
.attr("x", anchorIn.x - 3)
.attr("y", anchorIn.y - 3);
d3.select('rect.anchor.out')
.attr("x", anchorOut.x - 3)
.attr("y", anchorOut.y - 3);
const circleCenter = alongSegment(
points[1],
{ x: (anchorIn.x + anchorOut.x)/2, y: (anchorIn.y + anchorOut.y)/2 },
Math.sqrt(Math.pow(radiusToUse, 2) + Math.pow(distanceToTangentPoint, 2))
);
d3.select('path.triangles')
.attr("d", `M${points[1].x} ${points[1].y} L${circleCenter.x} ${circleCenter.y} L${anchorIn.x} ${anchorIn.y} L${circleCenter.x} ${circleCenter.y} L${anchorOut.x} ${anchorOut.y}`);
d3.select('text.angle').text(`${Math.round(acuteAngle * 180 / Math.PI)}°`);
d3.select('text.shortest').text(Math.round(shortestRay));
d3.select('text.maxradius').text(Math.round(shortestRay * Math.tan(acuteAngle/2)));
d3.select('text.toAnchor').text(Math.round(distanceToTangentPoint));
d3.select('text.determinate').text(determinant < 0 ? 'neg.' : 'pos.');
}
function dragended(d) {
d3.select(this).attr("stroke", null);
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
function alongSegment(from, toward, distanceAlong) {
const bearing = Math.atan2(from.y-toward.y, from.x-toward.x);
return {
bearing,
x: from.x - distanceAlong * Math.cos(bearing),
y: from.y - distanceAlong * Math.sin(bearing)
};
}
const chart = () => {
var color = d3.scaleOrdinal().range(d3.schemeCategory20);
const svg = d3.select("svg")
.attr("viewBox", [0, 0, width, height]);
const angle = Math.atan2(points[1].y-points[0].y, points[1].x-points[0].x)
- Math.atan2(points[1].y-points[2].y, points[1].x-points[2].x);
const acuteAngle = Math.min(Math.abs(angle), 2*Math.PI-Math.abs(angle));
const shortestRay = Math.min(
Math.sqrt(Math.pow(points[1].x-points[0].x, 2) + Math.pow(points[1].y-points[0].y, 2)),
Math.sqrt(Math.pow(points[1].x-points[2].x, 2) + Math.pow(points[1].y-points[2].y, 2))
);
const radiusToUse = Math.min( cornerRadius, shortestRay * Math.tan(acuteAngle/2) );
const distanceToTangentPoint = Math.abs(radiusToUse / Math.tan(acuteAngle/2));
const determinant = (points[1].x-points[0].x)*(points[1].y-points[2].y) - (points[1].x-points[2].x)*(points[1].y-points[0].y);
const sweepFlag = determinant < 0 ? 1 : 0;
const anchorIn = alongSegment(points[1], points[0], distanceToTangentPoint);
const anchorOut = alongSegment(points[1], points[2], distanceToTangentPoint);
const circleCenter = alongSegment(
points[1],
{ x: (anchorIn.x + anchorOut.x)/2, y: (anchorIn.y + anchorOut.y)/2 },
Math.sqrt(Math.pow(radiusToUse, 2) + Math.pow(distanceToTangentPoint, 2))
);
const manualPathDesc = `M${points[0].x} ${points[0].y}
L${anchorIn.x} ${anchorIn.y}
A${radiusToUse} ${radiusToUse} 0 0 ${sweepFlag} ${anchorOut.x} ${anchorOut.y}
L${points[2].x} ${points[2].y}
`;
svg.append('rect')
.attr("x", 8)
.attr("y", 10)
.attr("width", 160)
.attr("height", 105)
.attr("fill", '#eee');
svg.append('text')
.attr("class", 'angle')
.attr("x", 35)
.attr("y", 25)
.attr("text-anchor", "end")
.text(`${Math.round(acuteAngle * 180 / Math.PI)}°`);
svg.append('text')
.attr("class", 'shortest')
.attr("x", 35)
.attr("y", 45)
.attr("text-anchor", "end")
.text(Math.round(shortestRay));
svg.append('text')
.attr("class", 'maxradius')
.attr("x", 35)
.attr("y", 65)
.attr("text-anchor", "end")
.text(Math.round(shortestRay * Math.tan(acuteAngle/2)));
svg.append('text')
.attr("class", 'toAnchor')
.attr("x", 35)
.attr("y", 85)
.attr("text-anchor", "end")
.text(Math.round(distanceToTangentPoint));
svg.append('text')
.attr("class", 'determinate')
.attr("x", 35)
.attr("y", 105)
.attr("text-anchor", "end")
.text(determinant < 0 ? 'neg.' : 'pos.');
svg.append('text')
.attr("x", 40)
.attr("y", 25)
.attr("text-anchor", "start")
.text('angle between rays');
svg.append('text')
.attr("x", 40)
.attr("y", 45)
.attr("text-anchor", "start")
.text('length of shortest ray');
svg.append('text')
.attr("x", 40)
.attr("y", 65)
.attr("text-anchor", "start")
.text('max radius, to fit');
svg.append('text')
.attr("x", 40)
.attr("y", 85)
.attr("text-anchor", "start")
.text('from vertex to anchors');
svg.append('text')
.attr("x", 40)
.attr("y", 105)
.attr("text-anchor", "start")
.text('determinant value');
svg.append('path')
.attr("class", 'arced')
.datum(points)
.attr("d", manualPathDesc)
.attr("stroke", 'orange')
.attr("stroke-width", 5)
.attr("fill", 'none');
svg.append('path')
.attr("class", 'angled')
.attr("d", 'M' + points.map(d => `${d.x} ${d.y}`).join(', '))
.attr("stroke", '#888')
.attr("fill", 'none');
svg.append('rect')
.attr("class", 'anchor in')
.attr("x", anchorIn.x - 3)
.attr("y", anchorIn.y - 3)
.attr("width", 6)
.attr("height", 6)
.attr("fill", '#888');
svg.append('rect')
.attr("class", 'anchor out')
.attr("x", anchorOut.x - 3)
.attr("y", anchorOut.y - 3)
.attr("width", 6)
.attr("height", 6)
.attr("fill", '#ccc');
svg.append('path')
.attr("class", 'triangles')
.attr("d", `M${points[1].x} ${points[1].y} L${circleCenter.x} ${circleCenter.y} L${anchorIn.x} ${anchorIn.y} L${circleCenter.x} ${circleCenter.y} L${anchorOut.x} ${anchorOut.y}`)
.attr("stroke", '#ccc')
.attr("fill", 'none');
svg.selectAll("circle")
.data(points)
.enter()
.append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", 6)
.attr("fill", (d, i) => color(i))
.on("mouseover", function (d) {d3.select(this).style("cursor", "move");})
.on("mouseout", function (d) {})
.call(drag());
return svg.node();
}
const width = 1000;
const height = 600;
const cornerRadius = 50;
const points = d3.range(3).map(i => ({
x: Math.random() * (width - 10 * 2) + 10,
y: Math.random() * (300 - 10 * 2) + 10,
}));
chart();
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="1000" height="600"></svg>
I have a vertical line graph in d3 and I'm trying to add a tooltip in the form of a line and circle that follows the cursor movement in the vertical. The latter works but the circle fails to follow the path - I've tried different variations but the circle never follows the path, it's currently just following the y-axis of the graph (see attached image for example). I've achieved the same effect for a horizontal plot but when I try to adapt the code for a vertical graph I just can't get the circle to work properly.
I've brought an example together with the code below, still very new to javascript so code is a bit of a mess.
Screenshot of graph with circle (red) failing to follow the path:
function test(test_data) {
// setup params
var margin_ = {top: 30, right: 60, bottom: 30, left: 20},
width_ = 300
height_ = 700
// Add svg
var line_graph = d3.select("#my_dataviz_test")
.append("svg")
.attr("width", width_ + 100)
.attr("height", height_)
.append("g")
.attr("transform",
"translate(" + margin_.left + "," + margin_.top + ")");
d3.csv(test_data,
function(d){
return { output_time_ref: d.output_time_ref = +d.output_time_ref,
output_time: d3.timeParse("%d/%m/%Y %H:%M")(d.output_time),
prediction: d.prediction = +d.prediction,
}
},
function(data) {
// Add x axis
var x_test = d3.scaleLinear()
.domain([0, d3.max(data, function(d) { return +d.prediction; })])
.range([ 0, width_ ]);
line_graph.append("g")
.attr("transform", "translate(" + 0 + "," + height_ + ")")
.call(d3.axisBottom(x_test).tickSizeOuter(0).tickSizeInner(0).ticks(2))
.select(".domain").remove();
// Add Y axis
var y_test = d3.scaleLinear()
.domain([0, d3.max(data, function(d) { return +d.output_time_ref; })])
.range([ height_, 0 ]);
line_graph.append("g")
.call(d3.axisLeft(y_test).tickSizeOuter(0).tickSizeInner(0).ticks(5))
.select(".domain").remove();
// Add the line
path_test = line_graph.append("path")
.datum(data)
.attr("fill", "none")
.attr("fill", "steelblue")
.attr("fill-opacity", 0.2)
.attr("stroke", "steelblue")
.attr("stroke-width", 1)
.attr("d", d3.line()
.curve(d3.curveBasis)
.x(function(d) { return x_test(d.prediction) })
.y(function(d) { return y_test(d.output_time_ref) })
)
var mouseG2 = line_graph
.append("g")
.attr("class", "mouse-over-effects");
mouseG2
.append("path")
.attr("class", "mouse-line2")
.style("stroke", "#393B45")
.style("stroke-width", "0.5px")
.style("opacity", 0.75)
mouseG2.append("text")
.attr("class", "mouse-text2")
var totalLength2 = path_test.node().getTotalLength();
var mousePerLine2 = mouseG2.selectAll('.mouse-per-line2')
.data(data)
.enter()
.append("g")
.attr("class", "mouse-per-line2");
mousePerLine2.append("circle")
.attr("r", 8)
.style("stroke", 'red')
.style("fill", "none")
.style("stroke-width", "2px")
.style("opacity", "0");
mouseG2
.append('svg:rect')
.attr('width', width_)
.attr('height', height_)
.attr('fill', 'none')
// .attr('opacity', 0.2)
.attr('pointer-events', 'all')
.on('mouseout', function() {
d3.select("#my_dataviz_test")
.selectAll(".mouse-per-line2 circle")
.style("opacity", "0"); })
var mouseover = function(d) {
d3.select("#my_dataviz_test")
.select(".mouse-line2")
.style("opacity", "1")
.select(".mouse-text2")
.style("opacity", "1")
.select(".mouse-per-line2 circle")
.style("opacity", "1");
///////////////////////////////////////////////////
d3.select("#my_dataviz_test")
var mouse2 = d3.mouse(this);
d3.select("#my_dataviz_test")
.select(".mouse-text2")
.attr("y", mouse2[1])
.attr("transform", "translate(" + (mouse2[1]+60) + "," + (mouse2[1]+5) + ") rotate(90)")
d3.select("#my_dataviz_test")
.select(".mouse-line2")
.attr("d", function() {
var d = "M" + width_ + "," + mouse2[1];
d += " " + 0 + "," + mouse2[1];
return d;
})
d3.select("#my_dataviz_test")
.selectAll(".mouse-per-line2")
.attr("transform", function(d, i) {
var beginning2 = 0,
end2 = totalLength2
target2 = null;
while (true){
target2 = Math.floor((beginning2 + end2) / 2);
var pos2 = path_test.node().getPointAtLength(target2);
if ((target2 === end2 || target2 === beginning2) && pos2.y !== mouse2[1]) {
break;
}
if (pos2.y > mouse2[1]) { end2 = target2; }
else if (pos2.y < mouse2[1]) { beginning2 = target2; }
else {break};
}
d3.select("#my_dataviz_test").select('circle')
.style("opacity", 1)
return "translate(" + (pos2.x) + "," + mouse2[1] +")";
});
///////////////////////////////////////////////////
}
var mouseleave = function(d) {
d3.select("#my_dataviz_test")
.select(".mouse-line2")
.style("opacity", "0")
d3.select("#my_dataviz_test")
.select(".circle")
.style("opacity", "0")
}
line_graph
.on("mouseover", mouseover)
.on("mouseleave", mouseleave)
})
}
test_data.csv:
output_time_ref,output_time,prediction
0,04/01/2013 00:00,0
1,04/01/2013 00:30,0
2,04/01/2013 01:00,0
3,04/01/2013 01:30,0
4,04/01/2013 02:00,0
5,04/01/2013 02:30,0
6,04/01/2013 00:00,0
7,04/01/2013 03:30,0
8,04/01/2013 04:00,0
9,04/01/2013 04:30,8.17E-05
10,04/01/2013 05:00,0.002014463
11,04/01/2013 05:30,0.01322314
12,04/01/2013 06:00,0.033264463
13,04/01/2013 06:30,0.059607438
14,04/01/2013 07:00,0.098553719
15,04/01/2013 07:30,0.145661157
16,04/01/2013 08:00,0.186983471
17,04/01/2013 08:30,0.225206612
18,04/01/2013 09:00,0.267561983
19,04/01/2013 09:30,0.314049587
20,04/01/2013 10:00,0.334710744
21,04/01/2013 10:30,0.350206612
22,04/01/2013 11:00,0.359504132
23,04/01/2013 11:30,0.375
24,04/01/2013 12:00,0.393595041
25,04/01/2013 12:30,0.396694215
26,04/01/2013 13:00,0.393595041
27,04/01/2013 13:30,0.385330579
28,04/01/2013 14:00,0.367768595
29,04/01/2013 14:30,0.344008264
30,04/01/2013 15:00,0.320247934
31,04/01/2013 15:30,0.297520661
32,04/01/2013 16:00,0.273760331
33,04/01/2013 16:30,0.254132231
34,04/01/2013 17:00,0.216942149
35,04/01/2013 17:30,0.167355372
36,04/01/2013 18:00,0.123966942
37,04/01/2013 18:30,0.080785124
38,04/01/2013 19:00,0.041115702
39,04/01/2013 19:30,0.015805785
40,04/01/2013 20:00,0.002489669
41,04/01/2013 20:30,2.67E-05
42,04/01/2013 21:00,1.24E-05
43,04/01/2013 21:30,0
44,04/01/2013 22:00,0
45,04/01/2013 22:30,0
46,04/01/2013 23:00,0
47,04/01/2013 23:30,0
You can precisely compute x by y using the input data:
const y = d3.event.layerY - margin_.top;
const curY = y_test.invert(y);
const minY = Math.floor(curY);
const maxY = Math.ceil(curY);
if (data[minY] && data[maxY]) {
const yDelta = curY - minY;
const minP = data[minY].prediction;
const maxP = data[maxY].prediction;
const curP = minP + (maxP - minP) * yDelta;
const xPos = x_test(curP)
...
}
See it's working in the snippet:
const csvData = `output_time_ref,output_time,prediction
0,04/01/2013 00:00,0
1,04/01/2013 00:30,0
2,04/01/2013 01:00,0
3,04/01/2013 01:30,0
4,04/01/2013 02:00,0
5,04/01/2013 02:30,0
6,04/01/2013 00:00,0
7,04/01/2013 03:30,0
8,04/01/2013 04:00,0
9,04/01/2013 04:30,8.17E-05
10,04/01/2013 05:00,0.002014463
11,04/01/2013 05:30,0.01322314
12,04/01/2013 06:00,0.033264463
13,04/01/2013 06:30,0.059607438
14,04/01/2013 07:00,0.098553719
15,04/01/2013 07:30,0.145661157
16,04/01/2013 08:00,0.186983471
17,04/01/2013 08:30,0.225206612
18,04/01/2013 09:00,0.267561983
19,04/01/2013 09:30,0.314049587
20,04/01/2013 10:00,0.334710744
21,04/01/2013 10:30,0.350206612
22,04/01/2013 11:00,0.359504132
23,04/01/2013 11:30,0.375
24,04/01/2013 12:00,0.393595041
25,04/01/2013 12:30,0.396694215
26,04/01/2013 13:00,0.393595041
27,04/01/2013 13:30,0.385330579
28,04/01/2013 14:00,0.367768595
29,04/01/2013 14:30,0.344008264
30,04/01/2013 15:00,0.320247934
31,04/01/2013 15:30,0.297520661
32,04/01/2013 16:00,0.273760331
33,04/01/2013 16:30,0.254132231
34,04/01/2013 17:00,0.216942149
35,04/01/2013 17:30,0.167355372
36,04/01/2013 18:00,0.123966942
37,04/01/2013 18:30,0.080785124
38,04/01/2013 19:00,0.041115702
39,04/01/2013 19:30,0.015805785
40,04/01/2013 20:00,0.002489669
41,04/01/2013 20:30,2.67E-05
42,04/01/2013 21:00,1.24E-05
43,04/01/2013 21:30,0
44,04/01/2013 22:00,0
45,04/01/2013 22:30,0
46,04/01/2013 23:00,0
47,04/01/2013 23:30,0`;
var margin_ = {top: 30, right: 60, bottom: 30, left: 20},
width_ = 300
height_ = 700
// Add svg
var line_graph = d3.select("#my_dataviz_test")
.append("svg")
.attr("width", width_ + 100)
.attr("height", height_)
.append("g")
.attr("transform",
"translate(" + margin_.left + "," + margin_.top + ")");
const point = line_graph.append('circle')
.attr('r', 5)
.style('fill', 'red');
const data = d3.csvParse(csvData).map(d => ({
output_time_ref: +d.output_time_ref,
output_time: d3.timeParse("%d/%m/%Y %H:%M")(d.output_time),
prediction: +d.prediction,
}));
console.log(data);
// Add x axis
var x_test = d3.scaleLinear()
.domain([0, d3.max(data, d => d.prediction)])
.range([ 0, width_ ]);
line_graph.append("g")
.attr("transform", "translate(" + 0 + "," + height_ + ")")
.call(d3.axisBottom(x_test).tickSizeOuter(0).tickSizeInner(0).ticks(2))
.select(".domain").remove();
// Add Y axis
var y_test = d3.scaleLinear()
.domain([0, d3.max(data, d => +d.output_time_ref)])
.range([ height_, 0 ]);
line_graph.append("g")
.call(d3.axisLeft(y_test).tickSizeOuter(0).tickSizeInner(0).ticks(5))
.select(".domain").remove();
// Add the line
path_test = line_graph.append("path")
.datum(data)
.attr("fill", "none")
.attr("fill", "steelblue")
.attr("fill-opacity", 0.2)
.attr("stroke", "steelblue")
.attr("stroke-width", 1)
.attr("d", d3.line()
.curve(d3.curveBasis)
.x(function(d) { return x_test(d.prediction) })
.y(function(d) { return y_test(d.output_time_ref) })
)
line_graph.on('mousemove', () => {
const y = d3.event.layerY - margin_.top;
const curY = y_test.invert(y);
const minY = Math.floor(curY);
const maxY = Math.ceil(curY);
if (data[minY] && data[maxY]) {
const yDelta = curY - minY;
const minP = data[minY].prediction;
const maxP = data[maxY].prediction;
const curP = minP + (maxP - minP) * yDelta;
const xPos = x_test(curP)
// console.log(xPos);
point
.attr('cx', xPos)
.attr('cy', y)
}
// line_graph
// y_test
})
/*
var mouseG2 = line_graph
.append("g")
.attr("class", "mouse-over-effects");
mouseG2
.append("path")
.attr("class", "mouse-line2")
.style("stroke", "#393B45")
.style("stroke-width", "0.5px")
.style("opacity", 0.75)
mouseG2.append("text")
.attr("class", "mouse-text2")
var totalLength2 = path_test.node().getTotalLength();
var mousePerLine2 = mouseG2.selectAll('.mouse-per-line2')
.data(data)
.enter()
.append("g")
.attr("class", "mouse-per-line2");
mousePerLine2.append("circle")
.attr("r", 8)
.style("stroke", 'red')
.style("fill", "none")
.style("stroke-width", "2px")
.style("opacity", "0");
mouseG2
.append('svg:rect')
.attr('width', width_)
.attr('height', height_)
.attr('fill', 'none')
// .attr('opacity', 0.2)
.attr('pointer-events', 'all')
.on('mouseout', function() {
d3.select("#my_dataviz_test")
.selectAll(".mouse-per-line2 circle")
.style("opacity", "0"); })
var mouseover = function(d) {
d3.select("#my_dataviz_test")
.select(".mouse-line2")
.style("opacity", "1")
.select(".mouse-text2")
.style("opacity", "1")
.select(".mouse-per-line2 circle")
.style("opacity", "1");
d3.select("#my_dataviz_test")
var mouse2 = d3.mouse(this);
d3.select("#my_dataviz_test")
.select(".mouse-text2")
.attr("y", mouse2[1])
.attr("transform", "translate(" + (mouse2[1]+60) + "," + (mouse2[1]+5) + ") rotate(90)")
d3.select("#my_dataviz_test")
.select(".mouse-line2")
.attr("d", function() {
var d = "M" + width_ + "," + mouse2[1];
d += " " + 0 + "," + mouse2[1];
return d;
})
d3.select("#my_dataviz_test")
.selectAll(".mouse-per-line2")
.attr("transform", function(d, i) {
var beginning2 = 0,
end2 = totalLength2
target2 = null;
while (true){
target2 = Math.floor((beginning2 + end2) / 2);
var pos2 = path_test.node().getPointAtLength(target2);
if ((target2 === end2 || target2 === beginning2) && pos2.y !== mouse2[1]) {
break;
}
if (pos2.y > mouse2[1]) { end2 = target2; }
else if (pos2.y < mouse2[1]) { beginning2 = target2; }
else {break};
}
d3.select("#my_dataviz_test").select('circle')
.style("opacity", 1)
return "translate(" + (pos2.x) + "," + mouse2[1] +")";
});
}
var mouseleave = function(d) {
d3.select("#my_dataviz_test")
.select(".mouse-line2")
.style("opacity", "0")
d3.select("#my_dataviz_test")
.select(".circle")
.style("opacity", "0")
}
line_graph
.on("mouseover", mouseover)
.on("mouseleave", mouseleave);
*/
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div id="my_dataviz_test" />
I would like to create an application like scratch or node-red, with D3.js, by this I mean create some svg elements by clicking on a 'button list' to create an element and then drag them over an area to arrange them.
This idea is working with my code below. I can click to create shapes (svg group). Once created, I can click on them (AGAIN) and drag it over svg area.
But, I want to mimic the behavior of same apps node-red and scratch, by dragging the new svg element with the same click used to create it. Sparing a click, in one word. But I don't know how to start drag behavior programmatically on the element created. Here is my working code.
var svg = d3.select("body").append("svg")
.attr("width", 1500)
.attr("height", 800);
addButton(svg, 'ADD');
function addShape(svg, x, y) {
var dotContainer = svg.append("g")
.attr("class", "dotContainer")
.datum({
x: x,
y: y
})
.attr("transform", function(d) {
return 'translate(' + d.x + ' ' + d.y + ')';
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var text = dotContainer.append("text")
.datum({
x: 20,
y: 20
})
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
})
.text('Title');
var rectangle = dotContainer.append("rect")
.attr("width", 200)
.attr("height", 100)
.attr("x", 0)
.attr("y", 0)
.attr('style', "opacity:1;fill:#ffffff;fill-opacity:0;stroke:#000000;stroke-width:5;stroke-opacity:1")
.attr("ry", 8);
return dotContainer;
}
function dragstarted(d) {
let xCoord = d3.event.dx - d3.select(this).attr('x')
let yCoord = d3.event.dy - d3.select(this).attr('y')
}
function dragged(d) {
d3.select(this).select("text").text(d.x + ';' + d.y);
d.x += d3.event.dx;
d.y += d3.event.dy;
d3.select(this).attr("transform", function(d, i) {
return "translate(" + [d.x, d.y] + ")"
});
}
function dragended(d) {
d3.select(this).attr("transform", function(d, i) {
return "translate(" + [d.x, d.y] + ")"
});
}
function addButton(area, title) {
var group = area.append("g");
group.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 100)
.attr("height", 50)
.attr('style', 'fill:rgb(255,0,0);stroke-width:1;stroke:rgb(200,200,200)');
group.append("text")
.attr('x', 20)
.attr('y', 20)
.text(title);
group.on('mousedown', function() {
var grp = addShape(area, 0, 0);
//START DRAG ON grp HERE ???
});
}
<script src="https://d3js.org/d3.v5.min.js"></script>
So, my issue is here that I can't figure out how to call dragstarted() from outside of svg group dotContainer, since dragstarted use this and d, which refers to the svg group. Or use a complete other way to achieve this? I am lost here....
Thanks,
When in doubt, you can always reach back to vanilla JavaScript. In this case, you can dispatch a custom MouseDown event using the d3.event object as the attribute dictionary, essentially cloning the element.
Then, the MouseMove events take over and are processed seamlessly:
var svg = d3.select("body").append("svg")
.attr("width", 1500)
.attr("height", 800);
addButton(svg, 'ADD');
function addShape(svg, x, y) {
var dotContainer = svg.append("g")
.attr("class", "dotContainer")
.datum({
x: x,
y: y
})
.attr("transform", function(d) {
return 'translate(' + d.x + ' ' + d.y + ')';
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var text = dotContainer.append("text")
.datum({
x: 20,
y: 20
})
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
})
.text('Title');
var rectangle = dotContainer.append("rect")
.attr("width", 200)
.attr("height", 100)
.attr("x", 0)
.attr("y", 0)
.attr('style', "opacity:1;fill:#ffffff;fill-opacity:0;stroke:#000000;stroke-width:5;stroke-opacity:1")
.attr("ry", 8);
return dotContainer;
}
function dragstarted(d) {
let xCoord = d3.event.dx - d3.select(this).attr('x')
let yCoord = d3.event.dy - d3.select(this).attr('y')
}
function dragged(d) {
d3.select(this).select("text").text(d.x + ';' + d.y);
d.x += d3.event.dx;
d.y += d3.event.dy;
d3.select(this).attr("transform", function(d, i) {
return "translate(" + [d.x, d.y] + ")"
});
}
function dragended(d) {
d3.select(this).attr("transform", function(d, i) {
return "translate(" + [d.x, d.y] + ")"
});
}
function addButton(area, title) {
var group = area.append("g");
group.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 100)
.attr("height", 50)
.attr('style', 'fill:rgb(255,0,0);stroke-width:1;stroke:rgb(200,200,200)');
group.append("text")
.attr('x', 20)
.attr('y', 20)
.text(title);
group.on('mousedown', function() {
var grp = addShape(area, 0, 0);
grp.node().dispatchEvent(new MouseEvent(
"mousedown",
d3.event
));
});
}
<script src="https://d3js.org/d3.v5.js"></script>
Yo could listen for a mousedown on the button used to create the new shape. In the event listener, you create a new shape and create a new mousedown event which you dispatch immediately on the new element. This new mousedown event will trigger the drag behavior, triggering the drag-start listener once and the drag listener continuously until the mouse is raised. This could look like:
select.on("mousedown", function(event,d) {
// create some new shape:
var aNewShape = container.append("shape")
.attr(...)
....
// create a new event and dispatch it on the new shape
var e = document.createEvent("MouseEvents");
e.initMouseEvent("mousedown", true,true,window,0,0,0,event.x,event.y)
aNewShape.node().dispatchEvent(e)
})
Which could look something like:
var svg = d3.select("body")
.append("svg")
.attr("width",400)
.attr("height", 300);
var data = [
{shape: d3.symbolCross, y: 0, cy: 25, cx: 25},
{shape: d3.symbolWye, y: 60, cy: 85, cx: 25 },
{shape: d3.symbolDiamond, y: 120, cy: 145, cx: 25}
]
// Add some buttons:
var g = svg.selectAll("null")
.data(data)
.enter()
.append("g")
.attr("transform",function(d,i) {
return "translate("+[0,d.y]+")";
})
g.append("rect")
.attr("width", 50)
.attr("height", 50)
.attr("fill", "#ddd");
g.append("path")
.attr("d", function(d) { return d3.symbol().type(d.shape).size(100)(); })
.attr("transform","translate(25,25)")
.attr("fill", "#aaa");
// Some sort of drag function
var drag = d3.drag()
.on("drag", function(event,d) {
d.x = event.x;
d.y = event.y;
d3.select(this).attr("transform", "translate("+[d.x,d.y]+")");
})
.on("start", function() {
d3.select(this).transition()
.attr("fill","steelblue")
.duration(1000);
})
// Mouse down event:
g.on("mousedown", function(event,d) {
var shape = svg.append("path")
.datum({type:d.shape,x:d.cx,y:d.cy})
.attr("d", d3.symbol().type(d.shape).size(300)())
.attr("transform", function(d) { return "translate("+[d.x,d.y]+")" })
.attr("fill","black")
.call(drag);
var e = document.createEvent("MouseEvents");
e.initMouseEvent("mousedown", true,true,window,0,0,0,event.x,event.y)
shape.node().dispatchEvent(e);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
//First part is to create tessellation shapes.
http://jsfiddle.net/NYEaX/1526/
this code creates a tessellation shape of rects - and a circle -- but there must be a cleaner and better way of looping through rects first placing a set of cols and rows.
$(document).ready(function() {
function maskMaker(el) {
var backcolor = $(el).data("color");
var backopacity = $(el).data("opacity");
// Set the main elements for the series chart
var svgroot = d3.select($(el)[0]).append("svg");
var mask = svgroot
.append("defs")
.append("mask")
.attr("id", "myMask");
mask.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", "1200px")
.attr("height", 500)
.style("fill", "white")
.style("opacity", backopacity);
mask.append("circle")
.attr("cx", 350)
.attr("cy", 250)
.attr("r", 70);
function tesselateShape(mask, shape, x, y, size) {
if (shape == "rect") {
mask.append("g")
.attr("transform", "translate(30) rotate(45 50 50)")
.append("rect")
.attr("x", x)
.attr("y", y)
.attr("width", size)
.attr("height", size);
}
}
var size = 200;
var xStart = 500;
var yStart = -500;
var shapes = ["rect", "rect", "rect", "rect", "rect", "rect"];
var batch = 3; //group in 3 and then move into a new col.
var batchCount = 1;
for (i = 0; i < shapes.length; i++) {
tesselateShape(mask, "rect", xStart, yStart, size);
yStart += size;
xStart += size;
if (i % batch) {
var xStart = 500 + (-size * batchCount);
var yStart = -500 + (size * batchCount);
batchCount++;
}
}
/*
//col1
tesselateShape(mask, "rect", 400, -600, 200);
tesselateShape(mask, "rect", 600, -400, 200);
tesselateShape(mask, "rect", 800, -200, 200);
//col2
tesselateShape(mask, "rect", 600, -800, 200);
tesselateShape(mask, "rect", 800, -600, 200);
tesselateShape(mask, "rect", 1000, -400, 200);
*/
var svg = svgroot
.attr("class", "series")
.attr("width", "1200px")
.attr("height", "500px")
.append("g")
.attr("transform", "translate(0,0)")
var rect = svg
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", "1200px")
.attr("height", 500)
.attr("mask", "url(#myMask)")
.style("fill", backcolor);
}
//var el = $(".mask"); //selector
$('[data-role="mask"]').each(function(index) {
console.log("test")
maskMaker(this);
});
});
//Second part is to add in a rounded masked image and label, pointer
Would involve a def pattern part
http://jsfiddle.net/NYEaX/652/
[{
"label": "Google Street View",
"link": "https://maps.googleapis.com/maps/api/streetview?size=600x300&location=46.414382,10.013988&heading=151.78&pitch=-0.76"
}]
a json blob like
var defs = patternsSvg.append('svg:defs');
defs.append('svg:pattern')
.attr('id', index+"-"+value.userName.toLowerCase())
.attr('width', 65)
.attr('height', 65)
.append('svg:image')
.attr('xlink:href', value.userImage)
.attr('x', 0)
.attr('y', 0)
.attr('width', 65)
.attr('height', 65);
labels and pointer
//__labels
var labels = mask.append("g")
.attr("class", "labels")
//__ enter
var labels = labels.selectAll("text")
.data(data);
labels.enter()
.append("text")
.attr("text-anchor", "middle")
//__ update
labels
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
})
.text(function(d) {
return d.label;
})
.each(function(d) {
var bbox = this.getBBox();
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
d.sy = d.oy = d.y + 5;
})
.transition()
.duration(300)
labels
.transition()
.duration(300)
//__ exit
labels.exit().remove();
//__labels
//__labels
//__pointers
var pointers = mask.append("g")
.attr("class", "pointers")
pointers.append("defs").append("marker")
.attr("id", "circ")
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("refX", 3)
.attr("refY", 3)
.append("circle")
.attr("cx", 3)
.attr("cy", 3)
.attr("r", 3);
var pointers = pointers.selectAll("path.pointer")
.data(data);
//__ enter
pointers.enter()
.append("path")
.attr("class", "pointer")
.style("fill", "none")
.style("stroke", "black")
.attr("marker-end", "url(#circ)");
//__ update
pointers
.attr("d", function(d) {
if (d.cx > d.ox) {
return "M" + d.sx + "," + d.sy + "L" + d.ox + "," + d.oy + " " + d.cx + "," + d.cy;
} else {
return "M" + d.ox + "," + d.oy + "L" + d.sx + "," + d.sy + " " + d.cx + "," + d.cy;
}
})
.transition()
.duration(300)
pointers
.transition()
.duration(300)
//__ exit
pointers.exit().remove();
//__pointers
Unable to drag rectangle in d3js. The code looks fine to me. What I am I missing here?
All the drag behaviors like start drag and drag end are implemented but still the drag is not working
Here is my code:
var svgContainer = d3.select("body").append("svg")
.attr("width", 800)
.attr("height", 803);
var rect = svgContainer.append("rect")
.attr("x", 10)
.attr("y", 50)
.attr("width", 51)
.attr("height", 41)
.attr("stroke", "#7E7E7E")
.attr("cursor", "move")
var drag = d3.behavior.drag()
.on("dragstart", dragstart)
.on("drag", drag)
.on("dragend", dragend);
function dragstart()
{
d3.event.sourceEvent.stopPropagation();
}
function drag()
{
var self = d3.select(this);
//var translate =d3.transform(self.getAttribute("transform")).translate;
var translate = d3.transform(self.attr("transform")).translate;
var x = d3.event.dx + translate[0],
y = d3.event.dy + translate[1];
self.attr("transform", "translate(" + x + "," + y + ")");
}
function dragend()
{
var mouseCoordinates = d3.mouse(this);
if (mouseCoordinates[0] > 170)
{
//Append new element
svg.append("g").append("rect")
.attr("x", mouseCoordinates[0])
.attr("y", mouseCoordinates[1])
.attr("width", 51)
.attr("height", 41)
.attr("stroke", "#7E7E7E")
.style('cursor', 'move')
.classed("initialDragRect", true)
.call(drag);
}
}
rect.call(drag);
Change the translate variable declaration on your code in function drag into this
var translate = d3.transform(self.attr("transform")).translate;
The problem is because the self variable does not have the function getAttribute()