openseadragon svg overlay - clickable area is too large - d3.js

I am working with an OpenSeaDragon image with an overlay (array of overlays?) that has about 500 clickable svg rect elements, implemented using the Overlay.onclick() function
The bounding rectangle for the clickable area varies but is always much larger than the visible rectangle, and often covers neighboring rectangles as well. I have tried messing with margin, border, and padding to no avail. This image show an example, showing the difference. The actual displayed rectangle is the same dimension as the visible text box, while the clickable area is the entire highlighted rectangle.
There does not seem to be a lot of predictability - the clickable area varies in an apparently random way but is always larger than the correct size, up to about twice in both directions. As shown, it's not always centered - I'm not sure it ever is. The proportions remain the same when the image is zoomed in and out.
I'm fairly new at JS, and this involves so many components including D3 and the OSD suite, that I'm not sure where to start. Any suggestions would be appreciated!
For reference, here is the code where the boxes are generated from an array. This was adapted from a single rectangle example, and I have no idea if this was a good way to do this. (I would have liked to have the inside of the box fully transparent except when mousing over it, but that's a whole other problem...)
var overlay = this.viewer.svgOverlay();
len = nodes.length;
var d3Rect = [];
var url = [];
for (var i = 0; i < len; i++) {
var mynode = nodes[i];
d3Rect[i] = d3.select(overlay.node()).append("rect")
.style('fill', '#ffffff')
.style('fill-opacity', '0.05')
.style('stroke', '#000066')
.style('stroke-width', '0.0005')
.style('stroke-opacity', '0.5')
.attr("x", mynode.x1)
.attr("width", mynode.width)
.attr("y", mynode.y1)
.attr("height", mynode.height)
.attr("title", mynode.title)
.attr("href", mynode.link);
overlay.onClick(d3Rect[i].node(), function() {
window.open(this.element.getAttribute("href"), '_blank');
});
}

I am not 100% sure, but it looks like you are creating 1 overlay element of unknown size, then adding all the rects inside it, and then binding the click event to the top overlay instead of the individual rects.
In any case, if the shapes you need are simple rects, you should try using
viewer.addOverlay( element, location, placement, onDraw )
link to docs

One possibility is that you're running into precision problems. What are the dimensions of your image in viewport coordinates? By default the width would be 1, but this might cause rounding issues when zoomed in on these SVG elements. Try making your image 1000 wide and multiply all of your overlay coordinates by 1000 and see if that helps.

Related

dc.js heatmap - make the top row rects to begin at y="0"

I have a dc.js heatmap working:
But I want to add grid lines to it, like so:
You can see that the lines to not match up with the bottom edges of the rects. Inserting the lines themselves is easy, you just start at zero and add 11 lines based on the height of the rects, which in this case will always be 11 / chart.effectiveHeight().
The reason they do not match up, seems to be that the top rect row does not always start at 0, instead, there seems to be a random(?) y position that the chart starts at, this will change with the height of the chart container, eg this y position starts at 5:
If it was consistent, then I could just start appending lines from that number instead of 0, but it is not. I have tried a couple of hacky work arounds, however I am unsure as to how to get the y position of all the rects after they are available in the DOM.
Interestingly the demo heatmap does not have this issue:
Here is the code for the heatmap:
const heat_map = dc.heatMap('#heatmap');
heat_map
.width(0)
.height(0)
.margins(margins)
.dimension(hm_dim)
.group(hm_group)
.keyAccessor(function(d) { return +d.key[0]; })
.valueAccessor(function(d) { return +d.key[1]; })
.colorAccessor(function(d) { return +d.value; })
.colors(color_scale)
.calculateColorDomain()
.yBorderRadius(0)
.xBorderRadius(0)
heat_map.render();
Is there a way to force the rects to begin at 0? Or get the random y position for the top rows? I did have a look at the source code but got a bit lost. Also I thought about creating a false group that would include each rect in the grid, and the grid lines could then be rect borders, but I thought that was a bit heavy handed.
Outlining the cells using CSS
It's easy to outline the cells using CSS:
rect.heat-box {
stroke-width: 1;
stroke: black;
}
Example fiddle.
However, as you point out, this only works if all the cells have values; crossfilter will not create the empty ones and I agree it would be absurd fill them in using a fake group just for some lines.
So, to answer your original question...
Why is there a gap at the top of the chart?
The heatmap calculates an integer size for the cells, and there may be space left over (since the space doesn't divide perfectly).
It's kind of nasty but the heatmap example avoids having extra space by calculating the width and height for the chart using the count of cells in each dimension:
chart
.width(45 * 20 + 80)
.height(45 * 5 + 40)
The default margins are {top: 10, right: 50, bottom: 30, left: 30} so this allocates 45x45 pixels for each cell and adds on the margins to get the right chart size.
Since the heatmap in this example draws 20 columns by 5 rows, it will calculate the cell width and height as 45.
Alternative Answer for Responsive/Resizable Charts
I am revisiting this question after rewriting my heatmap chart to be responsive - using the "ResizeObserver" method outlined in the dc.js resizing examples and Gordon's answer to this question
While specifying the chart width and height for the heatmap in Gordon's answer still works, it does not combine well with the resizing method because resized charts will have their .width and .height set to 'null'. Which means that this rounding issue will reoccur and the heat boxes will be again be offset by a random integer x or y value of anywhere between 0 and 5 (unless you want to write a custom resizing function for heatmaps).
The alternative answer is relatively simple and can be determined by selecting just one heat-box element in the heatmap.
The vertical offset value for the heat boxes is the remainder value when the heat-box y attribute is divided by the heat-box height attribute.
const heatbox_y = heat_map.select('.heat-box').attr('y);
const heatbox_height = heat_map.select('.heat-box').attr('height')
const vertical_offset = heatbox_y % heatbox_height
The modulus % will return the remainder.
The horizontal offset can be determined in the same way.
Thus you can append lines to the chart at regular intervals determined by the heatbox_height + the vertical_offset values.
This will work if you pick any heat-box in the chart, and so it is suitable for instances like this where you cannot guarantee that there will be a heat-box at each x or y level. And it means that you are free to set your chart height and width to 'null' if needed.

In d3 selection method chaining, how can I reference the values of the selected element?

I have a simple horizontal bar chart, and I'm trying to place an image to the left of each horizontal bar to serve as a label for the bar.
At this point in the script, the bars have been created and are displayed.
I've taken a code sample for adding the labels to the right of the bars (which are included in the image) and tried to update it to place an image to the left.
bars.append("image")
.attr("class", "bar_image")
//y position of the image is halfway down the bar
.attr("y", function(d){
return y(d.Candidate) + y.rangeBand() / 2 + 4;
})
//x position to left of bar
.attr("x", function(d) {
return -10;
})
.attr('xlink:href', function(d){
return "/static/images/yang.png";
})
My understanding here is that I'm selecting each of the 'bar' elements and appending an image (which is the same image for all right now while I work on placement).
I'm setting the y attribute of the image element to midrange of each y range. This is how the label on the right has its y attr set. x, for now I'm just setting to left of the bar. Then I set the actual href to the image.
My problem now has to do with SIZING the image, and I'd like to be able to set its height/width dynamically based on the height of the 'bar' elements. These heights are obviously dependent on how many bars there are (this graph should scale up or down to any number of people)
Obviously the positioning isn't perfect, but for now I'd just be happy being able to resize the height/width attributes of the image based on the height of the bars in the chart.
Is there a way to reference in the selection and method chaining the attributes of the elements being selected?
Thanks for any help.
Not exactly what I was looking for, but I can set the
attr("height", x)
of the image element using the same function that sets the height attribute of the bars
.attr("height", y.rangeBand())

Issues with Datamaps D3 Pin/marker constant size and relative position while zooming

I have a D3.js Datamaps.js map with pin markers, that I've been trying to implement zooming for.
http://jsbin.com/xoferi/edit?html,output
The image pin markers are 20x20 and the point of the pin (at x+10,y+20) is over the latitude/longitude spot selected.
I'm having issues when zooming the map, with the images drifting from their relative position. For example: take a look a the Iceland pin. As you zoom in it drifts up and to the left.
How can I keep the pins the same size, and in the same relative position while zooming and click-dragging the map?
I've looked for other examples and I'm just not sure what I'm missing. Maybe the viewport size needs to be added to the calculation soemhow. Or maybe the origin of the pin markers needs to be changed from the default. I'm just not sure.
ref: d3 US state map with markers, zooming transform issues
Your original calculations make use of the width/height of the marker being 20x20 pixels:
return latLng[0] - 10;
Your "recalcs" need to do this to as well with the resized image. I would stash the "real" x/y positoin in your data, so you can re-perform the calculations:
datum.realx = latLng[0];
if ( latLng ) return latLng[0] - 10;
And then move them in your zoom handler:
map.svg.selectAll("image")
.attr("height", 20*(1/scale))
.attr("width", 20*(1/scale))
.attr("x", function(d){
return d.realx - (20*(1/scale)/2);
})
.attr("y", function(d){
return d.realy - (20*(1/scale));
});

How to correctly transition stacked bars in d3.js

I'm trying to get a stacked bar chart to animate correctly as bars come and go. There's probably a good example of this somewhere (maybe I'll ask as a separate question), but the examples I'm finding don't show transitions with individual stack elements exiting and entering I want to make sure that as bars are exiting, they drag down the bars above them, and as they're entering, they push up the bars above them. And I don't want any gaps or overlaps midway through the transition.
Can anyone point me to an example that does this?
Correcting my wrong-headed question:
Ashitaka answered the question with a helpful jsfiddle. His answer prompted me to look at the d3 stack layout more closely, where I read:
In the simplest case, layers is a two-dimensional array of values. All of the 2nd-dimensional arrays must be the same length.
So, I concluded I was going about this all wrong. I shouldn't have been trying to remove stack bars at all. If bars in my data were going to disappear, I should leave them in the data and change their height to zero. That way the transitions work great. I haven't yet had to deal with new bars appearing.
One confusing aspect of transitioning stacked charts (and working with SVG in general) is that the coordinate system origin is at the top-left corner, which means that y increases downwards.
First, our data should have 2 y related attributes:
y, the height of the bar
And y0, the baseline or the y position of the bar when it's on top of other bars. This should be calculated by d3.layout.stack().
Then, we should create 2 scales:
One for height, which works exactly as expected:
var heightScale = d3.scale.linear()
.domain([0, maxStackY])
.range([0, height]);
And one for the y position, which works in the reverse way:
var yScale = d3.scale.linear()
.domain([0, maxStackY])
.range([height, 0]);
With these two scales, we can create some functions to calculate the appropriate y positions and heights of our bars:
var barBaseY = function (d) { return yScale(d.y0); };
var barTopY = function (d) { return yScale(d.y0 + d.y); };
var barHeight = function (d) { return heightScale(d.y); };
Next, it's critical that we create a key function so that elements are bound to the correct data:
var joinKey = function (d) { return d.name; };
Without this function D3 would join the data using its index, which would break everything.
Now, to remove or add a set of bars from the stack, we take these steps:
Recalculate the stack:
var newStack = stack(enabledSeries());
Join the new stack with the current selection of layers with the data function:
layers = layers.data(newStack, joinKey);
With our key function, D3 determines the bars that are to be added, removed or updated.
Access the appropriate bars:
layers.enter() contains the "enter selection", that is, the new set of bars to be added.
layers.exit() contains the "exit selection", that is, the set of bars to be removed.
And simply layers contains the "update selection", that is, the bars that are to be updated. However, after enter.append the "update selection" is modified to contain both entering and updating elements. This has changed in D3 v4 though.
Animate the bars:
For added bars, we create them with height 0 and y position barBaseY.
Then we animate all the bars' height and y attributes.
For removed bars, we animate them to height 0 and y position barBaseY, the exact opposite of adding bars. Then we animate all the remaining bars' height and y attributes. D3 is smart enough to render all these animations at the same time.
Here's a pared down version of the stacked chart I linked to in my first comment.
And here's a visual explanation of why you have to animate both y and height attributes to simulate a bar diminishing in size "going down".

d3js scale, transform and translate

I've created nycMap, a project that uses angularJS (MVC), yeoman (build), d3 (mapping) and geoJSON (geo data).
Everything works very nicely, but I did have to spend quite some time getting the right scale and translation. I was wondering how I can automatically figure out at what scale the map will show its best and what x and y values go into the translation?
'use strict';
japanAndCo2App.controller('MainCtrl', function($scope) {
function makeJapanAll(){
var path, vis, xy;
xy = d3.geo.mercator().scale(16000).translate([-5600,2200]);
path = d3.geo.path().projection(xy);
vis = d3.select("#japanAll").append("svg:svg").attr("width", 1024).attr("height", 700);
d3.json("data/JPN_geo4.json", function(json) {
return vis.append("svg:g")
.attr("class", "tracts")
.selectAll("path")
.data(json.features).enter()
.append("svg:path")
.attr("d", path)
.attr("fill",function(d,i){ return d.properties.color || "transparent"});
});
}
makeJapanAll();
});
(If you are interested in the code, it's all on github. The code for the map is in scripts/controllers/main.js which is the same as shown above.)
I've had the same problems. But it is very easy to do when you have a bounding box, which can be determined from the GeoJSON (like meetamit said), or while creating the GeoJson. And the width of the wanted SVG.
I'll start with the variables lattop, lonleft, lonright, width and height for the bounding box of the geojson and the dimensions of the image. I haven't yet occupied myself with calculating a good height from the difference in latutude. So the height is just estimated to be big enough to fit the image. The rest should be clear from the code:
var xym = d3.geo.mercator();
// Coordinates of Flanders
var lattop = 51.6;
var lonleft = 2.4;
var lonright = 7.7;
var width = 1500;
var height =1000;
// make the scale so that the difference of longitude is
// exactly the width of the image
var scale = 360*width/(lonright-lonleft);
xym.scale(scale);
// translate the origin of the map to [0,0] as a start,
// not to the now meaningless default of [480,250]
xym.translate([0,0]);
// check where your top left coordinate is projected
var trans = xym([lonleft,lattop]);
// translate your map in the negative direction of that result
xym.translate([-1*trans[0],-1*trans[1]]);
var path = d3.geo.path().projection(xym);
var svg = d3.select("body").append("svg").attr("width",width).attr("height",height);
Note, if you go over the date line (180 degrees), you will have to take the overflow into account.
Given this:
xy = d3.geo.mercator().scale(someScale).translate([0, 0]);
someScale is the pixel width of the entire world when projected using the mercator projection. So, if your json data had outlines for the whole world – spanning from lat/lng -180,90 to latLng 180,-90 – and if someScale was 1024, then the world would be drawn such that it exactly fits within a 1024x1024-pixel square. That's what you see on in this Google Maps view (well... sort of... not quite... read on...).
That's not enough though. When the world is drawn at 1024px, without any translation, lat/lng 0,0 (i.e. the "middle" of the world) will sit at the 0,0 pixel of the projected map (i.e. the top left). Under these conditions, the whole northern hemisphere and western hemisphere have negative x or y values, and therefore fall outside the drawn region. Also, under these conditions, the bottom right of the world (i.e. lat/lng -90, 180) would sit at the exact middle of the 1024x1024 square (i.e. at pixel 512,512).
So, in order to center the world in the square described here, you need to translate the map by half its width in the X and Y directions. I.e. you need
xy = d3.geo.mercator().scale(1024).translate([512, 512]);
That'll give you exactly the Google Map view I linked to.
If your json data only has part of the world (like, nyc or NY state) drawing it with this xy projection will render the outlines in the correct geographic position relative to the entire 1024x1024 world-spanning region. So it would appear rather small, with lots of whitespace.
The challenge is how to scale and translate the projection such that the area in question fills up the 1024x1024 square. And... so far I haven't answered this question, but I hope that this explanation points you in the right direction towards figuring out this math. I'll also try to continue the answer later, when I have more time. :/
There's an example here that gets the bounds of countries from geojson and then scales and translates the map to that country. The code is a bit ugly; there're however efforts to make this easier in the future (see this and this issue).

Resources