Visit my Blog to get in touch or to see demos of this and much more.
A bubbles visualization for p5js. A fun way to represent quantitative data as set of bubbles that grow, move and collide on the screen.
Here are a couple of examples that you can use to understand how the visualization looks:
- Click here to see an example based on Top Grossing Movies (inflation adjusted)
- Click here to see an example based on Country by Population (2016 figures)
Note: The sample html included within this project contains useful examples of how to use the visualization. Do take a moment to read through this sample file to better understand how it all works.
Important Note on Performance: I discovered that my script can be slow when the bubble labels are long strings. If this is the case for you, then you can probably speed things up by playing with the constant
VIZ_FONT_RECALC
at the top of the script. The higher you set this, the faster things will become - as it cuts down on costly font recalculating. It is currently set to 10px, which is very low. The danger is that, if its too high, you might not get the best fitted font for each bubble. Anyways, do play with this setting if you find performance is an issue for you.
This project is available under the MIT license. Please respect the terms of the license.
This software is released with the karmaware tag
I've done best efforts testing on a range of modern browsers. If you find any problems, do let me know by raising an issue here. Better still, create a fix for the problem too and drop in the changes; that way everyone can benefit from it.
This plug relies on p5js (version 0.5.8+)
Its really easy to use the Bubbles visualization in your own project. You just need to include the script (after the main p5js script) then declare one instance of the Bubbles class and point it to a dataset. The process is all documented below, or just take a look at one of the samples listed above.
In order to use this script, you must declare one instance of the Bubbles class in your code (it must be called 'bubbles') as shown below:
<script>
var bubbles = new Bubbles ("mydata.json");
</script>
or more fully:
<script>
var bubbles = new Bubbles ("mydata.json", "hosting-control", onselect, tooltip);
</script>
The parameters for the Bubbles class are as follows:
Pos | Param | Description | Notes |
---|---|---|---|
1 | data-source | The URL for the JSON list of data items or a local JSON array | See below for examples |
2 | parent-control | The ID of the hosting parent control | Set to null to fill the whole HTML body |
3 | onselect (key, bubble) | The callback function for when a bubble is double-clicked | Set to null for no action |
4 | tooltip (key, bubble) | The callback function to return the bubble tooltip | Set to null for no tooltip |
The data returned from your data-source must be a JSON structure that (at least) has a "key", a "name" and a "count". The "name" is what is displayed inside each bubble and the "count" governs the bubbles area:
{
"foo1": { "name": "bar1", "count": 1000 },
"foo2": { "name": "bar2", "count": 750 },
"foo3": { "name": "bar3", "count": 123.45 }
}
You can also include other data in the JSON structure. These won't be read by Bubbles, but will be passed to your callback functions on-select and tooltip. For example, see the 'year' data item in the example below:
{
"Forrest_Gump" : { "name": "Forrest Gump" , "count": 680016, "year": 1994 },
"The_Godfather" : { "name": "The Godfather", "count": 682680, "year": 1972 },
"Jurassic_Park" : { "name": "Jurassic Park", "count": 795124, "year": 1993 },
}
In these examples and the project samples, we only show static JSON data. In a real system, you could dynamically generate your JSON dataset by reading values from a backing store or database.
The easiest way to have dynamic datasets is to simply reload a new page and point the newly created bubbles object at a new data source. The data source that you point to, does not need to be a static file. You could point to a server side end-point which dynamically creates the required JSON after loading data from a database.
If, for some reason, you don't want to reload a new page (for example you have a one-page app), you can still have dynamic data by calling the restart method at any time and specifying a new data source .
Method | Description |
---|---|
restart (datasource) | Live reloads the animation with a new data source |
Within your code, you can control the color of the bubbles, tooltips and the world background by using CSS. If you do not specify any color information anywhere, the default color set will be used. The defaults are documented further down within this readme.
You can override the default bubble color scheme by specify your alternative colors in some CSS.
<style type="text/css">
.bubbles_bubble { background-color: #FAFAFA; color: #000080; }
.bubbles_bubble:hover { background-color: lightblue; color: indianred; }
.bubbles_world { background-color: lightgrey; }
</style>
The CSS is evaluated on every draw request, so you can even change the CSS dynamically in response to things like user actions or time. Here is a complete list of the CSS selectors you can use along with what each does:
Class | Selector | Description |
---|---|---|
.bubbles_bubble | background-color | The background color of the bubble circle |
.bubbles_bubble | color | The color of the bubble text and bubble rim |
.bubbles_bubble:hover | background-color | The background color of the bubble circle - when the mouse is hovered over it |
.bubbles_bubble:hover | color | The color of the bubble text and bubble rim - when the mouse is hovered over it |
.bubbles_tooltip | background-color | The background color of the bubble tooltip |
.bubbles_tooltip | color | The color of the text on the bubble tooltip |
.bubbles_world | background-color | The background color of the canvas |
You can specify any subset of these CSS selectors - any that you don't specify will fall back to the default color scheme.
If you want to specify a different color for specific (or even each) bubble, you can do that by including CSS information within the source data as below:
{
"DE": {"name": "Germany", "count": 3466},
"UK": {"name": "United Kingdom", "count": 2629,
"css": {
".bubbles_bubble": { "background-color": "DarkSeaGreen", "color": "#006400"},
".bubbles_bubble:hover": { "background-color": "#000" , "color": "hotpink" } } },
"FR": {"name": "France", "count": 2463},
}
The following CSS styles can be overridden within the source data.
- .bubbles_bubble { background-color }
- .bubbles_bubble { color }
- .bubbles_bubble:hover { background-color }
- .bubbles_bubble:hover { color }
You can specify any subset of these CSS selectors - any that you don't specify will fall back to the default color scheme. You can apply per bubble coloring to any subset of your source data - any that you don't specify will fall back to the default color scheme.
The following is the precedence of specified colors on a per selector basis:
- Per bubble colors within the source data
- Any CSS that you have included
- The default coloring scheme
There are several constants at the top of the main script file, which you can modify in order to change the behavior and appearance of the visualization.
The following constants control the font inside the bubbles:
Constant | Default | Unit | Description |
---|---|---|---|
FONT_FACE | Tahoma | string | A browser safe font (btw Tahoma is a good choice as its quite narrow) |
FONT_HEAD | 24 | point | The size of page heading |
FONT_MAX | 36 | point | The largest allowed font |
FONT_MIN | 8 | point | The smallest allowed font |
FONT_STEP | 2 | >= 1 | The font size steps, from big to small |
FONT_PAD | 8 | pixels | Some text padding within the bubble |
The following constants control the default colors of various things:
Note: The best way to control the colors of bubbles is to use CSS - either at page level or within the source data. See the earlier section of this readme for more information about how to do that.
Constant | Default | Unit | Description |
---|---|---|---|
CLR_BACK | [230, 230, 250] | [r,g,b] | Background world color |
CLR_TEXT | [ 0, 102, 153] | [r,g,b] | Bubble text color |
CLR_BALL | [250, 250, 250] | [r,g,b] | Background bubble color |
CLR_HOVR | [100, 0, 0] | [r,g,b] | Hovered bubble text color |
CLR_OVER | [255, 228, 225] | [r,g,b] | Hovered background bubble color |
CLR_TIPS | [252, 240, 173] | [r,g,b] | Tooptip color |
The following constants control factors that influence appearance:
Constant | Default | Unit | Description |
---|---|---|---|
VIZ_WORLD_MARGIN | 10 | pixels | The margin around edge of the world that bubbles will bounce off |
VIZ_RIM_FRACTION | 20 | >= 1 | The fraction of each bubble that is the rim, bigger num = smaller rim |
VIZ_DBL_CLICK | 500 | millsecs | The time window between which two clicks becomes a double click |
VIZ_ALPHA_DEPTH | 0.60 | >0 & <=1 | The depth of fading of small bubbles, bigger num = more fading |
VIZ_CROWDEDNESS | 0.70 | > 0 | How crowded the world space is, bigger = more crowded (0.78 = π/4 is the ratio area of square to circle, which is about optimal) |
VIZ_FRAME_RATE | 50 | >1 & < 60 | The frame rate and hence smoothness of animation, bigger = smoother, but slower overall |
VIZ_FONT_RECALC | 10 | >= 1 | How much the radius must change before a font recalculation (prevents font 'flickering'). See also the note on performance at the top of this readme. |
The following constants control the tooltip appearance:
Constant | Default | Unit | Description |
---|---|---|---|
TIP_WANTED | true | bool | false = no tooltips at all |
TIP_OFFSET | 15 | pixels | The tooltip offset from mouse |
TIP_MARGIN | 4 | pixels | The tooltip margin |
TIP_DELAY | 1500 | millsecs | The tooltip appearance delay |
TIP_POINTSZ | 14 | point | The tooltip font point size |
The following constants control bubble movement and interaction (change with care):
Constant | Default | Unit | Description |
---|---|---|---|
PHYS_REBOUND | 0.75 | 0.0 to 1.0 | The bounce velocity factor of hitting the world boundary |
PHYS_FRICTION | 0.80 | 0.0 to 1.0 | The velocity drag of hitting the world boundary |
PHYS_BIRTH_SPEED | 4.00 | > 0.0 | The intial velocity for new bubbles |
PHYS_REST_JIGGLE | 0.02 | >= 0.0 | The restlessness for existing bubbles (zero means they will eventually come to a complete rest) |
There is absolutely no need to understand the background maths and algorithms to use the bubbles visualization. However, for those who are interested, I have included some details here. I've also tried hard to make the code in the main script as easy as possible to read and follow.
In the visualization, the area of the bubbles is in proportion to the data counts specified in the source data document. I've seen other similar works that scale the radius instead. That is, of course, dead wrong - as it over emphases larger data counts. Given we are presenting our output in a 2D plane, then area is clearly the right scaling domain. Similarly, if you do something like this on a VR headset, then you'd need to scale on volume.
As you can see in the diagram above, it's fairly easy to calculate the required radius of the bubble given a fixed area - its a simple reworking of the formula of the area of a circle.
How do we scale the bubbles for maximal effect? We start by looking at the largest count for all the elements in the source data. We then allocate that as 1000 'units'. We then scale every other element, in proportion to that 1000-unit largest bubble. Bubbles smaller than 1 (in this scale) are rounded up to 1 unit. These units are logical values only, on each draw cycle, we scale the units into the available screen real-estate.
So hence the bubbles are re-scaled on every frame tick. Why? So that it is maximally responsive to resolution changes. Shrink and grow the window whilst the visualization is running to see the effect. The bubbles don't immediately grow to their end size; instead the source data is filtered-in, a frame tick at a time. This gives a more dynamic experience, allowing the bubbles to slowly take on their end sizes.
In order to scale to the available screen real-estate, we start by calculating the available space and then we factor by a 'crowdedness' scalar. The higher this scalar, the more 'bunched up' the bubbles will be. The optimal value for this scalar is pretty much the ratio of the area of a circle to its circumscribed square (which is π / 4 - see background section below). Essentially turning the difficulties of tessellating circles, into the easier problem of tessellating squares.
You can easily change the 'crowdedness' scalar, as it’s just a constant at the top of the main script file. Generally, the bigger your screen real-estate, the more you will need to play around with crowdedness to get the most pleasing results.
Collisions are always fun! On every frame tick, we check if any bubble has collided with any other bubble, using the technique below:
This is also the same code which gets executed whenever you drag a bubble around - causing it to bump into others and pushing them away. It amazing that this high-school trigonometry can lead to such realistic movement - there is some deeper truth in that for sure!
Buried in this script, is a neat way to optimize the fitting of text within a circle with the largest font. I might (should) extract it one day into its own script.
In order to make life easier, we start by finding the inscribed square within the circle; then we find the largest text that fits within that square.
Why the inscribed square? Why not the radius of the circle? Well the radius it a more tempting space, as it larger - but it's only better on the horizontal and vertical lateral. On the diagonals, the text could well 'poke out' the bounds of the circle, if we just use the radius. In fact, using the radius is logically the same as using the circumscribed square (as described earlier when we spoke about crowdedness). But, to be fair, using bounding square at all is just a shortcut. If you are better than me at geometry, please do have a go at treating the text bounds as the true circular circumference.
Now we have the bounding box, we work out all the ways in which we can line-break the phrases inside the box (in the code these are call clauses). Here is the complete, exhaustive line-break set for an example phrase:
As you can see in the code, the best way to get the full list of line-broken strings is to use a recursive function.
So finally, we brute-force all the clauses, for all the font sizes in order to find the one that fills the bounding box with the greatest area. That is expensive (text width measuring costs many machine cycles), but there is also a simple cache so that it is only done once per bubble.
– Pete Rai