How to Change Position of One Element in a Bubble Chart - javascript

I'm trying to implement a bubble chart in D3.js with an interaction: selecting one bubble causes it to move to the center of the chart, increase size, and display additional information.
d3-hierarchy has the d3.pack layout, which works well for making bubble charts. I can update the data to increase the size of a circle in the selection. However, I don't see any way to set the position of a circle in the layout and get the other circles to arrange themselves around it. I'd need something like "given the circle at (x, y), create a bubble chart around it", and that doesn't exist in d3.pack.
d3-force can also be used to make bubble charts. If I add in some interaction similar to this example using d3.drag, I can make a circle move towards the center like so:
function dragstarted(event) {
const interX = d3.interpolate(event.x, dimensions.width / 2);
const interY = d3.interpolate(event.y, dimensions.height / 2);
if (!event.active) simulation.alphaTarget(0.3).restart();
(function toCenter(i) {
setTimeout(function() {
event.subject.fx = interX(i / 100);
event.subject.fy = interY(i / 100);
i++;
if (i < 100) {
toCenter(i);
} else {
event.subject.fx = null;
event.subject.fy = null;
}
}, 1)
})(1);
}
This only kind of works - it moves the circle to the center, but it runs the force through a few ticks so the circle might not actually end up at the center of the chart. Also, unlike updating data, I don't know how to reset the circles to their original positions when a user wants to dismiss the expanded circle.
My question is, how do I get a circle in a bubble chart to move to the center and have the other circles rearrange themselves? Is there some change I can make to d3.pack or d3.force that would work, or should I be trying a different approach? Another library, even?

Related

How to tell the Highcharts renderer to make the viewport bigger?

In the past I have drawn tables below the chart based on the series. This works because I can tell Highcharts to vertically align the legend, move it to the left, and I can use the legend height and positioning as well as x-axis tick information to figure out where to draw the table lines:
This takes advantage of the fact that once I set the legend vertical, Highcharts is going to internally set the viewport (viewbox?) large enough that the legend is visible, and since my table aligns with the legend, it's also visible.
Now I have a situation where I need to draw a table below the chart that is not based on the series. I actually moved the legend to be floating in the upper right of the chart because there's only one series and it fits nicely there.
I figured out how to position my table lines based on the x-axis label box dimensions (along with tick information), but my table extends out of the viewport (viewbox?) and is not visible:
If I go after the fact and do
myChart.renderer.box.setAttribute("viewBox", "0 0 1200 1200")
to "zoom out", I can see that my whole table did draw, it's in there in the SVG:
So - how do I tell the Highcharts renderer to extend the viewport/viewbox/canvas (whatever the correct term is) down so that when I add my table, it's visible?
I trigger the table drawing from a redraw event handler, and if it's a question of setting some option/parameter I can easily do the calculations I need to figure out how much to extend beforehand and include that in a chart.update({...stuff...}, true) so that it's the right size already when I go to draw my table.
Or I can mess with the chart.renderer directly in code if that's what it will take.
Ok, figured it out.
chart.renderer has a function to setSize, which will set the overall size of the SVG root element itself, however the SVG root is contained within the "highcharts container" div, which has inline CSS styles to set height and width, and importantly, has overflow: hiddden, so you have to also set the height of the container div in order for the increased SVG to be visible. Luckily that is easily accessible at chart.container.
In my case I need to conditionally draw a table after a zoom in or out, so in my function to actually draw the table I also calculate the height it will need and return that so that I can adjust the SVG root and container div after the table is drawn. It looks a little like this:
const drawTable = async (chart, dataForTable) => {
// figure out how much room the table is going to take up
const extraHeight = (rowHeight * dataForTable.length) + littleExtraOffset
// create the SVG container for the table
const group = chart.renderer.g('bottom-table').add();
// draw all the lines for the table etc and add them to the group, then
return {
group,
extraHeight
}
}
const redrawEventHandler = (event) => {
// do stuff to figure out if we need to draw a table or not
let newWidth = baseWidth;
let newHeight = baseHeight;
if (shouldDrawTable) {
const { dataForTable } = this.props;
drawTable(event.target, dataForTable).then(tableResult => {
// save the table SVG group so we can easily destroy it later
this.tableGroup = tableResult.group;
// calculate the new total height
newHeight += tableResult.extraHeight;
this.chart.renderer.setSize(newWidth, newHeight);
this.chart.container.style.height = `${newHeight}px`;
});
} else {
// no table, newWidth and newHeight are still
// the original height and width
this.chart.renderer.setSize(newWidth, newHeight);
this.chart.container.style.height = `${newHeight}px`;
}
}

Vertical line in D3 plot move on zoom/pan

I am trying to draw a vertical line marker in my graph in D3. It is modeled off of this example: https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172
My issue is that after I draw my line, it doesn't move as I zoom/scroll the graph. An example is shown below:
Currently, I have it calculated as a d3.area().
this.pastDateArea = d3.area()
.x(function(d) { return this.x(this.props.pastDate.toDate()) }.bind(this))
.y0(0)
.y1(function(d) { return this.height }.bind(this))
It is appended as
var pastDateData = [{x:this.props.pastDate.toDate(), y:150}]
this.focus.append("path")
.datum(pastDateData)
.attr("class", "area")
.attr("d", this.pastDateArea)
and zoomed/brushed using
//zoom
var t = d3.event.transform;
this.x.domain(t.rescaleX(this.x2).domain());
//brush
this.svg.select(".zoom").call(this.zoom.transform, d3.zoomIdentity
.scale(this.width / (s[1] - s[0]))
.translate(-s[0], 0));
I know there are similar questions to this one (namely, Draw a vertical line representing the current date in d3 gantt chart) but none of them include the zooming/panning features I have in my graph.
Please let me know if you need more information and thanks!
The issue is that you are not updating the vertical bar with each zoom event. Using the code of the example you show, several things are done when the chart is zoomed, including as you note:
x.domain(t.rescaleX(x2).domain()); // update x scale
focus.select(".area").attr("d", area); // redraw chart area
While you do give the new area the class of area, d3.select will only pick the first matching element. So, on zoom, only one .area element is updated (the first encountered, generally the first appended). But, replacing this with d3.selectAll(".area") will not generate the intended results as the area function referenced (.attr("d",area) ) is only used for the first area (that of the graph, not of the vertical bar).
A solution is to select each area (the chart and the bar) independently and update the area with their respective area generators. To do so, append the vertical bar with a unique class name, or an id and use that to select it later. Then when updating the graph on zoom or brush you can use:
x.domain(s.map(x2.invert, x2)); // update x scale
focus.select(".area").attr("d", area); // redraw chart area
focus.select(".bar").attr("d", pastDateArea);// redraw vertical bar
Remember that this needs to be done for both zoom and brush. Also, in the given example, a clip path is assigned in the css for .area, so you need to keep that in mind as well.
Here's a modified example.

How can I keep jointjs cells from overflowing the paper?

I'm using jointjs to make diagrams which will be user-editable. The user may drag them around and relocate each cell. However, when a cell is dragged to the edge, it overflows and becomes cut off. I want to prevent this from happening, instead the cell to stop before it gets to the edge of the paper and not be allowed to cross the edge, thus always staying completely within the paper. The behavior can be seen in jointjs' very own demos here:
http://www.jointjs.com/tutorial/ports
Try dragging the cell to the edge and you'll see that it eventually becomes hidden as it crosses the edge of the paper element.
Secondly, I'm using the plugin for directed graph layout, found here:
http://jointjs.com/rappid/docs/layout/directedGraph
As you can see, the tree position automatically moves to the upper left of the paper element whenever your click layout. How can I modify these default positions? The only options I see for the provided function are space between ranks and space between nodes, no initial position. Say I wanted the tree to appear in the middle of the paper upon clicking 'layout', where would I have to make changes? Thanks in advance for any help.
As an addition to Roman's answer, restrictTranslate can also be configured as true to restrict movement of elements to the boundary of the paper area.
Example:
var paper = new joint.dia.Paper({
el: $('#paper'),
width: 600,
height: 400,
model: graph,
restrictTranslate: true
})
I. To prevent elements from overflowing the paper you might use restrictTranslate paper option (JointJS v0.9.7+).
paper.options.restrictTranslate = function(cellView) {
// move element inside the bounding box of the paper element only
return cellView.paper.getArea();
}
http://jointjs.com/api#joint.dia.Paper:options
II. Use marginX and marginY DirectedGraph layout options to move the left-top corner of the resulting graph i.e. add margin to the left and top.
http://jointjs.com/rappid/docs/layout/directedGraph#configuration
I think my previous answer is still feasible, but this is how I implemented it in my project. It has an advantage over the other answer in that it doesn't require you to use a custom elementView and seems simpler (to me).
(Working jsfiddle: http://jsfiddle.net/pL68gs2m/2/)
On the paper, handle the cell:pointermove event. In the event handler, work out the bounding box of the cellView on which the event was triggered and use that to constrain the movement.
var graph = new joint.dia.Graph;
var width = 400;
var height = 400;
var gridSize = 1;
var paper = new joint.dia.Paper({
el: $('#paper'),
width: width,
height: height,
model: graph,
gridSize: gridSize
});
paper.on('cell:pointermove', function (cellView, evt, x, y) {
var bbox = cellView.getBBox();
var constrained = false;
var constrainedX = x;
if (bbox.x <= 0) { constrainedX = x + gridSize; constrained = true }
if (bbox.x + bbox.width >= width) { constrainedX = x - gridSize; constrained = true }
var constrainedY = y;
if (bbox.y <= 0) { constrainedY = y + gridSize; constrained = true }
if (bbox.y + bbox.height >= height) { constrainedY = y - gridSize; constrained = true }
//if you fire the event all the time you get a stack overflow
if (constrained) { cellView.pointermove(evt, constrainedX, constrainedY) }
});
Edit: I think this approach is still feasible,but I now think my other answer is simpler/better.
The JointJS docs provide a sample where the movement of a shape is contrained to lie on an ellipse:
http://www.jointjs.com/tutorial/constraint-move-to-circle
It works by
Defining a new view for your element, extending joint.dia.ElementView
Overiding the pointerdown and pointermove event in the view to implement the constraint. This is done by calculating a new position, based on the mouse position and the constraint, and then passing this to the base ElementView event handler
Forcing the paper to use your custom element view
This approach can be easily adapted to prevent a shape being dragged off the edge of your paper. In step 2, instead of calculating the intersection with the ellipse as in the tutorial, you would use Math.min() or Math.max() to calculate a new position.

Infinite zooming in force layout d3.js

There is an example where we can click on a circle and see inner circles.
Also there are different examples of the force layout.
Is it possible to have a force layout and each node of it will/can be a circle with inner force layout?
So it will work as infinite zoom (with additional data loading) for these circles.
Any ideas/examples are welcome.
I would approach the problem like this: Build a force-directed layout, starting with one of the tutorials (maybe this one since it build something that uses circle packing for initialization). Add D3's zoom behavior.
var force = d3.layout.force()
// force layout settings
var zoom = d3.behavior.zoom()
// etc.
So far, so good. Except that the force layout likes to hang out around [width/2, height/2], but it makes the zooming easier if you center around [0, 0]. Fight with geometric zooming for a little while until you realize that this problem really demands semantic zooming. Implement semantic zooming. Go get a coffee.
Figure out a relationship between the size of your circles and the zoom level that will let you tell when the next level needs to be uncovered. Something like this:
// expand when this percent of the screen is covered
var coverageThreshold = 0.6;
// the circles should be scaled to this size
var maxRadius = 20;
// the size of the visualization
var width = 960;
// which means this is the magic scale factor
var scaleThreshold = maxRadius / (coverageThreshold * width)
// note: the above is probably wrong
Now, implement a spatial data filter. As you're zooming down, you basically want to hide any data points that have zoomed out of view so that you don't waste gpu time computing their representation. Also, figure out an algorithm that will determine which node the user is zooming in on. This very well might use a Voronoi tessalation. Learn way more than you thought you needed to about geometry.
One more math thing we need to work out. We'll have the child nodes take the place of the parent node, so we need to scale their size based on the total size of the parent. This is going to be annoying and require some tweaking to get right, unless you know the right algorithm... I don't.
// size of the parent node
var parentRadius = someNumberPossiblyCalculated;
// area of the parent node
var parentArea = 2 * Math.PI * parentRadius;
// percent of the the parent's area that will be covered by children
// (here be dragons)
var childrenCoverageRatio = 0.8;
// total area covered by children
var childrenArea = parentArea * childrenCoverageArea;
// the total of the radiuses of the children
var childTotal = parent.children
.map(radiusFn)
.reduce(function(a, b) { return a + b; });
// the child radius function
// use this to generate the child elements with d3
// (optimize that divide in production!)
var childRadius = function(d) {
return maxRadius * radiusFn(d) / childTotal;
};
// note: the above is probably wrong
Ok, now we have the pieces in place to make the magic sauce. In the zoom handler, check d3.event.scale against your reference point. If the user has zoomed in past it, perform the following steps very quickly:
hide the parent elements that are off-screen
remove the parent node that is being zoomed into
add the child nodes of that parent to the layout at the x and y location of the parent
explicitly run force.tick() a handful of times so the children move apart a bit
potentially use the circle-packing layout to make this process cleaner
Ok, so now we have a nice little force layout with zoom. As you zoom in you'll hit some threshold, hopefully computed automatically by the visualization code. When you do, the node you're zooming in on "explodes" into all it's constituent nodes.
Now figure out how to structure your code so that you can "reset" things, to allow you to continue zooming in and have it happen again. That could be recursive, but it might be cleaner to just shrink the scales by a few orders of magnitude and simultaneously expand the SVG elements by the inverse factor.
Now zooming out. First of all, you'll want a distinct zoom threshold for the reverse process, a hysteresis effect in the control which will help prevent a jumpy visualization if someone's riding the mousewheel. You zoom in and it expands, then you have to zoom back just a little bit further before it contracts again.
Okay, when you hit the zoom out threshold you just drop the child elements and add the parent back at the centroid of the children's locations.
var parent.x = d3.mean(parent.children, function(d) { return d.x; });
var parent.y = d3.mean(parent.children, function(d) { return d.y; });
Also, as you're zooming out start showing those nodes that you hid while zooming in.
As #Lars mentioned, this would probably take a little while.

Parallax effect with zoom and rotating

I am currently experimenting with parallax effect that i am planning to implement to my HTML5-canvas game engine.
The effect itself is fairly easy to achieve, but when you add zooming and rotating, things get a little more complicated, at least for me. My goal is to achieve something like this:Youtube video.
As you can see, you can zoom in and out "to the center", and also rotate around it and get the parallax effect.
In my engine i want to have multiple canvases that are going to be my parallax layers, and i am going to translate them.
I came up with something like this:
var parallax = {
target: {
x: Mouse.x,
y: Mouse.y
},
offset: {
x: -ctx.width / 2,
y: -ctx.height / 2
},
factor: {
x: 1,
y: 1
}
}
var angle = 0;
var zoomX = 1;
var zoomY = 1;
var loop = function(){
ctx.canvas.width = ctx.canvas.width; //Clear the canvas.
ctx.translate(parallax.target.x * parallax.factor.x, parallax.target.y * parallax.factor.y);
ctx.rotate(angle);
ctx.scale(zoomX, zoomY);
ctx.translate((-parallax.target.x - parallax.offset.x) * parallax.factor.x, (-parallax.target.y - parallax.offset.y) * parallax.factor.y);
Draw(); //Function that draws all the objects on the screen.
}
This is a very small and simplified part of my script, but i hope that's enough to get what i am doing. The object "parallax" contains the target position, the offset(the distance from the target), and the factor that is determining how fast the canvas is moving away relatively to the target. ctx is the canvas that is moving in the opposite direction of the target.(In this example i am using only one layer.) I am using the mouse as the "target", but i could also use the player, or some other object with x and y property. The target is also the point around which i rotate and scale the canvas.
This method works completely fine as long as the factor is equal to 1. If it is something else, the whole thing suddenly stops working correctly, and when i try to zoom, it zooms to the top-left corner, not the target. I also noticed that if i zoom out too much, the canvas is not moving in the opposite way of the target, but in the same direction.
So my question is: What is the correct way of implementing parallax with zooming and rotating?
P.S. It is important to me that i am using canvases as the layers.
To prepare for the next animation frame, you must undo any previous transforms in the reverse order they were executed:
context.translate(x,y);
context.scale(sx,sy);
context.rotate(r);
// draw stuff
context.rotate(-r);
context.scale(-sx,-sy);
context.translate(-x,-y);
Alternatively, you can use context.save / context.restore to undo the previous transforms.
Adjust your parallax values for the current frame,
Save the un-transformed context state using context.save(),
Do your transforms (translate, scale, rotate, etc),
Draw you objects as if they were in non-transformed space with [0,0] at your translate point,
Restore your context to it's untransformed state using context.restore()/
Either way will correctly give you a default-oriented canvas to use for your next animation frame.
The exact parallax effects you apply are up to your own design, but using these methods will make the canvas return to a normal default state for you to design with.

Categories