Why does my minimap viewport rectangle pan while zooming? - d3.js

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.

Related

Tooltip in d3/topojson choropleth map not working

I have a Choropleth map where the tooltip is working for most of it, but the central states are now showing the tooltip...in face, they are not even running the mouseout callback function at all (tested with a console.log command).
At first I was using d3-tip, and that wasn't working, and it was the first time attempting it, so I thought I might be doing something wrong, so I opted to implement a standard div that toggles between display: none and display: block and when it still wasn't working, I threw in a console.log command to see if the callback function was running at all, and it's not. It's mostly an issue with Kansas, but some of the counties in the surrounding states are having problems too. and I know it's not an issue with the data set, because the example given, which pulls from the same data set is working fine.
Here is the css for the tooltip:
#tooltip{
display: none;
background-color: rgba(32,32,32,1);
position: absolute;
border-radius: 10px;
padding: 10px;
width: 200px;
height: 40px;
color: white
}
and the JS code:
$(function(){
//svg setup
const svgPadding = 60;
const svgWidth = 1000;
const svgHeight = 600;
var svg = d3.select('body')
.append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight)
.attr('id', 'map');
function createChart(topData, eduData){
//scales
var colorScale = d3.scaleSequential(d3.interpolateBlues);
var unitScale = d3.scaleLinear()
.domain(d3.extent(eduData.map(e => e.bachelorsOrHigher)))
.range([0,1])
//map
var path = d3.geoPath();
svg.selectAll('.county')
.data(topojson.feature(topData, topData.objects.counties).features)
.enter()
.append('path')
.attr('class', 'county')
.attr('d', path)
.attr('data-fips', d=>d.id)
.attr('eduIndex', d => eduData.map(e => e.fips).indexOf(d.id))
.attr('data-education', function(){
var index = d3.select(this).attr('eduIndex');
if (index == -1)return 0;
return eduData[
d3.select(this).
attr('eduIndex')
]
.bachelorsOrHigher
})
.attr('fill', function(){
var value = d3.select(this).attr('data-education');
return colorScale(unitScale(value));
})
.attr('stroke', function(){
return d3.select(this).attr('fill');
})
.on('mouseover', function(d){
var index = d3.select(this).attr('eduIndex');
var education = d3.select(this).attr('data-education');
var county = index == -1 ? 'unknown' : eduData[index].area_name;
console.log(county)
var tooltip = d3.select('#tooltip')
.style('left', d3.event.pageX + 10 + 'px')
.style('top', d3.event.pageY + 10 + 'px')
.style('display', 'block')
.attr('data-education', education)
.html(`${county}: ${education}`)
})
.on('mouseout', ()=>d3.select('#tooltip').style('display', 'none'));
svg.append('path')
.datum(topojson.mesh(topData, topData.objects.states, (a,b)=>a.id!=b.id))
.attr('d', path)
.attr('fill', 'rgba(0,0,0,0)')
.attr('stroke', 'black')
.attr('stroke-width', 0.4)
//legend scale
const legendWidth = 0.5 * svgWidth;
const legendHeight = 30;
const numCells = 1000;
const cellWidth = legendWidth/numCells;
const legendUnitScale = d3.scaleLinear()
.domain([0, legendWidth])
.range([0,1]);
//legend
var legend = svg.append('svg')
.attr('id', 'legend')
.attr('width', legendWidth)
.attr('height', legendHeight)
.attr('x', 0.5 * svgWidth)
.attr('y', 0)
for (let i = 0; i < numCells; i++){
legend.append('rect')
.attr('x', i * cellWidth)
.attr('width', cellWidth)
.attr('height', legendHeight - 10)
.attr('fill', colorScale(legendUnitScale(i*cellWidth)))
}
}
//json requests
d3.json('https://raw.githubusercontent.com/no-stack-dub-sack/testable-projects-fcc/master/src/data/choropleth_map/counties.json')
.then(function(topData){
d3.json('https://raw.githubusercontent.com/no-stack-dub-sack/testable-projects-fcc/master/src/data/choropleth_map/for_user_education.json')
.then(function(eduData){
createChart(topData, eduData);
});
});
});
The issue is that you are applying a fill to the state mesh. Let's change the fill from rgba(0,0,0,0) to rgba(10,10,10,0.1):
It should be clear now why the mouse interaction doesn't work in certain areas: the mesh is filled over top of it. Regardless of the fact you can't see the mesh due to it having 0 opacity, it still intercepts the mouse events.
The mesh is meant only to represent the borders: it is a collection of geojson lineStrings (see here too). The mesh is not intended to be filled, it only should have a stroke.
If you change the mesh fill to none, or the pointer events of the mesh to none, then the map will work as expected.

How to rotate SVG using d3.drag()?

I have a simple rectangle appended as a SVG. I want to rotate it with a mouse mouse drag so I used the function d3.drag(). Here is what I have attempted in order to achieve this but it does not seem to work:
<div id = "svgcontainer"></div>
<script language = "javascript">
var width = 300;
var height = 300;
var origin = {
x: 55,
y: -40
};
var svg = d3.select("#svgcontainer")
.append("svg")
.attr("width", width)
.attr("height", height);
var group = svg.append("g");
var rect = group.append("rect")
.attr("x", 20)
.attr("y", 20)
.attr("width", 60)
.attr("height", 30)
.attr("fill", "green")
group.call(d3.drag().on('drag', dragged));
function dragged() {
var r = {
x: d3.event.x,
y: d3.event.y
};
group.rotate([origin.x + r.x, origin.y + r.y]);
};
</script>
When I click on the rectangle and try to drag it to rotate, I am getting some error in the last line with group.rotate(...). Can anyone please sort out the mistake in this code.
group is a d3 selection holding a g, it doesn't have a rotate method, but you can set the transform attribute for the selection with:
group.attr("transform",rotate(θ,cx,cy));
From the example, I'm unsure on how you want to rotate the block, I've set in in the example below to rotate around the center based on the movement of the drag along the x axis:
var width = 300;
var height = 300;
var origin = {
x: 50,
y: 35
};
var svg = d3.select("#svgcontainer")
.append("svg")
.attr("width", width)
.attr("height", height);
var group = svg.append("g");
var rect = group.append("rect")
.attr("x", 20)
.attr("y", 20)
.attr("width", 60)
.attr("height", 30)
.attr("fill", "green")
group.call(d3.drag().on('drag', dragged));
function dragged() {
var r = {
x: d3.event.x,
y: d3.event.y
};
group.attr("transform","rotate("+r.x+","+origin.x+","+origin.y+")" );
};
rect {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<div id="svgcontainer"></div>

how to automatically resize d3.js graph to include axis

I have a bit of a problem building a bar chart. I'm learning d3.js for the first time and being someone who always worked with PHP/MySQL, I haven't had to learn javascript. As a result, I'm struggling a bit.
My question is more conceptual in nature. If, let's say in a bar chart, the Y axis is contained in a g element and the bars are contained in another one, how can I ensure that my axis takes a dyanmic width based on the data presented?
I managed to generate a bar chart and it works great, but the padding is a fixed number (let's say 50px). it works great now, because my numbers go from 0 to 50, so everything fits. What happens if I get trillions instead? The width of the axis will change, yet my padding remains 50px, which means it will clip my content.
What is the "convention" when it comes to this? Any tricks?
Thanks
One trick you might use here is what I like to call the "double-render". You essentially draw the axis first (before the rest of the plot) and get the width of the greatest tick label. The, You can draw the plot conventionally with that value as the margin. This trick is especially useful for string "category" labels, but will work for numbers as well.
Here's a commented example. Run it multiple times to see how it refits the axis:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.bar {
fill: steelblue;
}
.bar:hover {
fill: brown;
}
.axis--x path {
display: none;
}
</style>
<svg width="300" height="300"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="//chancejs.com/chance.min.js"></script>
<script>
// set up some random data
// pick a random max value to render on the yaxis
var maxVal = chance.integer({
min: 1,
max: chance.pickone([1e1, 1e5, 1e10])
}),
// generate some fake data
data = [{
x: chance.word(),
y: chance.floating({
min: 0,
max: maxVal
})
}, {
x: chance.word(),
y: chance.floating({
min: 0,
max: maxVal
})
}, {
x: chance.word(),
y: chance.floating({
min: 0,
max: maxVal
})
}, {
x: chance.word(),
y: chance.floating({
min: 0,
max: maxVal
})
}];
// create svg and set up a y scale, the height value doesn't matter
var svg = d3.select("svg"),
y = d3.scaleLinear().rangeRound([100, 0]);
// set domain
y.domain([0, d3.max(data, function(d) {
return d.y;
})]);
// draw fake axis
var yAxis = svg.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y));
// determine max width of text label
var mW = 0;
yAxis.selectAll(".tick>text").each(function(d) {
var w = this.getBBox().width;
if (w > mW) mW = w;
});
// remove fake yaxis
yAxis.remove();
// draw plot normally
var margin = {
top: 20,
right: 20,
bottom: 30,
left: mW + 10 // max with + padding fudge
},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// reset to actual height
y.range([height, 0]);
var x = d3.scaleBand().rangeRound([0, width]).padding(0.1);
x.domain(data.map(function(d) {
return d.x;
}));
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y));
g.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) {
return x(d.x);
})
.attr("y", function(d) {
return y(d.y);
})
.attr("width", x.bandwidth())
.attr("height", function(d) {
return height - y(d.y);
});
</script>

d3js how to get rotated rect's corner coordinates?

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:

Why is this D3 drag not working?

I'm new to D3 and just started playing with it. I want to move a pre-created rect (or circle) through drag and drop. Here is my fiddle.
// Create the SVG
var svg = d3.select("body").append("svg")
.attr("width", 700)
.attr("height", 400);
// Add a background
svg.append("rect")
.attr("width", 700)
.attr("height", 400)
.style("stroke", "#999999")
.style("fill", "#F6F6F6")
svg.append("circle")
.attr({ cx: 50, cy: 50, r: 5, fill: "red" })
.style("cursor", "pointer")
.call(drag);
svg.append("rect")
.attr({ x: 20, y: 20, width: 10, height: 10, fill: "blue" })
.style("cursor", "pointer")
.call(drag);
// Define drag beavior
var drag = d3.behavior.drag()
.on("drag", dragmove);
function dragmove(d) {
var x = d3.event.x;
var y = d3.event.y;
d3.select(this).attr("transform", "translate(" + x + "," + y + ")");
}
Two questions:
How to make the drag move work?
Why is my rect not showing up?
http://jsfiddle.net/5hemY/1/
Quick debugging led me to find that drag wasn't defined yet when you were trying to .call() it

Resources