This is an answer to the original question about this code. Since it was posted the question has been changed several times - the requirement to save as PNG was added, the number of charts was changed from 1 in the original code to 4 and the framework used was changed from Chart.js rendering on HTML Canvas to Raphaël rendering on SVG. I am leaving the solutions that I posted in hope that it will be useful to someone in the future.
I have few ideas here:
Finding pixels
A slower but a sure way: knowing that you are interested in black pixels, you can iterate over all pixels of the canvas and remember 4 numbers: the smallest and biggest x and y coordinates of any black pixel that you find. You can add some margin to that and you'll have a rectangle that is always spot on, even when the library starts to write the text in a different place in future versions.
You'll have to recalculate it every time a window is resized, after the canvas is redrawn.
For that to work your text will have to be in a color that is not present anywhere else on the canvas (which is currently the case).
Guess coordinates
You can guess the coordinates - or calculate them, to be more precise - knowing how it is drawn. It seems that the big circle is taking the entire space on the smaller dimension (the entire height in this case) and is centered in the other axis (in this case it's centered horizontally).
Using that knowledge you can calculate the size and position of the inner (white) circle having only the canvas dimension in a way similar to this:
Find which width or height of the canvas is smaller and use it as a base number. Dividing it by 2 will give you R
- the radius of the big circle. Dividing R/2
will roughly give you r
- the radius of the small, internal white circle. Calculate x
and y
- coordinates of the center of the canvas - x = width/2
and y = height /2
.
Now you can experiment with the rectangle where the text will be. It may be something like: x - 0.7*r
and x + 0.7*r
for left and right edges and y - 0.4*r
and y + 0.4*r
for the bottom and top edges. Those are just examples, you can tweek those numbers to your satisfaction.
Those numbers don't have to be perfect because you should have a few pixels of margin around the text anyway.
Here it may not work when the library starts to draw it completely differently in the future, but it probably won't for a simple chart like this.
The good thing is that you don't have to look for specific colors and that calculation will be faster that examining every pixel.
Again, you have to recalculate those dimensions if the chart ever gets redrawn with a different size.
Change your pluginService
Another idea would be to change you pluginService
's beforeDraw
function so that it saves the numbers that it already has.
In particular, you already have:
textX = Math.round((width - ctx.measureText(text).width) / 2),
textY = height / 2;
If you change it to:
var measured = ctx.measureText(text);
textX = Math.round((width - measured.width) / 2),
textY = height / 2;
(just to avoid recalculating the text measurement later) then you can store somewhere the following numbers:
Either just textX
and textY
together with measured.width
and measured.height
or maybe an object with following properties:
var textPos = {
x1: textX,
y1: textY,
x2: textX + measured.width,
y2: textY + measured.height
}
Make sure to use rounding if you need to. You can store that object for example in some global object, or as a data-*
attribute of some HTML element (like on the canvas itself).
This last solution is nice because you don't have to worry about color, you don't have to guess where the text will be put because you know that exactly, and you don't have to worry about recalculation of this on resize because that code runs every time the text itself is drawn.
The drawback is that you need to modify your pluginService
.
A div over canvas
Another way is putting a div over your canvas and putting your text in that div instead of in the canvas. That way you have all the convenience of adding event listeners etc.
You can do something like this:
Put your canvas and and empty div (or more divs) inside a bigger div:
<div id="chart">
<canvas id="myChart"></canvas>
<div class="chart-text" id="text1"></div>
</div>
You can add more divs like the text1 for more circles/charts, like this:
<div id="chart">
<canvas id="myChart"></canvas>
<div class="chart-text" id="text1"></div>
<div class="chart-text" id="text2"></div>
</div>
Add this CSS to have them stack properly:
#chart { position: relative; }
.chart-text { position: absolute; }
And now you add your text to that inner div instead of drawing it on the canvas:
var text1 = document.getElementById('text1');
text1.addEventListener("click", function (e) {
alert("CLICKED!");
});
Chart.pluginService.register({
beforeDraw: function(chart) {
var width = chart.chart.width,
height = chart.chart.height;
var fontSize = (height / 114).toFixed(2);
text1.style.font = fontSize + "em sans-serif";
var text = "75%";
text1.innerText = text;
var r = text1.getBoundingClientRect();
text1.style.left = ((width-r.width)/2)+"px";
text1.style.top = ((height-r.height)/2)+"px";
}
});
See DEMO.
It can probably be simplified but it is probably simpler that putting the text inside of the canvas, and you can have event listeners or easy CSS styling. For example adding:
.chart-text:hover { color: red; }
will make it red on hover.
Empty div over canvas
Here is yet another update after posting another requirements in the comments that were not included in the question.
You can have this HTML as in the version above:
<div id="chart">
<canvas id="myChart"></canvas>
<div class="chart-text" id="text1"></div>
</div>
But this time you can add an empty div over your canvas, so that way the text is included in the canvas and saving will it will include the text.
Here is CSS that is needed:
#chart { position: relative; }
.chart-text { position: absolute; }
Here is CSS that will show you the position of the invisible div:
#chart { position: relative; }
.chart-text { position: absolute; border: 1px solid red; }
And now the code to put the div where it should be:
var text1 = document.getElementById('text1');
text1.addEventListener("click", function (e) {
alert("CLICKED!");
});
Chart.pluginService.register({
beforeDraw: function(chart) {
var width = chart.chart.width,
height = chart.chart.height,
ctx = chart.chart.ctx;
ctx.restore();
var fontSize = (height / 114).toFixed(2);
ctx.font = fontSize + "em sans-serif";
ctx.textBaseline = "middle";
var text = "75%",
m = ctx.measureText(text),
textX = Math.round((width - m.width) / 2),
textY = height / 2;
var emInPx = 16;
text1.style.left = textX + "px";
text1.style.top = (textY - fontSize*emInPx/2) + "px";
text1.style.width = m.width + "px";
text1.style.height = fontSize+"em";
ctx.fillText(text, textX, textY);
ctx.save();
}
});
Make sure that the emInPx
has the correct numper of px
(CSS pixels) per one em
unit. You define the fontSize
in em
units and we need pixels to calculate the correct position.
See DEMO
(it has a red border to make the div visible - just remove border: 1px solid red;
from CSS to make it disappear)
Big empty div over canvas
Another example - this time the div is bigger than the text:
var text1 = document.getElementById('text1');
text1.addEventListener("click", function (e) {
alert("CLICKED!");
});
Chart.pluginService.register({
beforeDraw: function(chart) {
var width = chart.chart.width,
height = chart.chart.height,
ctx = chart.chart.ctx;
ctx.restore();
var fontSize = (height / 114).toFixed(2);
ctx.font = fontSize + "em sans-serif";
ctx.textBaseline = "middle";
var text = "75%",
m = ctx.measureText(text),
textX = Math.round((width - m.width) / 2),
textY = height / 2;
var d = Math.min(width, height);
var a = d/2.5;
text1.style.left = ((width - a) / 2) + "px";
text1.style.top = ((height - a) / 2) + "px";
text1.style.width = a + "px";
text1.style.height = a + "px";
ctx.fillText(text, textX, textY);
ctx.save();
}
});
See DEMO.
It doesn't depend on the em
size in px
and on the text size.
This line changes the size of the square:
var a = d / 2.5;
You can try changing the 2.5 to 2 or 3 or something else.
Round empty div over canvas
This is a variant that uses border-radius
to make a round div instead of rectangular and seems to fill up the inner white circle perfectly.
HTML:
<div id="chart">
<canvas id="myChart"></canvas>
<div class="chart-text" id="text1"></div>
</div>
CSS:
#chart, #myChart, .chart-text { padding: 0; margin: 0; }
#chart { position: relative; }
.chart-text { position: absolute; border-radius: 100%; }
JS:
var text1 = document.getElementById('text1');
text1.addEventListener("click", function (e) {
alert("CLICKED!");
});
Chart.pluginService.register({
beforeDraw: function(chart) {
var width = chart.chart.width,
height = chart.chart.height,
ctx = chart.chart.ctx;
ctx.restore();
var fontSize = (height / 114).toFixed(2);
ctx.font = fontSize + "em sans-serif";
ctx.textBaseline = "middle";
var text = "75%",
m = ctx.measureText(text),
textX = Math.round((width - m.width) / 2),
textY = height / 2;
var d = Math.min(width, height);
var a = d / 2;
text1.style.left = (((width - a) / 2 - 1)|0) + "px";
text1.style.top = (((height - a) / 2 - 1)|0) + "px";
text1.style.width = a + "px";
text1.style.height = a + "px";
ctx.fillText(text, textX, textY);
ctx.save();
}
});
See DEMO.
Raphael
in the central hole of each? – Stiff