I wish to have tooltips AND do some custom stuff when a user hovers over a .c3-bar. This is both not supported by the c3 api and made difficult because the way c3 manages pointer events (by using css to turn them off). If I turn .c3-bar to pointer-events:auto !important tooltips stop working, but otherwise I can't listen to events on .c3-bar. Does anyone know how to address this?
I got it to work, but its ugly ugly
const c3Hover = 'c3-hover'
const barSelector = '.c3-bar'
const $wrapper = $('#' + this.chart.id)
const getBarRects = () =>
_.map($wrapper.find(barSelector),
(bar) => bar.getBoundingClientRect())
$wrapper.on('mousemove', ({clientX,clientY}) =>
_.reduce(getBarRects(), (acc, {left,right,top,bottom}) =>
acc || left <= clientX && clientX <= right
&& top <= clientY && clientY <= bottom
, false) ? $wrapper.addClass(c3Hover)
: $wrapper.removeClass(c3Hover))
There's a built-in way that reports an event when it hovers over data points, but it seems to fire once per series over a particular x value - i.e. on a bar chart with 2 series it will fire twice. Don't know if that's good enough for your situation?
var chart = c3.generate({
data: {
columns: [
['data1', 30, 200, 100, 400, 150, 250],
['data2', 130, 100, 140, 200, 150, 50]
],
type: 'bar',
onmouseover: function (d) {
console.log ("yo", d);
},
},
bar: {
width: {
ratio: 0.5 // this makes bar width 50% of length between ticks
}
}
});
outputs:
yo Object {x: 0, value: 30, id: "data1", index: 0, name: "data1"}
yo Object {x: 0, value: 130, id: "data2", index: 0, name: "data2"}
when moved over the first pair of bars (regardless of the particular one it's actually over)
http://c3js.org/reference.html#data-onmouseover
Edit: Aah, I see it also fires when you're not over the actual bar but also in the general vicinity. I guess this doesn't meet your requirements then.
Related
in my Highcharts project, I successfully make all the shapes in a single annotation draggable, but when I add the SVG (the type of this shape is "path"), the SVG doesn't move along with other shapes in the annotation.
I need to have some customised shapes in the annotation. Can anyone point out what the issue is for this SVG? Is it possible to put the SVG within annotation and still be draggable? Or is it some mistake I made that causes the issue?
My example here. https://jsfiddle.net/roy_jaide/754xuhtk/ As you can see, the label, circle and the line are all draggable, but the SVG shape just doesn't move at all.
Thanks for reading my question, and much appreciated if any solution provided.
Highcharts.Annotation.prototype.onDrag = function (e) {
if (this.chart.isInsidePlot(e.chartX - this.chart.plotLeft, e.chartY - this.chart.plotTop)) {
var translation = this.mouseMoveToTranslation(e),
xAxis = this.chart.xAxis[0],
yAxis = this.chart.yAxis[0],
xStep = this.options.stepX,
yStep = this.options.stepY,
pxStep = xAxis.toPixels(xStep) - xAxis.toPixels(0),
pyStep = yAxis.toPixels(yStep) - yAxis.toPixels(0);
if (this.options.draggable === 'x') { //for now, it's exclusive for age handle
this.potentialTranslationX += translation.x;
if (Math.abs(this.potentialTranslationX) >= Math.abs(pxStep)) {
translation.x = (this.potentialTranslationX > 0) ? pxStep : -pxStep;
translation.y = 0;
this.currentXValue += (this.potentialTranslationX > 0) ? xStep : -xStep;
this.potentialTranslationX -= (this.potentialTranslationX > 0) ? pxStep : -pxStep; //minus the step and continue to cumulate
//this.potentialTranslation = 0; //not correct, the mouse will go faster than the handle
if (this.points.length) {
this.translate(translation.x, 0);
} else {
this.shapes.forEach(function (shape) {
shape.translate(translation.x, 0);
});
this.labels.forEach(function (label) {
label.translate(translation.x, 0);
label.text = label.annotation.options.preText + label.annotation.currentXValue;
});
}
}
}
this.redraw(false);
}
}
Update 1: After trying Sebastian's answer on my chart, it turns out to be difficult to calculate the correct coordinates. At last I use type "image" to put display the shape. The shape is a Font-Awesome icon so before using "image" I did try to add a label with "useHTML" : true, but it seems the icon is moved a little after the first redraw(false), not sure why.
The image of the shape. I achieved this by adding "image" shape.
d: ["M", 440, 72, "L", 410, 45, 470, 45, "Z"],
I wouldn't recommend this way to create a draggable custom shape. This option creates a shape as expected but also sets a fixed position. Probably it is possible to implement move functionality, but I think that it will require a lot of changes into draggable core code.
What I can suggest is to make it this way:
annotations: [{
draggable: 'x',
shapes: [{
points: [{
x: 440,
y: 72
}, {
x: 410,
y: 45
}, {
x: 470,
y: 45
}, {
x: 440,
y: 72
}],
fill: 'red',
type: 'path',
stroke: "blue",
strokeWidth: 3,
markerStart: 'circle',
}]
}]
Where markerStart is a defined shape.
See a Demo
I want to create a chart like the below.
https://www.reddit.com/r/interestingasfuck/comments/9togwf/the_major_world_economies_over_time/
I created this chart by Highcharts. But, I can't animate changing the order of bars.
function rotate(array, times) {
while (times--) {
var temp = array.shift();
array.push(temp)
}
}
window.data = {
categories: ['Africa', 'America', 'Asia', 'Europe', 'Oceania'],
y_values: [100, 200, 300, 400, 500],
colors: ['#DC4D3A', '#E93D3F', '#83C6C7', '#46D388', '#D1D785']
};
document.getElementById("button").addEventListener("click", function () {
for (var i = 0; i < data['y_values'].length; i++) {
chart.series[0].data[i].update({y: data['y_values'][i]});
chart.series[0].data[i].update({color: data['colors'][i]});
}
chart.xAxis[0].update({categories: data['categories']});
rotate(data['y_values'], 1);
rotate(data['categories'], 1);
rotate(data['colors'], 1);
}, false);
All code I wrote are in JSFiddle.
https://jsfiddle.net/Shinohara/35e8gbyz/
Please anyone can help me?
Highcharts provides animate method for SVG elements, which you can use to achieve the wanted result. You need to animate columns, axis labels and data labels:
document.getElementById("button").addEventListener("click", function() {
var points = chart.series[0].points,
ticks = chart.xAxis[0].ticks;
points[0].graphic.animate({
x: points[1].shapeArgs.x
});
points[1].graphic.animate({
x: points[0].shapeArgs.x
});
points[0].dataLabel.animate({
y: points[1].dataLabel.translateY
});
points[1].dataLabel.animate({
y: points[0].dataLabel.translateY
});
ticks[0].label.animate({
y: ticks[1].label.xy.y
});
ticks[1].label.animate({
y: ticks[0].label.xy.y
});
}, false);
Live demo: https://jsfiddle.net/BlackLabel/ux59vcd6/
API Reference: https://api.highcharts.com/class-reference/Highcharts.SVGElement#animate
It seems there is no native implementation.
The only idea I have it to not display Y axis labels at all. Using SVGRenderer
draw the text and save it as a variable (for further updating)
chart.customText = chart.renderer.text('label name', 100, 100) // label name, X, Y
.attr({
useHTML: true
})
.css({
fontSize: '16px' // and other CSS if necessary
})
.add();
Then you can update x, y or text
chart.customText.attr({
str: 'new text. You can use HTML',
y: 150 // new value
})
Your next step is to do something with Y position. Or even you can try to draw only 1 text with HTML that contains all labels. Then using JS move them as you need.
I'm trying to build some chart like this one:
Chart Visual
But the main struggle is to add two different series that complement each other.
I really appreciate any help that someone could give me.
Many thanks in advance.
You can achieve it, but the process of implementation is not so easy. I prepared the example which shows how to do that, and I will try to explain what I did, step by step.
First, you need to define your data array just like that:
var data = [40, 30, 10, 20]
Then define your chart configuration, and inside of chart.events.load function handler put whole logic of creating desired effect.
Next step is iterate on all data positions, and create its own specific point, series, yAxis and pane, basing on calculations like below:
load() {
var chart = this,
series = [],
panes = [],
yAxes = [],
radius = 112,
innerRadius = 88,
pointAngle,
prevPointAngle = 0,
pointPadding = (radius - innerRadius) / 4,
colors = Highcharts.getOptions().colors,
additionalPointPadding = 2;
data.forEach(function(p, i) {
pointAngle = (p * 360) / 100 // Calculate point angle
// Prepare pane for each point
panes.push({
startAngle: prevPointAngle + pointPadding + additionalPointPadding,
endAngle: (pointAngle + prevPointAngle) - pointPadding - additionalPointPadding,
background: [{
backgroundColor: '#fff',
borderWidth: 0
}]
})
// Prepare yAxis for specific pane
yAxes.push({
min: 0,
max: 100,
lineWidth: 0,
tickPositions: [],
pane: i
})
// Prepare series with specific point
series.push({
name: 'Exercise ' + i,
data: [{
color: colors[i],
radius: radius + '%',
innerRadius: innerRadius + '%',
y: 100,
percents: p
}],
yAxis: i
})
prevPointAngle += pointAngle
})
And finally, update our chart by new objects:
chart.update({
pane: panes,
yAxis: yAxes,
series: series
},true, true)
Last thing you have to know, that your chart configuration should have the same amount of empty objects in pane array, like the data positions, e.g:
var data = [10, 80, 10]
(...)
pane: [{},{},{}]
Here is the example which shows the final effect: https://jsfiddle.net/yamu5z9r/
Kind regards!
I have a thermometer of svg type. I want to create some animation using Snap. I could animate the text with the given value but could not animate the dashed line that you can see in the screenshot to move as per the given value. How should i do it?
Here is my code
const svg = Snap(this.svg);
const { color } = this.state;
svg.line(55, 366, 90, 366).attr({
id: 'marker-line',
stroke: color,
strokeDasharray: '2 4',
strokeWidth: '1'
});
const animateMarker = (value, svg, marker, lastValue) => {
// Snap.animate(value);
const markerLine = svg.select('#marker-line');
if (markerLine) {
Snap.animate(
lastValue || 0,
value,
val => {
// markerLine.attr({ y1: 366-val });
marker.textContent = roundOffDecimalDigit(val, 2); // eslint-disable-line no-param-reassign
},
400
);
}
};
Without the rest of the code, it's difficult to tell. Your code looks ok though, just it only sets one of the y co-ordinates, and I assume you want to set both. Eg
Snap.animate(lastValue,value, function( val ) { markerLine.attr({ y1: val, y2: val }); }, 400 )
If it's still not working, try putting it on a jsfiddle and it will be easier to help.
I've been experimenting with the excellent PhysicsJS library, especially constraints and collisions. What I'm trying to achieve is a polygon which can spin about a fixed rotation point. As a real world example, imagine a wide shelf nailed to a wall with just one long nail through the very center, so that it spins round maybe half or one revolution when something off-center drops on it from above.
Here is an example fiddle: http://jsfiddle.net/2HRGW/41/
and the code:
Physics(function (world) {
var renderer = Physics.renderer('canvas', {
el: 'viewport',
width: 500,
height: 400
});
world.add(renderer);
var nail = Physics.body('circle', {
x: 250,
y: 200,
radius: 5,
mass: 1,
fixed: true
});
world.add(nail);
var shelf = Physics.body('convex-polygon', {
x: 250,
y: 200,
vertices: [{
x: -100,
y: -10
}, {
x: 100,
y: -10
}, {
x: 100,
y: 10
}, {
x: -100,
y: 10
}],
mass: 100,
restitution: 0.5
});
world.add(shelf);
var ball = Physics.body('circle', {
x: 175,
y: 50,
radius: 20,
mass: 10
});
world.add(ball);
world.add(Physics.integrator('verlet', {
drag: 0.003
}));
var verletConstraints = Physics.behavior('verlet-constraints', {
iterations: 2
});
verletConstraints.distanceConstraint(shelf, nail, 1);
world.add(verletConstraints);
world.add(Physics.behavior('constant-acceleration'));
world.add(Physics.behavior('body-collision-detection'));
world.add(Physics.behavior('body-impulse-response'));
world.add(Physics.behavior('sweep-prune'));
world.add(Physics.behavior('verlet-constraints'));
var bounds = Physics.aabb(0, 0, 500, 400);
world.add(Physics.behavior('edge-collision-detection', {
aabb: bounds,
restitution: 0.01
}));
Physics.util.ticker.subscribe(function (time, dt) {
world.step(time);
});
world.render();
Physics.util.ticker.start();
world.subscribe('step', function () {
world.render();
});
});
I define a fixed circle for the nail and a non-fixed polygon for the shelf and add a distance constraint linking the polygon to the circle. As you can see, there are 2 problems. Firstly, the shelf immediately drops down slightly until its top edge is flush with the top of the nail, rather than remaining evenly positioned around the nail. Secondly, when the ball drops onto the shelf, the shelf goes crazy spinning round endlessly, despite having mass and various restitution settings tried. Adjusting its position slightly, sometimes it can even detach completely and fly off.
Am I on the right track using constraints in this way, or is there a simpler solution?
As other users have mentioned, this is a current limitation of PhysicsJS. It's being worked out:
scoping of collisions https://github.com/wellcaffeinated/PhysicsJS/issues/30
constraints https://github.com/wellcaffeinated/PhysicsJS/issues/5
In the mean time, instead of patching the library, why not create a custom pin constraint behavior. For a simple pin constraint behavior that pins the body centroid to a target position, it's quite easy. Here's a jsFiddle of your example with a custom pin constraint manager defined at the beginning.
http://jsfiddle.net/wellcaffeinated/2HRGW/50/
// create a behavior to handle pin constraints
Physics.behavior('pin-constraints', function( parent ){
return {
init: function( opts ){
parent.init.call( this, opts );
this.pins = [];
},
add: function( body, targetPos ){
this.pins.push({
body: body,
target: Physics.vector( targetPos )
});
},
behave: function( data ){
var pins = this.pins
,pin
;
for (var i = 0, l = pins.length; i < l; i++){
pin = pins[ i ];
// move body to correct position
pin.body.state.pos.clone( pin.target );
}
}
};
});
Your overall implementation is correct.
The only thing you got wrong is the mass of the convex-polygon, 100 is a wild value.
A mass of around 0.2 should be enough for that object.