Related
I am trying to move a circle by dragging it in d3js, but I can't get it to move in the y-axis direction.
I am using v7 of d3js and from several attempts I have found that the event.x and event.y in the drag function are the coordinates on the graph of the cursor being dragged. Therefore, I tried to transform these two coordinates with the scale transformation function to obtain the coordinates of the display on the svg.
There are two problems here.
The first is that the y-coordinates obtained by event.y are the opposite of the actual vertical movement. For example, if I actually run the code and drag the circle to the upper right, the circle will move to the lower right. This is a line symmetry-like movement in the y-coordinate of the circle's initial position; the positive and negative values of the y-coordinate movement are reversed. This is also true for event.dy. The x-coordinates in event.x and event.dx are working fine. Also, d3.pointer(event) does not work as intended.
The second problem is that the circle jumps at the beginning of the drag. I found the cause of this by looking within the function that is executed during the drag. The initial values of event.x and event.y are the initial values of the circle's coordinates, even after multiple drags, which causes the circle to always start moving from the initial value of the circle when it is dragged.
I have a program that reproduces the problem.
How can these problems be resolved?
Thanks.
Here is the code.
var dataset = [
{x: 5, y: 20, name: "one"},
{x: 100, y: 33, name: "two"},
{x: 250, y: 50, name: "three"},
{x: 480, y: 90, name: "four"},
];
const width = 400;
const height = 300;
const margin = { "top": 30, "bottom": 60, "right": 30, "left": 60 };
const svgWidth = width + margin.left + margin.right;
const svgHeight = height + margin.top + margin.bottom;
var svg = d3.select("#graph-area").append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight)
.style("border-radius", "20px 20px 20px 20px")
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
var xScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d.x; }) + 10])
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d.y; }) + 10])
.range([height, 0]);
var axisx = d3.axisBottom(xScale).ticks(5);
var axisy = d3.axisLeft(yScale).ticks(5);
var x = svg.append("g")
.attr("transform", "translate(" + 0 + "," + height + ")")
.call(axisx)
.attr("class", "x_axis")
x.append("text")
.attr("x", (width - margin.left - margin.right) / 2 + margin.left)
.attr("y", 35)
.attr("text-anchor", "middle")
.attr("font-size", "10pt")
.attr("font-weight", "bold")
.text("X Label")
.attr("class", "x_axis")
var y = svg.append("g")
.call(axisy)
.attr("class", "y_axis")
y.append("text")
.attr("x", -(height - margin.top - margin.bottom) / 2 - margin.top)
.attr("y", -35)
.attr("transform", "rotate(-90)")
.attr("text-anchor", "middle")
.attr("font-weight", "bold")
.attr("font-size", "10pt")
.text("Y Label")
.attr("class", "y_axis")
var clip = svg.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("width", width )
.attr("height", height )
.attr("x", 0)
.attr("y", 0);
var circle = svg.append("g")
.attr("id", "scatterplot")
.attr("clip-path", "url(#clip)");
circle.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) { return xScale(d.x); })
.attr("cy", function(d) { return yScale(d.y); })
.attr("fill", "steelblue")
.attr("r", 20)
.on('mouseover',function(elem, data) {
//console.log("over data name: " + data.name);
})
.on('mouseout',function (elem, data) {
//console.log("out data name: " + data.name);
})
.on("mousedown", function(elem, data){
console.log("down data name: " + data.name);
})
.on("mouseup", function(elem, data){
console.log("up data name: " + data.name);
})
.call(d3.drag().on("drag", circleDragged).on("start", circleStartDrag))
function circleStartDrag(event){
console.log("start drag");
}
function circleDragged(event){
d3.select(this).attr('cx', xScale(event.x)).attr('cy', yScale(event.y));
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>D3 Scatter Plot</title>
<script src="https://code.jquery.com/jquery-3.6.0.slim.min.js" integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI=" crossorigin="anonymous"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
<div id="graph-area" stype="position: relative;"></div
</body>
</html>
Edit (Perception of coordinate calculation in the drag function)
The drag function refers to the values set in the data (cx and cy) and indicates the new coordinates of the cursor reflecting dy and dx from those values. So in my case I have the unscaled data values set as event.subject, so when I drag directly above, it will be calculated as (cx=250, cy=-40) based on (cx=250, cy=-50) and dy=-1. This is then reflected in event.y. This is the reason why the y-coordinates of the circle I was talking about during the drag were upside down.
The correct value is set as (cx=204, cy=150) as a scaled value of the data at the time of enter() execution. When the circle is dragged directly up, event.y is set to cy=140 based on dy=-1. In pixel coordinates in svg, a decrease in the y coordinate value means an upward movement, so the circle moves up. At this point I need to update d.x=event.y to take into account the reference to the value of event.subject in the next drag.
This is the expected result given your code.
You initially position the data points with scale(d.y) which converts your data vales to pixel values. When you drag you take the pixel value (event.y) and apply the scale again - scaling pixel values as though they were data.
The data value of d.y = 0 is mapped to height by the scale. If you drag a circle that is at the top of the SVG (y=0) then the scale will return height which is at the bottom of the SVG - hence your inversion.
The solution is to just use the event.y / event.x values as these values represent pixel values already - there is no need to scale them. If you want to convert the pixel values to data values, then you can use scale.invert() - but this is only useful in updating the data, not the pixel coordinates of each circle.
Removing the scaling functions from the drag reveals one last issue - while the drag moves along with the mouse, you still have a jump on drag start: the drag subject (reference coordinates of the thing being dragged) is by default the x,y properties of your datum. As this represents the data values in your datum, rather than scaled pixel values, you get a jump. The quickest solution would be to redefine the x,y properties of the datum as the scaled pixel values and update these on drag.
var dataset = [
{x: 5, y: 20, name: "one"},
{x: 100, y: 33, name: "two"},
{x: 250, y: 50, name: "three"},
{x: 480, y: 90, name: "four"},
];
const width = 400;
const height = 300;
const margin = { "top": 30, "bottom": 60, "right": 30, "left": 60 };
const svgWidth = width + margin.left + margin.right;
const svgHeight = height + margin.top + margin.bottom;
var svg = d3.select("#graph-area").append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight)
.style("border-radius", "20px 20px 20px 20px")
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
var xScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d.x; }) + 10])
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d.y; }) + 10])
.range([height, 0]);
var axisx = d3.axisBottom(xScale).ticks(5);
var axisy = d3.axisLeft(yScale).ticks(5);
var x = svg.append("g")
.attr("transform", "translate(" + 0 + "," + height + ")")
.call(axisx)
.attr("class", "x_axis")
x.append("text")
.attr("x", (width - margin.left - margin.right) / 2 + margin.left)
.attr("y", 35)
.attr("text-anchor", "middle")
.attr("font-size", "10pt")
.attr("font-weight", "bold")
.text("X Label")
.attr("class", "x_axis")
var y = svg.append("g")
.call(axisy)
.attr("class", "y_axis")
y.append("text")
.attr("x", -(height - margin.top - margin.bottom) / 2 - margin.top)
.attr("y", -35)
.attr("transform", "rotate(-90)")
.attr("text-anchor", "middle")
.attr("font-weight", "bold")
.attr("font-size", "10pt")
.text("Y Label")
.attr("class", "y_axis")
var clip = svg.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("width", width )
.attr("height", height )
.attr("x", 0)
.attr("y", 0);
var circle = svg.append("g")
.attr("id", "scatterplot")
.attr("clip-path", "url(#clip)");
circle.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) { return d.x = xScale(d.x); })
.attr("cy", function(d) { return d.y = yScale(d.y); })
.attr("fill", "steelblue")
.attr("r", 20)
.on('mouseover',function(elem, data) {
//console.log("over data name: " + data.name);
})
.on('mouseout',function (elem, data) {
//console.log("out data name: " + data.name);
})
.on("mousedown", function(elem, data){
console.log("down data name: " + data.name);
})
.on("mouseup", function(elem, data){
console.log("up data name: " + data.name);
})
.call(d3.drag().on("drag", circleDragged).on("start", circleStartDrag))
function circleStartDrag(event){
console.log("start drag");
}
function circleDragged(event){
d3.select(this).attr('cx', d=>d.x=event.x).attr('cy', d=>d.y=event.y);
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>D3 Scatter Plot</title>
<script src="https://code.jquery.com/jquery-3.6.0.slim.min.js" integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI=" crossorigin="anonymous"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
<div id="graph-area" stype="position: relative;"></div
</body>
</html>
I'm making text box in d3 and need to show hyperlink and several text chunks divided by line separators in rect divs like this and code in jsfiddle.
However while I can hard code one div by directing inputting HTML into .append('text').html(...), I don't know what's the best data format to achieve this. As I have multiple textboxes, I would need them to read from the data rather than being hard-coded.
I tried replacing text with HTML (as in the commented-out part of data), but the double quotes in href tends to interfere with the quotation marks on the text field, and the line can get really long. How shall I properly do HTML formatting on textbox in d3? Thanks.
var data = [{
x: 50,
y: 100,
text: "title with hyperlink, para1, para2"
}, {
x: 300,
y: 100,
text: "title2 with hyperlink, para 1"
}];
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 500);
var g = svg.selectAll('.someClass')
.data(data)
.enter()
.append("g")
.attr("class","someClass")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
g.append("rect")
.attr("width", 220)
.attr("height", 40)
.style("fill", "lightblue");
g.append("text")
.style("fill", "black")
.attr('y',10)
.text(function(d) {
return d.text;
})
SVG has foreignObjects which allow to insert HTML directly into an SVG, as long as you give them a proper height and width. To append them in D3, make sure to give the appended elements the xhtml: namespace.
g.append("foreignObject")
.attr("width", 200)
.attr("height", 40)
.append("xhtml:p")
.style("fill", "black")
.html(function(d) {
return d.text;
})
var data = [{
x: 50,
y: 100,
text: "title with hyperlink, para1, para2, and some really long text that will wrap"
}, {
x: 300,
y: 100,
text: "<b>title2 with <br>hyperlink, para 1</b>"
}];
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 500);
var g = svg.selectAll('.someClass')
.data(data)
.enter()
.append("g")
.attr("class","someClass")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
g.append("rect")
.attr("width", 220)
.attr("height", 40)
.style("fill", "lightblue");
g.append("foreignObject")
.attr("width", 220)
.attr("height", 60)
.append("xhtml:p")
.style("fill", "black")
.html(function(d) {
return d.text;
})
<script src="https://d3js.org/d3.v5.min.js"></script>
I'm pretty new to d3js and feeling a little overwhelmed here. I'm trying to figure out how to query a rotated rectangle's corner coordinates so i can place a circle on that location (eventually I'm going to use that as a starting coordinate for a line to link to other nodes).
Here is an image showing what I'm trying to do:
Currently I'm getting the circle on the left of the svg boundary below, I'm trying to place it roughly where the x is below.
Here is my code for the circle:
let rx = node.attr("x");
let ry = node.attr("y");
g.append("circle")
.attr("cx",rx)
.attr("cy",ry)
.attr("r",5);
Here is my jsFiddle: jsFiddle and a Stack Overflow snippet
let d3Root = 'd3-cpm';
let w = document.documentElement.clientWidth;
let h = document.documentElement.clientHeight;
//TODO put type any
let eData = {
width: 180,
height: 180,
padding: 80,
fill: '#E0E0E0',
stroke: '#c3c5c5',
strokeWidth: 3,
hoverFill: '#1958b5',
hoverStroke: '#0046ad',
hoverTextColor: '#fff',
rx: 18,
ry: 18,
rotate: 45,
label: 'Decision Node',
textFill: 'black',
textHoverFill: 'white'
};
let cWidth;
let cHeight = h;
d3.select(d3Root)
.append("div")
.attr("id", "d3-root")
.html(function () {
let _txt = "Hello From D3! <br/>Frame Width: ";
let _div = d3.select(this);
let _w = _div.style("width");
cWidth = parseInt(_div.style("width"));
_txt += cWidth + "<br/> ViewPort Width: " + w;
return _txt;
});
let svg = d3.select(d3Root)
.append("svg")
.attr("width", cWidth)
.attr("height", cHeight)
.call(d3.zoom()
//.scaleExtent([1 / 2, 4])
.on("zoom", zoomed));
;
let g = svg.append("g")
.on("mouseover", function (d) {
d3.select(this)
.style("cursor", "pointer");
d3.select(this).select("rect")
.style("fill", eData.hoverFill)
.style("stroke", eData.hoverStroke);
d3.select(this).select("text")
.style("fill", eData.textHoverFill);
})
.on("mouseout", function (d) {
d3.select(this)
.style("cursor", "default");
d3.select(this).select("rect")
.style("fill", eData.fill)
.style("stroke", eData.stroke);
d3.select(this).select("text")
.style("fill", eData.textFill);
});
let node = g.append("rect")
.attr("width", eData.width)
.attr("height", eData.height)
.attr("fill", eData.fill)
.attr("stroke", eData.stroke)
.attr("stroke-width", eData.strokeWidth)
.attr("rx", eData.rx)
.attr("ry", eData.ry)
.attr("y", eData.padding)
.attr('transform', function () {
let _x = calcXLoc();
console.log(_x);
return "translate(" + _x + "," + "0) rotate(45)";
})
.on("click", ()=> {
console.log("rect clicked");
d3.event.stopPropagation();
//this.nodeClicked();
});
let nText = g.append('text')
.text(eData.label)
.style('fill', eData.textFill)
.attr('x', calcXLoc() - 50)
.attr('y', eData.width + 10)
.attr("text-anchor", "middle")
.on("click", ()=> {
console.log("text clicked");
d3.event.stopPropagation();
//this.nodeClicked();
});
let rx = node.attr("x");
let ry = node.attr("y");
g.append("circle")
.attr("cx",rx)
.attr("cy",ry)
.attr("r",5);
function calcXLoc() {
return (cWidth / 2 - eData.width / 2) + eData.width;
}
function zoomed() {
g.attr("transform", d3.event.transform);
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<d3-cpm></d3-cpm>
You're applying a transform to your rect to position and rotate it. It has no x attribute, so that comes back as undefined. This gets you slightly closer:
let rx = parseInt(node.attr("x"), 10) | 0;
let ry = parseInt(node.attr("y"), 10) | 0;
let height = parseInt(node.attr("height"), 10) | 0;
let transform = node.attr("transform");
g.append("circle")
.attr("cx",rx + height)
.attr("cy",ry + height)
.attr("transform", transform)
.attr("r",5);
But note that this is going to get kind of clunky and difficult to deal with - it'd be better if your data was modeled in such a way that the circular points were handled in there as well and could be somehow derived/transformed consistently....
Updated fiddle: https://jsfiddle.net/dcw48tk6/7/
Image:
I created a CodePen to illustrate my problem:
See the CodePen Simple Minimap dashed Viewport.
I'm part of a small team, my job is (among others) to do the minimap.
The code in the pen is a boiled-down version of our code regarding the minimap.
I'm new to d3js, so I'm happy to hear anything which could be improved about it.
I'm stuck with the bug regarding the dashed rectangle, which is supposed to show what part of the big map we can see.
Panning works, zooming might be finished - the (in my eyes) only technical problem remaining is that the dashed rectangle also pans when you zoom.
The zoom-panning amount is dependant on the mouse position - if the mouse is in the top-left corner, panning is minimal. The further the mouse moves to bottom or right (or both), the more this bug becomes apparent.
I'm using d3.event to get information about pan & zoom amount, and noticed that even if you do not pan at all, if you only zoom, d3.event.translate[0/1] contains values != zero.
Is my approach wrong? Being a new member of the team, I cannot change much of the code, but I can change everything regarding the minimap.
I need to capture pan and zoom events from
var rect = svg.append("rect")
...
.style("pointer-events", "all");
Can anyone tell me what I'm doing wrong? I'm stuck on that problem for days now, any help would be appreciated a lot. Thanks in advance!
HTML:
<div class="canvas" id="canvas"></div>
CSS:
body
{
background-color: #fcfcfc;
}
.canvas
{
position: absolute;
border-radius: 7px;
background-color: #ddd;
left: 7%;
top: 10%;
}
.minimap {
position: absolute;
border-radius: 8px;
border: 2px solid #c2d1f0;
bottom: 2%;
right: 2%;
}
JS:
var width = 800;
var height = 500;
var canvas = d3.select("#canvas")
.attr("width", width)
.attr("height", height);
var zoom = d3.behavior.zoom()
.scaleExtent([0.1, 10])
.on("zoom", zoomed);
var svg = canvas.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.call(zoom);
// invisible rectangle to handle mouse interaction
var rect = svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "none")
.style("pointer-events", "all");
var group = svg.append("g");
function zoomed() {
group.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
// update the minimap whenever pan or zoom have happened
updateMinimap();
}
// an array of circles just to have something to look at and as orientation
var circles = [
{x: 150, y: 100},
{x: 50, y: 50},
{x: 80, y: 350},
{x: 200, y: 150},
{x: 350, y: 200},
{x: 140, y: 300},
{x: 230, y: 280}
];
// draw those circles
group.selectAll("circle")
.data(circles)
.enter()
.append("circle")
.attr("r", 5)
.attr("fill", "red")
.attr("cx", function(d) {return d.x;})
.attr("cy", function(d) {return d.y;})
var minimapScale = 1 / 3; // size of big map times this = size of minimap
var minimapWidth = width * minimapScale;
var minimapHeight = height * minimapScale;
var minimap = canvas.append("svg")
.attr("class", "minimap")
.attr("width", minimapWidth)
.attr("height", minimapHeight);
// run it once so we can see it even if no action was done
updateMinimap();
function updateMinimap() {
// clear outdated objects from minimap
minimap.selectAll("*").remove();
// set default values
var scale = 1;
var dx = 0;
var dy = 0;
if (d3.event) { // overwrite those values when necessary
scale = d3.event.scale;
dx = -d3.event.translate[0] * minimapScale;
dy = -d3.event.translate[1] * minimapScale;
}
// debug output
//console.log("scale: " + scale + ", dx: " + dx + ", dy: " + dy);
// repaint objects on minimap
group.selectAll("*").each(function (circle) {
var cx = circle.x * minimapScale;
var cy = circle.y * minimapScale;
minimap.append("circle")
.attr("r", 2)
.attr("fill", "blue")
.attr("cx", cx)
.attr("cy", cy);
});
// draw the dashed rectangle, indicating where we are on the big map
drawDashedRectangle(scale, dx, dy);
}
function drawDashedRectangle(scale, dx, dy) {
minimap.append("rect")
.attr("x", dx)
.attr("y", dy)
.attr("width", minimapWidth / scale)
.attr("height", minimapHeight / scale)
.style("stroke-dasharray", (10, 5))
.style("stroke-width", 2)
.style("stroke", "gray")
.style("fill", "none");
}
We'd like to have something like that, though we don't need pan/zoom interaction on the minimap. We only need it on the big map, and the minimap should only reflect those changes.
So I've just started my D3 journey, and wanted to ask about how one would create a small 1px border around the chart.
I created the variables "border" and "bordercolor" and then I added .attr("border",border) to the var svg = d3.select("body") portion of my code. It doesn't crash, but I get no border either.
I guess the question is how do i add this border, and if someone could explain why what i did is wrong.
<script type="text/javascript">
//Width and height
var w = 800;
var h = 400;
var padding = 20;
var border=1;
var bordercolor='black';
var dataset = [
[5, 20], [480, 90], [250, 50], [100, 33], [330, 95],[-50,-100],[50,-45],
[410, 12], [475, 44], [25, 67], [85, 21], [220, 88],[-480, 90], [3,-90]
];
// create scale functions
var xScale = d3.scale.linear()
.domain([d3.min(dataset, function(d) { return d[0]; }), d3.max(dataset, function(d) { return d[0]; })])
.range([padding, w - padding * 2]);
var yScale = d3.scale.linear()
.domain([d3.min(dataset, function(d) { return d[0]; }), d3.max(dataset, function(d) { return d[1]; })])
.range([h - padding, padding]);
var rScale = d3.scale.linear()
.domain( [-100, d3.max(dataset, function(d) { return d[1]; })] )
.range([2,5]);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h)
.attr("border",border)
;
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r", 3);
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d[0] + "," + d[1];
})
.attr("x", function(d) {
return xScale(d[0]);
})
.attr("y", function(d) {
return yScale(d[1]);
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "red");
</script>
Use the style attribute to place an outline around the svg:
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("style", "outline: thin solid red;") //This will do the job
.attr("width", w)
.attr("height", h);
The svg var is just a container. You need to add a path or element to the container and then give it the stroke color and width you want for your border. There is more than one way to do this. In this gist I did it by adding a rect with the following values:
var borderPath = svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("height", h)
.attr("width", w)
.style("stroke", bordercolor)
.style("fill", "none")
.style("stroke-width", border);
IMHO it's better to keep separated shape from style instructions:
.append("rect")
.attr("x", 5)
.attr("y", 5)
.attr("height", 40)
.attr("width", 50)
.attr("class","foo")
...
CSS:
svg rect.foo {
fill: white;
stroke-width: 0.5;
stroke: grey;
}
Simply use css:
svg {
border:1px solid black;
}
If X and Y Axis are used, other option is to use tickSizeOuter()
Example:
var yAxis = d3.axisLeft(y).ticks(5).tickSizeOuter(-width);
var xAxis = d3.axisBottom(x).ticks(5).tickSizeOuter(-height);