Javascript Draw Concentric Circles Canvas
Recently one of our customers wanted to accept a webpage with an illustration including concentric circles / rings with clickable text on the arcs. When the user clicks on the texts nosotros should display related information from SharePoint lists, but information technology is irrelevant to the primary topic of this post. Our first idea was to create a static paradigm map, only the client informed us, that the texts and the arrangement might change several times, and so I preferred some kind of dynamic solution and decided to utilise HTML5 and JavaScript.
In this post I would like to introduce the results of my piece of work and share the code with the community.
First I created a few JavaScript "classes" describing entities similar a circumvolve / band (with attributes similar eye, inner and outer radius) and arc (with attributes like kickoff and terminate angle, text and text alignment). Then I defined helper functions that enable creation of rings or appending arcs adjacent to each other. Finally I wrote the necessary methods to draw the arcs, to write text along the arcs (supporting multiline text, various alignments like align to left, correct, centre or justify, and support to write text acme-downwardly at the upper part of the circle and lesser-up at the lower function), and to detect if the mouse is over an arc (and if it is, highlight the arc), and to react on mouse click.
Note: Multiline text support in this version is limited to explicit line breaks ('\n' in text), no automatic word wrapping.
The algorithms in the solution require a solid understanding of trigonometry. Fortunately, I constitute several resources on the spider web (encounter the source lawmaking at the lesser for related URLs) that helped me in the first steps.
The screenshot below illustrates a sample including a few rings and arcs for each band. Note the diverse text directions (at the superlative and bottom parts of the circles, like the directions of "Test 2" versus direction of "Test 3") and text alignments.
Note: In this implementation the circles and arcs are hardcoded, but you are free to get the data to initialization from a background system, like SharePoint lists or so.
When a mouse hovers over an arc, the arc is highlighted with an other background color.
Annotation: Using a significantly unlike background color for highlighting does not work well (you tin can try it for example using cerise instead of gray, like illustrated below), every bit afterward the mouse moves out from the area and the original background color is applied over again, the borders of the arcs remain "muddied".
Finally, the arc reacts on mouse click with an alert including the text of the arc, nonetheless yous can extend the solution with more advanced interaction, like calling Rest interfaces to display the response dynamically.
Beneath I publish the whole source lawmaking of this sample solution.
- <! DOCTYPE HTML >
- < html >
- < head >
- < style >
- body
- {
- margin: 0px;
- padding: 0px;
- }
- </ style >
- </ caput >
- < body >
- < canvass id ="myCanvas" width ="800" summit ="800"></ sail >
- < script >
- // http://www.html5canvastutorials.com/tutorials/html5-sheet-arcs/
- // http://world wide web.html5canvastutorials.com/labs/html5-canvas-text-along-arc-path/
- var canvas = certificate.getElementById('myCanvas');
- canvas.addEventListener('mousemove', track_mouse, imitation);
- canvas.addEventListener('click', click_mouse, false);
- TextAlignment = {
- Left : 0,
- Center : 1,
- Right : 2,
- Justify : 3
- }
- function point (x, y) {
- this.x = 10;
- this.y = y;
- }
- function band (center, minRadius, maxRadius) {
- this.middle = center;
- this.minRadius = minRadius;
- this.maxRadius = maxRadius;
- }
- function arc (ring, startAngle, endAngle, text, alignment) {
- this.ring = band;
- this.startAngle = startAngle;
- this.endAngle = endAngle;
- this.text = text;
- this.alignment = (alignment != undefined) ? alignment : TextAlignment.Center;
- this.createArcAfter = role (angle, text) {
- render new arc(this.band, this.endAngle, this.endAngle + bending, text);
- };
- this.createArcAfterUpTo = function (upToArc, text) {
- return new arc(this.ring, this.endAngle, upToArc.startAngle, text);
- };
- this.isInside = function (pos) {
- // http://stackoverflow.com/questions/6270785/how-to-make up one's mind-whether-a-point-ten-y-is-contained-inside-an-arc-department-of-a-c
- // Angle = arctan(y/10); Radius = sqrt(x * x + y * y);
- var upshot = false;
- var radius = Trig.distanceBetween2Points(pos, this.ring.heart);
- // we summate atan simply if the radius is OK
- if ((radius >= this.ring.minRadius) && (radius <= this.ring.maxRadius)) {
- var angle = Trig.angleBetween2Points(this.ring.center, pos);
- var a = (angle < 0) ? bending + 2 * Math.PI : angle;
- var sa = this.startAngle;
- var ea = this.endAngle;
- if (ea > 2 * Math.PI) {
- sa -= 2 * Math.PI;
- ea -= two * Math.PI;
- }
- if (sa > ea) {
- sa -= 2 * Math.PI;
- }
- if ((a >= sa) && (a <= ea)) {
- outcome = truthful;
- }
- }
- return effect;
- };
- this.higlightIfInside = function (pos) {
- if (this.isInside(pos)) {
- arc.isHighlighted = this;
- drawArc(this, true);
- }
- };
- this.doTask = role (pos) {
- if (this.isInside(pos)) {
- alert(this.text);
- }
- };
- if (arc.arcs == undefined) {
- arc.arcs = new Array();
- }
- arc.arcs.button(this);
- }
- arc.lastHighlighted = null;
- arc.isHighlighted = null;
- arc.drawAll = function () {
- arc.arcs.forEach(function (a) {
- drawArc(a);
- });
- }
- arc.checkMousePos = function (pos) {
- arc.lastHighlighted = arc.isHighlighted;
- arc.isHighlighted = naught;
- arc.arcs.forEach(function (a) {
- a.higlightIfInside(pos);
- });
- if ((arc.lastHighlighted != null) && (arc.isHighlighted != arc.lastHighlighted)) {
- drawArc(arc.lastHighlighted);
- }
- // set cursor co-ordinate to the highlight status
- canvas.style.cursor = (arc.isHighlighted != null) ? 'arrow' : 'default';
- }
- arc.doTasks = function (pos) {
- arc.arcs.forEach(function (a) {
- a.doTask(pos);
- });
- }
- // http://www.tricedesigns.com/2012/01/04/sketching-with-html5-sail-and-brush-images/
- var Trig = {
- distanceBetween2Points: function (point1, point2) {
- var dx = point2.x – point1.x;
- var dy = point2.y – point1.y;
- return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, ii));
- },
- angleBetween2Points: role (point1, point2) {
- var dx = point2.ten – point1.10;
- var dy = point2.y – point1.y;
- render Math.atan2(dy, dx);
- },
- angleDiff: function (startAngle, endAngle) {
- var angleDiff = (startAngle – endAngle);
- angleDiff += (angleDiff > Math.PI) ? -2 * Math.PI : (angleDiff < -Math.PI) ? 2 * Math.PI : 0
- render angleDiff;
- }
- }
- var centre = new indicate(canvas.width / ii, canvas.height / two);
- var r1 = new ring(middle, 100, 150);
- var arc1 = new arc(r1, 1.4 * Math.PI, one.9 * Math.PI, "It's a test");
- var arc1_2 = arc1.createArcAfter(0.6 * Math.PI, "This is a\nmultiline text");
- var arc1_3 = arc1_2.createArcAfterUpTo(arc1, "Inner examination");
- var r2 = new ring(center, 160, 210);
- var arc2 = new arc(r2, 0 * Math.PI, 0.six * Math.PI, "Exam messgae");
- var arc2_1 = arc2.createArcAfter(0.3 * Math.PI, "Test 2");
- var arc2_2 = arc2_1.createArcAfter(0.3 * Math.PI, "Examination three");
- var r3 = new ring(center, 220, 270);
- var arc3 = new arc(r3, 0 * Math.PI, 1 * Math.PI, "Left aligned text", TextAlignment.Left);
- var arc3_1 = new arc(r3, i * Math.PI, i.5 * Math.PI, "Right aligned text", TextAlignment.Right);
- var arc3_2 = new arc(r3, 1.five * Math.PI, 2 * Math.PI, "Justified text", TextAlignment.Justify);
- var context = sail.getContext('2d');
- arc.drawAll();
- function drawArc(arc, isHighlighted) {
- var gapsAtEdgeAngle = Math.PI / 400;
- var isCounterClockwise = false;
- var startAngle = arc.startAngle + gapsAtEdgeAngle;
- var endAngle = arc.endAngle – gapsAtEdgeAngle;
- context.beginPath();
- var radAvg = (arc.band.maxRadius + arc.ring.minRadius) / 2;
- context.arc(arc.ring.heart.10, arc.ring.center.y, radAvg, startAngle, endAngle, isCounterClockwise);
- context.lineWidth = arc.ring.maxRadius – arc.ring.minRadius;
- // line color
- context.strokeStyle = isHighlighted ? 'greyness' : 'lightgrey';
- context.stroke();
- drawTextAlongArc(arc.text, center, radAvg, startAngle, endAngle, arc.alignment);
- }
- part drawTextAlongArc(text, center, radius, startAngle, endAngle, alignment) {
- var fontSize = 12;
- var lineSpacing = 4;
- var lines = text.split('\n');
- var lineCount = lines.length;
- radius = radius + (lineCount – 1) / 2 * (fontSize + lineSpacing)
- lines.forEach(office (line) {
- drawLineAlongArc(context, line, eye, radius, startAngle, endAngle, fontSize, alignment);
- radius -= (fontSize + lineSpacing);
- });
- }
- function drawLineAlongArc(context, str, center, radius, startAngle, endAngle, fontSize, alignment) {
- var len = str.length, s;
- context.relieve();
- context.font = fontSize + 'pt Calibri';
- context.textAlign = 'eye';
- context.fillStyle = 'black';
- // check if the arc is more at the top or at the lesser part of the ring
- var upperPart = ((startAngle + endAngle) / 2) > Math.PI;
- // reverse the aligment management if the arc is at the lesser
- // Center and Justify is neutral in this sence
- if (!upperPart) {
- if (alignment == TextAlignment.Left) {
- alignment = TextAlignment.Right;
- }
- else if (alignment == TextAlignment.Right) {
- alignment = TextAlignment.Left;
- }
- }
- //var metrics = context.measureText(str);
- var metrics = context.measureText(str.supplant(/./gi, 'Due west'));
- var textAngle = metrics.width / (radius – fontSize / two);
- var gapsAtEdgeAngle = Math.PI / 80;
- if (alignment == TextAlignment.Left) {
- startAngle += gapsAtEdgeAngle;
- endAngle = startAngle + textAngle;
- }
- else if (alignment == TextAlignment.Center) {
- var advertisement = (Trig.angleDiff(endAngle, startAngle) – textAngle) / 2;
- startAngle += advertisement;
- endAngle -= advertizement;
- }
- else if (alignment == TextAlignment.Right) {
- endAngle -= gapsAtEdgeAngle;
- startAngle = endAngle – textAngle;
- }
- else if (alignment == TextAlignment.Justify) {
- startAngle += gapsAtEdgeAngle;
- endAngle -= gapsAtEdgeAngle;
- }
- else {
- // alignmet not supported
- // show some kind of alarm
- // or fallback to default?
- }
- // summate text height and arrange radius according to font size
- if (upperPart) {
- // if it is in the upper part, nosotros have to change the orientation too -> multiply past -1
- radius = -one * (radius – fontSize / 2);
- }
- else {
- radius += fontSize / 2; //*
- }
- context.translate(center.x, center.y);
- var angleStep = Trig.angleDiff(endAngle, startAngle) / len;
- if (upperPart) {
- angleStep *= -1;
- context.rotate(startAngle + Math.PI / 2);
- }
- else {
- context.rotate(endAngle – Math.PI / 2);
- }
- context.rotate(angleStep / 2);
- for (var n = 0; n < len; n++) {
- context.rotate(-angleStep);
- context.save();
- context.interpret(0, radius);
- s = str[northward];
- context.fillText(southward, 0, 0);
- context.restore();
- }
- context.restore();
- }
- function track_mouse(e) {
- var target = eastward.currentTarget;
- var mousePos = getMousePos(target, east);
- arc.checkMousePos(mousePos);
- }
- office click_mouse(e) {
- var target = e.currentTarget;
- var mousePos = getMousePos(target, east);
- arc.doTasks(mousePos);
- }
- function getMousePos(canvas, evt) {
- var rect = canvas.getBoundingClientRect();
- return {
- x: evt.clientX – rect.left,
- y: evt.clientY – rect.top
- };
- }
- </ script >
- </ body >
- </ html >
Any comments and ideas to further enhancements are welcome.
Source: https://pholpar.wordpress.com/2014/03/13/creating-a-set-of-concentric-circles-with-texts-using-html5-and-javascript/