Is it possible to have a floating x-axis on a d3js grapgh? - d3.js

Have a graph that is very long. And I have the x-axis on the top of the graph. Was wondering if it is possible to have a floating x-axis so when the user scrolls down the graph the x-axis stays on the screen for the user to see?
Thank you for your help

In most case in d3js,
developers create a single svg element. But you can separate them; eg. x/y axis and data drawing area. And then You can assign this svg field statically via css to be constant.
var axisSvg = d3js.select('axisSvg').append('svg').attr('class','axisSvg);
var mainSvg = d3js.select('mainSvg').append('svg').attr('class','maingSvg);
and in css
.axisSvg{
position: fixed;
bottom: 0;
right: 0;
width: 300px;
}

Related

D3.js v7: interpolate translate/scale with new position?

I have a graph of several nodes and edges. I used a layout algorithm (elkjs) to calculate the position of the nodes and edges. The graph is fairly large, with 268 nodes and 276 edges, such that:
svg width: 800, height: 600; graph width: 1844, height: 3007
As such, I had to do the requisite math to calculate pan offsets and scaling so it would fully fit, centered in the viewport:
translate(225.228, 15) scale(0.1896)
I programmatically transition it into place over 2500ms - it works fine and fits nicely.
But then I wish to switch to a subgraph, picking a node to see only it and its descendants. This particular example subgraph is near the bottom of the graph, so the starting position of its y-values are relatively large. After I call the layout algorithm again to get the new positions, the repositioned graph is smaller:
svg width: 800, height: 600; graph width: 874, height: 459
I then do the join/enter/update/exit thing to update the positions over a duration (and remove the other elements not in the subgraph), and I also do the math again for pan offsets and scaling:
translate(20, 100.4348) scale(0.8696)
Here's the problem: while it does end up in the right place, it pans off-screen before panning back on-screen:
I think I see why: the re-positioning transition has the same duration (2500ms) as the panning/scaling, and something about that all combined has that undesirable effect. I'm able to keep the subgraph on-screen by making the positioning happen faster:
rects
.transition().duration(1500) // instead of 2500
.attr('x', (d) => d.x ?? 0)
.attr('y', (d) => d.y ?? 0)
but that's of course not a universal solution, and it kind of moves around haphazardly. I'd rather have a way to interpolate the re-positioning with the translate so it smoothly grows and pans to be centered without going off-screen. Is this possible? I'm aware of d3.interpolate and scaleExtent/translateExtent but am stumped on how to use them - so far I haven't figured out how to account for both repositioning and translate/scale at the same time.
Other relevant bits of code:
<svg
style={{ borderStyle: "Solid", borderWidth: "1px" }}
width="800"
height="600"
onClick={onClickHandler} <!-- this just switches to the subgraph -->
>
<g />
</svg>
const gr = d3.select('g');
const rects = gr.selectAll<SVGSVGElement, ElkNode>('rect').data(elkGraph.children, (d) => d.id);
// update, enter, exit logic omitted; see above for position update
// offset and scale logic omitted
gr.transition().duration(2500)
.attr('transform', `translate(${xCenterOffset}, ${yCenterOffset}) scale(${scaleFactor})`)

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.

Drawing state borders with d3 not working as expected

Here's an example of my problem. Eventually this is going to have county/zipcode info drawn on it.
I create a projection and a geo path:
var projection = d3.geo.albersUsa()
.scale(width * 1.3)
.translate([width / 2, height / 2]);
var path = d3.geo.path().projection(projection);
However when I try to add state borders... this happens:
#state-borders {
fill: none;
stroke: #333;
stroke-width: 1.5px;
stroke-linejoin: round;
stroke-linecap: round;
pointer-events: none;
}
svg.append('path')
.datum(topojson.mesh(states, states.objects.states, function(a, b) { return a !== b; }))
.attr('id', 'state-borders')
.attr('d', path);
I've looked at other examples and they all seem to be doing something similar, I can't figure out what I am doing wrong. Its almost like the stroke-livejoin isnt working at all.
Your geographic data is already projected; it does not need to be projected again. A projection takes unprojected points on a 3d globe and projects them to a 2d space, doing this on data that is already 2d won't work.
Your data doesn't comprise of 3d points (with a coordinate space measured in degrees), but rather has 2d points (with a coordinate space measured in pixels). So we can just use an identity transform as there is no need to modify/project/transform the coordinates. In d3v4+ we could use:
var path = d3.geoPath(); // default projection is an identity
But with d3v3, the default projection of a geo path is AlbersUsa. So we need to explicitly state we don't want a projection:
var path = d3.geo.path().projection(null);
This gives us a working fiddle.
That this file is pre-pojected is not immediately self evident. Generally a map that looks like a ball of yarn is a good indicator that you are projecting already projected data.
Now we may have new problems, working with pre-projected geometry can make sizing difficult, more so in d3v3 - this file is intended for a 960x600 viewport. To automatically scale and center a map based on its features see this quesion, several of the answers are meant for v3 of d3 (a,b). But this gets much easier with the fitSize method that you can use in d3v4+, as described in this answer. And for more of a discussion of working pre-projected geometry there is this question. If you intend on overlaying features on top of the preprojected geometry you are probably best to find an unprojected US topo/geojson to simplify things, as discussed here.

openseadragon svg overlay - clickable area is too large

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.

Retrieving SVG position in Firefox vs Chrome with viewBox set

I'm trying to get the left position of an svg element that has a viewBox set. The viewBox is basically a square, while the actual svg element is more rectangular. In most cases, this isn't a problem (and in Chrome, everything works fine), however, when trying to get the left position of the element within Firefox, as the viewBox is square, Firefox reports the left position of the viewBox rather than the svg element.
See http://jsfiddle.net/c6SW6/11/ for an example which should make things obvious.
Basically, in Chrome, the number reported for the left position is 8 This is the number that I want. In Firefox, it's reported as 108. How do I get Firefox to report the number 8 as well?
Code
HTML
<div>
<svg viewBox="0 0 100 100"><rect x=0 y=0 width=100 height=100></rect></svg>
</div>
<p>
</p>
CSS
div {
height: 100px;
width: 300px;
margin: 0;
padding: 0;
position: absolute;
top: 200px;
}
svg {
background-color: green;
height: 100%;
width: 100%;
}
JS
$('p').text($('svg').offset().left);
Assuming we gave the rect an id="r" attribute...
If you just want the offset of the rect in the svg itself then it's
$('p').text(document.getElementById("r").getCTM().e);
If you want the offset from the page origin instead...
Call rect.getBoundingClientRect() and the left and top will contain the answer
$('p').text(document.getElementById("r").getBoundingClientRect().left);
or alternatively rect.getScreenCTM() the e and f members of the result will be the answer
$('p').text(document.getElementById("r").getScreenCTM().e);
If you rotate the svg using a transform then you'll get different answers, getBoundingClientRect() will give you an unrotated bounding rect but getScreenCTM will give you a transformed offset, since you're not doing that you can use either currently.
The 8 is the difference, i.e. the position of the element element on the page. That's not consistent with the description but if you want 8 then it's:
$('p').text(document.getElementById("r").getScreenCTM().e -
document.getElementById("r").getCTM().e);

Resources