One way to draw text and graphics on a chart is to draw on the same canvas after the chart is completely loaded. You can do that implementing your code in a function assigned to the animation.onComplete property. You can also write a simple plugin. Another way to draw over or under a chart is to draw on top of another canvas, and position it exactly over or under your chart canvas. This is easy to do if you won’t be resizing your page. If you do any resizing, you will have to write additional scripts to scale your canvas content to keep it in sync with the chart (in this case, a plugin would be a better solution).
As an example, let's use the GeoJSON world map we loaded and rendered in Chapter 2, Technology Fundamentals, and place it under the bubble chart with the city populations we created in Chapter 4, Creating Charts. Since the map uses a simple cylindrical projection, we just have to make them both the same size, and use CSS absolute positioning to stack one over the other:
<html lang="en">
<head>
<script src="../JavaScript/canvasmap.js" ></script>
<script src=".../Chart.min.js"></script>
<script src=".../papaparse.min.js"></script>
<style>
canvas {
position: absolute;
top: 0;
left: 0;
}
</style>
</head>
<body>
<canvas id="map" width="1000" height="500"></canvas>
<canvas id="my-bubble-chart" width="1000" height="500"></canvas>
<script>...</script>
</body></html>
The drawings also have to start on the same point and use the same scales. The code uses four functions from JavaScript/canvasmap.js: a simple script that draws a map from GeoJSON data:
- map.setCanvas(canvas): receives the background canvas where the map will be drawn
- map.drawMap(geodata): receives an array of GeoJSON features and draws the map
- map.scaleX(longitude) and map.scaleY(latitude): converts latitudes and longitudes into pixel coordinates
The following code obtains the canvas context for the map and sets its fill and stroke styles, loads and parses a GeoJSON file containing shapes for a world map, and a CSV containing city names, populations, latitudes, and longitudes. It then calls functions to draw the map and the chart:
const mapCanvas = document.getElementById("map");
const mapContext = mapCanvas.getContext("2d");
// Map ocean background
mapContext.fillStyle = 'rgb(200,200,255)';
mapContext.fillRect(0, 0, mapCanvas.width, mapCanvas.height);
// countries border and background
mapContext.lineWidth = .25;
mapContext.strokeStyle = 'white';
mapContext.fillStyle = 'rgb(50,50,160';
// setup map canvas
map.setCanvas(mapCanvas); // Function from JavaScript/canvasmap.js
// load files
const files = ['../Data/world.geojson', '../Data/cities15000.csv'];
const promises = files.map(file => fetch(file).then(resp =>
resp.text()));
Promise.all(promises).then(results => {
// Draw the map
const object = JSON.parse(results[0]);
map.drawMap(object.features); // function from
JavaScript/canvasmap.js
// Draw the chart
const data = Papa.parse(results[1], {header: true});
drawChart(data.data); // function described below
});
The radius of each bubble will be somewhat proportional to the population. This function will return a value that fits well in the map:
function scaleR(value) {
const r = Math.floor(value / 100000);
return r != 0 ? r/10 : .25;
}
The drawChart() function uses the parsed CSV datasets to generate an array of location objects, each containing name and the required bubble chart properties: r radius and x, y coordinates. The generated locations array is used as the dataset for the bubble chart:
function drawChart(datasets) {
const locations = [];
datasets.forEach(city => {
const obj = {
x: map.scaleX(+city.longitude), // From
JavaScript/canvasmap.js
y: map.scaleY(-city.latitude), // From
JavaScript/canvasmap.js
r: scaleR(city.population),
name: city.asciiname
};
locations.push(obj);
});
const dataObj = {
datasets: [
{ data: locations,
backgroundColor: function(context) {...}
}
]
}
The options configuration object must configure scales so that there are no margins. Setting min and max properties for the ticks, removing legends and making responsive:false will guarantee this. Tooltips were also configured to show name and population (this is not shown here, but you can see the full code in Multiple/multiple-3-overlay.html):
const chartObj = {
type: "bubble",
data: dataObj,
options: {
scales: {
xAxes: [{ display: false,
ticks: {
min: map.scaleX(-180), // match map size
with
max: map.scaleX(180) // canvas size
}
}
],
yAxes: [{ display: false,
ticks: {
min: map.scaleY(-90), // match map size
with
max: map.scaleY(90) // canvas size
}
}
]
},
tooltips: {...}, // see full code
animation: { duration: 0 },
responsive: false,
legend: { display: false }
}
};
new Chart("my-bubble-chart", chartObj);
}
The final result is as follows. The chart is interactive; you can hover over a large city and get details:
Code: Multiple/multiple-3-overlay.html.
Since we used very large files in this example, it takes a while to load the chart and the tooltips may run a bit slow on some systems. A quick way to optimize it is to reduce the data files previously before loading them. You can also filter and only use the large cities, drawing the small ones separately with Canvas.