How to create semi pie progress with highcharts? - javascript

semi-pie progress, what i want
Hello guys. How can i implement this kind of semi-pie progress bar with highcharts?
I tried alot and my result show as below photo. i want to create dataLabels same as first photo. if there is any way, please let me know.
i use this code for datalables:
dataLabels: {
formatter: function() {
return this.y > 1 ? '<b>' + this.point.name + ': ' + this.y + '%</b>' : null;
}
my result

So I created a guideline with custome functionality how to render a dataLabel which is 'attaching' to the pie point via using the SVGRenderer feature.
See code below and the demo: https://jsfiddle.net/BlackLabel/23ybcdjm/ and the version with path: https://jsfiddle.net/BlackLabel/u843xnoe/
chart: {
events: {
render() {
let chart = this,
series = chart.series[0],
point1 = series.points[0],
point2 = series.points[1],
shape1 = point1.shapeArgs,
shape2 = point2.shapeArgs,
distance,
distance2 = 50,
PI = -Math.PI,
label1,
label2,
x1,
y1,
x2,
y2;
//check if label exist while resize
if (chart.label1) {
chart.label1.destroy()
}
if (chart.label2) {
chart.label2.destroy()
}
//set x and y positions for labels
x1 = shape1.x + (shape1.r * Math.cos(shape1.end));
y1 = shape1.y + (shape1.r * Math.sin(shape1.end));
x2 = shape2.x + shape2.r + distance2;
y2 = shape2.y + chart.plotTop;
//render label1
chart.label1 = chart.renderer.label('Actual: ' + point1.y, x1, y1)
.css({
color: '#FFFFFF'
})
.attr({
fill: 'rgba(0, 0, 0, 0.75)',
padding: 8,
zIndex: 6,
})
.add();
//render label2
chart.label2 = chart.renderer.label('Target: ' + point2.y, x2, y2)
.css({
color: '#FFFFFF'
})
.attr({
fill: 'rgba(0, 0, 0, 0.75)',
padding: 8,
zIndex: 6,
})
.add();
//find distance for label which is moving around the pie
if (shape1.end > PI && shape1.end <= 0.55 * PI) {
distance = -chart.label2.getBBox().width
} else if (shape1.end > 0.55 * PI && shape1.end <= 0.45 * PI) {
distance = -chart.label2.getBBox().width / 2
} else if (shape1.end > 0.5 * PI && shape1.end < 0.25 * PI) {
distance = chart.label2.getBBox().width / 3
} else if (shape1.end > 0.25 * PI && shape1.end <= 0) {
distance = chart.label2.getBBox().width / 2
}
label1 = chart.label1;
label2 = chart.label2;
//translate labels
label1.translate(label1.x + distance + chart.plotLeft, label1.y)
chart.label2.translate(chart.label2.x, chart.label2.y - chart.label2.getBBox().height)
}
}
},
API: https://api.highcharts.com/highcharts/chart.events.render
API: https://api.highcharts.com/class-reference/Highcharts.SVGRenderer#label

Related

Editable table with fabricjs

I'm trying to create a table with TextBox or IText which can be edited with fabricjs.
I was able to create a table view by combining multiple Textboxes with background colours. according to the design, table should have rounded corners. Any Idea how to add those rounded corners?
using custom classes (Rect + TextBox) was able to find a solution for this.
const canvas = new fabric.Canvas('c')
fabric.RectWithText = fabric.util.createClass(fabric.Rect, {
type: 'rectWithText',
text: null,
textOffsetLeft: 0,
textOffsetTop: 0,
_prevObjectStacking: null,
_prevAngle: 0,
type: "roundedRect",
topLeft: this.topLeft || [0,0],
topRight: this.topRight || [0,0],
bottomLeft: this.bottomLeft || [0,0],
bottomRight: this.bottomRight || [0,0],
_render: function(ctx) {
var w = this.width,
h = this.height,
x = -this.width / 2,
y = -this.height / 2,
/* "magic number" for bezier approximations of arcs (http://itc.ktu.lt/itc354/Riskus354.pdf) */
k = 1 - 0.5522847498;
ctx.beginPath();
// top left
ctx.moveTo(x + this.topLeft[0], y);
// line to top right
ctx.lineTo(x + w - this.topRight[0], y);
ctx.bezierCurveTo(x + w - k * this.topRight[0], y, x + w, y + k * this.topRight[1], x + w, y + this.topRight[1]);
// line to bottom right
ctx.lineTo(x + w, y + h - this.bottomRight[1]);
ctx.bezierCurveTo(x + w, y + h - k * this.bottomRight[1], x + w - k * this.bottomRight[0], y + h, x + w - this.bottomRight[0], y + h);
// line to bottom left
ctx.lineTo(x + this.bottomLeft[0], y + h);
ctx.bezierCurveTo(x + k * this.bottomLeft[0], y + h, x, y + h - k * this.bottomLeft[1], x, y + h - this.bottomLeft[1]);
// line to top left
ctx.lineTo(x, y + this.topLeft[1]);
ctx.bezierCurveTo(x, y + k * this.topLeft[1], x + k * this.topLeft[0], y, x + this.topLeft[0], y);
ctx.closePath();
this._renderPaintInOrder(ctx);
},
recalcTextPosition: function () {
const sin = Math.sin(fabric.util.degreesToRadians(this.angle))
const cos = Math.cos(fabric.util.degreesToRadians(this.angle))
const newTop = sin * this.textOffsetLeft + cos * this.textOffsetTop
const newLeft = cos * this.textOffsetLeft - sin * this.textOffsetTop
const rectLeftTop = this.getPointByOrigin('left', 'top')
this.text.set('left', rectLeftTop.x + newLeft)
this.text.set('top', rectLeftTop.y + newTop)
},
initialize: function (rectOptions, textOptions, text) {
this.callSuper('initialize', rectOptions)
this.text = new fabric.Textbox(text, {
...textOptions,
selectable: false,
evented: false,
})
this.textOffsetLeft = this.text.left - this.left
this.textOffsetTop = this.text.top - this.top
this.on('moving', () => {
this.recalcTextPosition()
})
this.on('rotating', () => {
this.text.rotate(this.text.angle + this.angle - this._prevAngle)
this.recalcTextPosition()
this._prevAngle = this.angle
})
this.on('scaling', (e) => {
this.recalcTextPosition()
})
this.on('added', () => {
this.canvas.add(this.text)
})
this.on('removed', () => {
this.canvas.remove(this.text)
})
this.on('mousedown:before', () => {
this._prevObjectStacking = this.canvas.preserveObjectStacking
this.canvas.preserveObjectStacking = true
})
this.on('mousedblclick', () => {
this.text.selectable = true
this.text.evented = true
this.canvas.setActiveObject(this.text)
this.text.enterEditing()
this.selectable = false
})
this.on('deselected', () => {
this.canvas.preserveObjectStacking = this._prevObjectStacking
})
this.text.on('editing:exited', () => {
this.text.selectable = false
this.text.evented = false
this.selectable = true
})
}
})
const rectOptions = {
left: 10,
topLeft: [20,20],
top: 10,
width: 200,
height: 75,
fill: 'rgba(30, 30, 30, 0.3)',
rx: 10
}
const textOptions = {
left: 35,
top: 30,
width: 150,
fill: 'white',
shadow: new fabric.Shadow({
color: 'rgba(34, 34, 100, 0.4)',
blur: 2,
offsetX: -2,
offsetY: 2
}),
fontSize: 30,
selectable: true
}
const rectWithText = new fabric.RectWithText(rectOptions, textOptions, 'Some text')
canvas.add(rectWithText)
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.2.4/fabric.min.js"></script>
<canvas id="c" width="600" height="600"></canvas>

Show xrange x and x2 values in chart

How to show the min and max or x and x2 values in the chart and Need the partial fill to in value than %.
min = 0;
max = 150;
y=95; //points scored.
{
showInLegend: false,
pointWidth: 25,
data: [{
x: 0,
x2: 150,
y: 0,
partialFill: 0.75
}],
dataLabels: {
enabled: true
}
}
0 -------------------95points---------------150
Fiddle Example: https://jsfiddle.net/bv4uyazq/
If I understood you correctly you can achieve it following these approaches:
1) render custom labels using SVGRenderer:
Code:
chart: {
type: 'xrange',
events: {
render: function() {
var chart = this,
offsetTop = -8,
offsetLetf = 5,
x1,
x2,
y,
label1,
label2,
label2BBox;
if (chart.customLabels && chart.customLabels.length) {
chart.customLabels.forEach(function(label) {
label.destroy();
});
}
chart.customLabels = [];
chart.series[0].points.forEach(function(point) {
x1 = point.plotX + chart.plotLeft + offsetLetf;
x2 = x1 + point.shapeArgs.width - 2 * offsetLetf;
y = point.plotY + chart.plotTop + point.shapeArgs.height / 2 + offsetTop;
label1 = chart.renderer.text(chart.xAxis[0].dataMin, x1, y).css({
fill: '#fff'
}).add().toFront();
label2 = chart.renderer.text(chart.xAxis[0].dataMax, x2, y).css({
fill: '#fff'
}).add().toFront();
label2BBox = label2.getBBox();
label2.translate(-label2BBox.width, 0);
chart.customLabels.push(label1);
chart.customLabels.push(label2);
});
}
}
}
Demo:
https://jsfiddle.net/BlackLabel/twuv96ex/1/
2) set xAxis labels positions as dataMin and dataMax and translate it using offset property:
Code:
xAxis: {
type: 'linear',
visible: true,
offset: -110,
tickPositioner: function() {
return [this.dataMin, this.dataMax];
}
}
Demo:
https://jsfiddle.net/BlackLabel/m7s4pfo5/
API reference:
https://api.highcharts.com/class-reference/Highcharts.SVGRenderer#text
https://api.highcharts.com/class-reference/Highcharts.DataLabelsOptionsObject#formatter
https://api.highcharts.com/highcharts/xAxis.offset

Highcharts connected mappoint with "direction"

I released an interactive map with Highcharts.
It represents the path of an artist in 3 years in Italy.
It contains a serie with 3 points connected.
How can I "print" arrows on the path to esplicate the "direction" of the path?
{
type: "mappoint",
lineWidth: 2,
data: [
{ lat: 41.108679365839755, lon: 16.849069442461108 },
{ lat: 40.65378710700787, lon: 14.759846388659303 },
{ lat: 41.90017321198485, lon: 12.16516614442158 }
]
}
The full code is on jsfiddle
http://jsfiddle.net/0ghkmjpg/
You can wrap a method which is responsible for rendering line in the map and change the path so it shows an arrow between points. You can see the answer how to do it in a simple line chart here.
You can also use Renderer directly and draw a path on load/redraw event.
Function for rendering path might look like this:
function renderArrow(chart, startPoint, stopPoint) {
const triangle = function(x, y, w, h) {
return [
'M', x + w / 2, y,
'L', x + w, y + h,
x, y + h,
'Z'
];
};
var arrow = chart.arrows[startPoint.options.id];
if (!arrow) {
arrow = chart.arrows[startPoint.options.id] = chart.renderer.path().add(startPoint.series.group);
}
const x1 = startPoint.plotX;
const x2 = stopPoint.plotX;
const y1 = startPoint.plotY;
const y2 = stopPoint.plotY;
const distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
const h = Math.min(Math.max(distance / 3, 10), 30);
const w = h / 1.5;
if (distance > h * 2) {
const offset = h / 2 / distance;
const y = y1 + (0.5 + offset) * (y2 - y1);
const x = x1 + (0.5 + offset) * (x2 - x1);
const arrowPath = triangle(x - w / 2, y, w, h);
const angle = Math.round(Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI) + 90;
arrow.attr({
d: arrowPath.join(' '),
transform: `rotate(${angle} ${x} ${y})`,
fill: 'black',
zIndex: 10,
visibility: 'visible'
});
} else {
arrow.attr({
visibility: 'hidden'
});
}
}
And function which loops through the points and render arrows
function renderArrows() {
if (!this.arrows) {
this.arrows = {};
}
const points = this.series[1].points;
points.reduce((start, stop) => {
renderArrow(this, start, stop);
return stop;
});
}
Attach rendering arrows on load/redraw event
Highcharts.mapChart('container', {
chart: {
animation: false,
events: {
load: renderArrows,
redraw: renderArrows
}
},
Of course there is a lot of space in that question how the arrows should behave - should they have always constant size, when should they appear/disappear, exact shape and styles of the arrow, etc. - but you should be able to adjust the code above.
Live example and output
http://jsfiddle.net/jq0oxtpw/

Fabric.js - wrong group position

I would like add arrow to my canvas - user click on canvas, fabric draw line + triangle. Then, when user move mouse line is "moving" and arrow angle is changed. "Drawing mode" is end when mouse up. This works great, but positions and selection area are wrong.
I write that for mouse:down:
// Draw new arrow using move
canvas.fabric.on('mouse:down', function(options){
$scope.lineOptions = {
stroke: '#000',
selectable: true,
strokeWidth: '2',
padding: 5,
hasBorders: false,
hasControls: false,
//originX: 'top',
//originY: 'left',
lockScalingX: true,
lockScalingY: true
};
var zoomFactor = Math.pow(canvas.currentZoom, -1);
var x = (options.e.clientX - canvas.fabric._offset.left) * zoomFactor;
var y = (options.e.clientY - canvas.fabric._offset.top) * zoomFactor;
var points = [ 0, 0, 0, 0 ];
temp_line = new fabric.Line(points, $scope.lineOptions);
temp_arrow = new fabric.Triangle({
left: x,
top: y,
angle: -45,
originX: 'center',
originY: 'center',
width: 20,
height: 20,
fill: '#000'
});
arrowShape = new fabric.Group([ temp_line, temp_arrow ], {
left: x,
top: y
});
canvas.fabric.add(arrowShape);
});
Zoom factor is here negligible.
There is code for mouse:move:
canvas.fabric.on('mouse:move', function(o){
var pointer = canvas.fabric.getPointer(o.e);
arrowShape.item(0).set({ x2: pointer.x, y2: pointer.y });
arrowShape.item(1).set({ left: pointer.x, top: pointer.y });
x1 = arrowShape.item(0).get('x1');
y1 = arrowShape.item(0).get('y1');
x2 = arrowShape.item(0).get('x2');
y2 = arrowShape.item(0).get('y2');
angle = calcArrowAngle(x1, y1, x2, y2);
arrowShape.item(1).set('angle', angle + 90);
//arrowShape.item(0).setCoords();
//arrowShape.item(1).setCoords();
//arrowShape.setCoords();
canvas.fabric.renderAll();
});
function calcArrowAngle(x1, y1, x2, y2) {
var angle = 0,
x, y;
x = (x2 - x1);
y = (y2 - y1);
if (x === 0) {
angle = (y === 0) ? 0 : (y > 0) ? Math.PI / 2 : Math.PI * 3 / 2;
} else if (y === 0) {
angle = (x > 0) ? 0 : Math.PI;
} else {
angle = (x < 0) ? Math.atan(y / x) + Math.PI : (y < 0) ? Math.atan(y / x) + (2 * Math.PI) : Math.atan(y / x);
}
return (angle * 180 / Math.PI);
}
and for mouse:up:
canvas.fabric.on('mouse:up', function(o){
canvas.fabric.setActiveObject(arrowShape);
// My drawing mode
$scope.drawMode = false;
});
Adding works great, change direction works great, change angle works great... but group selection and line start point is wrong:
OriginX/originY are default, so they should be top/left.. but the aren't. Also, group selection area are wrong, too small for both shapes. I forgot about something... but about what?

kineticjs performance lag

I am working on a radial control similar to the HTML5 wheel of fortune example. I've modified the original here with an example of some additional functionality I require: http://jsfiddle.net/fEm9P/ When you click on the inner kinetic wedges they will shrink and expand within the larger wedges. Unfortunately when I rotate the wheel it lags behind the pointer. It's not too bad here but it's really noticeable on a mobile.
I know this is due to the fact that I'm not caching the wheel. When I do cache the wheel (uncomment lines 239-249) the inner wedges no longer respond to mouse/touch but the response on rotation is perfect. I have also tried adding the inner wedges to a separate layer and caching the main wheel only. I then rotate the inner wheel with the outer one. Doing it this way is a little better but still not viable on mobile.
Any suggestions would be greatly appreciated.
Stephen
//constants
var MAX_ANGULAR_VELOCITY = 360 * 5;
var NUM_WEDGES = 25;
var WHEEL_RADIUS = 410;
var ANGULAR_FRICTION = 0.2;
// globals
var angularVelocity = 360;
var lastRotation = 0;
var controlled = false;
var target, activeWedge, stage, layer, wheel,
pointer, pointerTween, startRotation, startX, startY;
var currentVolume, action;
function purifyColor(color) {
var randIndex = Math.round(Math.random() * 3);
color[randIndex] = 0;
return color;
}
function getRandomColor() {
var r = 100 + Math.round(Math.random() * 55);
var g = 100 + Math.round(Math.random() * 55);
var b = 100 + Math.round(Math.random() * 55);
var color = [r, g, b];
color = purifyColor(color);
color = purifyColor(color);
return color;
}
function bind() {
wheel.on('mousedown', function(evt) {
var mousePos = stage.getPointerPosition();
angularVelocity = 0;
controlled = true;
target = evt.targetNode;
startRotation = this.rotation();
startX = mousePos.x;
startY = mousePos.y;
});
// add listeners to container
document.body.addEventListener('mouseup', function() {
controlled = false;
action = null;
if(angularVelocity > MAX_ANGULAR_VELOCITY) {
angularVelocity = MAX_ANGULAR_VELOCITY;
}
else if(angularVelocity < -1 * MAX_ANGULAR_VELOCITY) {
angularVelocity = -1 * MAX_ANGULAR_VELOCITY;
}
angularVelocities = [];
}, false);
document.body.addEventListener('mousemove', function(evt) {
var mousePos = stage.getPointerPosition();
var x1, y1;
if(action == 'increase') {
x1 = (mousePos.x-(stage.getWidth() / 2));
y1 = (mousePos.y-WHEEL_RADIUS+20);
var r = Math.sqrt(x1 * x1 + y1 * y1);
if (r>500){
r=500;
} else if (r<100){
r=100;
};
currentVolume.setRadius(r);
layer.draw();
} else {
if(controlled && mousePos && target) {
x1 = mousePos.x - wheel.x();
y1 = mousePos.y - wheel.y();
var x2 = startX - wheel.x();
var y2 = startY - wheel.y();
var angle1 = Math.atan(y1 / x1) * 180 / Math.PI;
var angle2 = Math.atan(y2 / x2) * 180 / Math.PI;
var angleDiff = angle2 - angle1;
if ((x1 < 0 && x2 >=0) || (x2 < 0 && x1 >=0)) {
angleDiff += 180;
}
wheel.setRotation(startRotation - angleDiff);
}
};
}, false);
}
function getRandomReward() {
var mainDigit = Math.round(Math.random() * 9);
return mainDigit + '\n0\n0';
}
function addWedge(n) {
var s = getRandomColor();
var reward = getRandomReward();
var r = s[0];
var g = s[1];
var b = s[2];
var angle = 360 / NUM_WEDGES;
var endColor = 'rgb(' + r + ',' + g + ',' + b + ')';
r += 100;
g += 100;
b += 100;
var startColor = 'rgb(' + r + ',' + g + ',' + b + ')';
var wedge = new Kinetic.Group({
rotation: n * 360 / NUM_WEDGES,
});
var wedgeBackground = new Kinetic.Wedge({
radius: WHEEL_RADIUS,
angle: angle,
fillRadialGradientStartRadius: 0,
fillRadialGradientEndRadius: WHEEL_RADIUS,
fillRadialGradientColorStops: [0, startColor, 1, endColor],
fill: '#64e9f8',
fillPriority: 'radial-gradient',
stroke: '#ccc',
strokeWidth: 2,
rotation: (90 + angle/2) * -1
});
wedge.add(wedgeBackground);
var text = new Kinetic.Text({
text: reward,
fontFamily: 'Calibri',
fontSize: 50,
fill: 'white',
align: 'center',
stroke: 'yellow',
strokeWidth: 1,
listening: false
});
text.offsetX(text.width()/2);
text.offsetY(WHEEL_RADIUS - 15);
wedge.add(text);
volume = createVolumeControl(angle, endColor);
wedge.add(volume);
wheel.add(wedge);
}
var activeWedge;
function createVolumeControl(angle, colour){
var volume = new Kinetic.Wedge({
radius: 100,
angle: angle,
fill: colour,
stroke: '#000000',
rotation: (90 + angle/2) * -1
});
volume.on("mousedown touchstart", function() {
currentVolume = this;
action='increase';
});
return volume;
}
function animate(frame) {
// wheel
var angularVelocityChange = angularVelocity * frame.timeDiff * (1 - ANGULAR_FRICTION) / 1000;
angularVelocity -= angularVelocityChange;
if(controlled) {
angularVelocity = ((wheel.getRotation() - lastRotation) * 1000 / frame.timeDiff);
}
else {
wheel.rotate(frame.timeDiff * angularVelocity / 1000);
}
lastRotation = wheel.getRotation();
// pointer
var intersectedWedge = layer.getIntersection({x: stage.width()/2, y: 50});
if (intersectedWedge && (!activeWedge || activeWedge._id !== intersectedWedge._id)) {
pointerTween.reset();
pointerTween.play();
activeWedge = intersectedWedge;
}
}
function init() {
stage = new Kinetic.Stage({
container: 'container',
width: 578,
height: 500
});
layer = new Kinetic.Layer();
wheel = new Kinetic.Group({
x: stage.getWidth() / 2,
y: WHEEL_RADIUS + 20
});
for(var n = 0; n < NUM_WEDGES; n++) {
addWedge(n);
}
pointer = new Kinetic.Wedge({
fillRadialGradientStartPoint: 0,
fillRadialGradientStartRadius: 0,
fillRadialGradientEndPoint: 0,
fillRadialGradientEndRadius: 30,
fillRadialGradientColorStops: [0, 'white', 1, 'red'],
stroke: 'white',
strokeWidth: 2,
lineJoin: 'round',
angle: 30,
radius: 30,
x: stage.getWidth() / 2,
y: 20,
rotation: -105,
shadowColor: 'black',
shadowOffset: {x:3,y:3},
shadowBlur: 2,
shadowOpacity: 0.5
});
// add components to the stage
layer.add(wheel);
layer.add(pointer);
stage.add(layer);
pointerTween = new Kinetic.Tween({
node: pointer,
duration: 0.1,
easing: Kinetic.Easings.EaseInOut,
y: 30
});
pointerTween.finish();
var radiusPlus2 = WHEEL_RADIUS + 2;
wheel.cache({
x: -1* radiusPlus2,
y: -1* radiusPlus2,
width: radiusPlus2 * 2,
height: radiusPlus2 * 2
}).offset({
x: radiusPlus2,
y: radiusPlus2
});
layer.draw();
// bind events
bind();
var anim = new Kinetic.Animation(animate, layer);
//document.getElementById('debug').appendChild(layer.hitCanvas._canvas);
// wait one second and then spin the wheel
setTimeout(function() {
anim.start();
}, 1000);
}
init();
I made a couple of changes to the script which greatly improved the response time. The first was replacing layer.draw() with layer.batchDraw(). As the draw function was being called on each touchmove event it was making the interaction clunky. BatchDraw on the other hand will stack up draw requests internally "limit the number of redraws per second based on the maximum number of frames per second" (http://www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-batch-draw).
The jumping around of the canvas I seeing originally when I cached/cleared the wheel was due to the fact that I wasn't resetting the offset on the wheel when I cleared the cache.
http://jsfiddle.net/leydar/a7tkA/5
wheel.clearCache().offset({
x: 0,
y: 0
});
I hope this is of benefit to someone else. It's still not perfectly responsive but it's at least going in the right direction.
Stephen

Categories

Resources