I need to respond to a click event on each slice in a Raphael Pie Chart. I tried it this way, but it does not seem to do it. My code is just two lines commented as "My code" in the below code, which comes directly from the demo Pie Chart of RaphaelJS...
<script>
var u = "";
var r = "";
window.onload = function () {
r = Raphael("holder"),
pie = r.piechart(320, 240, 100, [25, 20, 13, 32, 5, 21, 14, 10,41,16,12,18,16,14,12,13],
{ legend: ["%%.%% - Enterprise Users", "IE Users"],
legendpos: "west", href: ["http://raphaeljs.com", "http://g.raphaeljs.com"]
}
);
r.text(320, 100, "Interactive Pie Chart").attr({ font: "20px sans-serif" });
pie.hover(function () {
u = this; // My Code
u.onclick = clickEvent; // hook to the function
this.sector.stop();
this.sector.scale(1.1, 1.1, this.cx, this.cy); // Scale slice
if (this.label) { // Scale button and bolden text
this.label[0].stop();
this.label[0].attr({ r: 7.5 });
this.label[1].attr({ "font-weight": 800 });
}
}, function () {
this.sector.animate({ transform: 's1 1 ' + this.cx + ' ' + this.cy }, 500, "bounce");
if (this.label) {
this.label[0].animate({ r: 5 }, 1500, "bounce");
this.label[1].attr({ "font-weight": 400 });
}
});
};
function clickEvent(){
console.log("Clicked!")
}
</script>
Raphaels syntax is a little different.
Check this documentation
Your code should be as below:
u.click(clickEvent);
Related
I know it is weird but I want to customize step line chart like horizontal step is thicker and vertical step is thinner, is there any way to achieve this?
Thanks in advance
You can render other lines in the same positions as the horizontal ones. Please check the below solution:
chart: {
events: {
render: function() {
const points = this.series[0].points;
points.forEach(point => {
const nextPoint = points[point.index + 1];
if (nextPoint) {
const d = [
'M',
point.plotX + this.plotLeft - 1,
point.plotY + this.plotTop,
'L',
nextPoint.plotX + this.plotLeft + 1,
point.plotY + this.plotTop
];
if (point.customPath) {
point.customPath.attr({
d
});
} else {
point.customPath = this.renderer
.path([])
.attr({
d,
stroke: 'red',
'stroke-width': 7
})
.add();
}
}
});
}
}
}
Live demo: http://jsfiddle.net/BlackLabel/wc5zrax9/
API Reference: https://api.highcharts.com/class-reference/Highcharts.SVGRenderer#path
I'm trying to apply ZoomIn and ZoomOut in a line chart on a mobile device. The goal is to click on a zone of the chart and ZoomIn in the first click and ZoomOut on the second. The sequence will always be this one.
I already live to see the documentation / examples and I can not find anything to solve this situation.
I have already tried using this properties in the chart: property
pinchType : 'y',
zoomType: 'none'
I tried the zoomtype but the behavior is not what I expect. I want to have a click to zoom this specific area of the chart. I do not want to zoom with two fingers.
{
chart: {
pinchType : 'x'
},
legend: {
itemStyle: {
color: '#fff'
}
},
plotOptions: {
series: {
animation: {
duration: 2000
}
}
},
xAxis: {
tickInterval: 1
},
series: [
{
type: 'spline',
color : '#fff'
},
{
dashStyle: 'longdash',
color: '#b3be77'
}
],
}
As simple as clicking to get zoomin and zoomout
Yes, the second challenge can be easily achieved by adding this logic to plotOptions.series.events.click callback function:
chart: {
events: {
load: function() {
this.clickedOnce = false;
},
click: function() {
const chart = this;
if (chart.clickedOnce) {
chart.zoomOut();
chart.clickedOnce = false;
}
}
}
},
plotOptions: {
series: {
events: {
click: function(e) {
const chart = this.chart,
yAxis = chart.yAxis[0],
xAxis = chart.xAxis[0];
let x,
y,
rangeX,
rangeY;
if (!chart.clickedOnce) {
x = xAxis.toValue(e.chartX);
y = yAxis.toValue(e.chartY);
rangeX = xAxis.max - xAxis.min;
rangeY = yAxis.max - yAxis.min;
xAxis.setExtremes(x - rangeX / 10, x + rangeX / 10, false);
yAxis.setExtremes(y - rangeY / 10, y + rangeY / 10, false);
chart.redraw();
chart.clickedOnce = true;
} else {
chart.zoomOut();
chart.clickedOnce = false;
}
}
}
}
}
Demos:
https://jsfiddle.net/BlackLabel/kotgea5n/
https://jsfiddle.net/BlackLabel/s8w2xg3e/1/
This functionality is not implemented in Highcharts by default, but you can easily achieve it by adding your custom logic when the chart area is clicked.
When area is clicked the first time use axis.setExtremes() method to zoom in. On the second click use chart.zoomOut() to zoom out the chart. Check demo and code posted below.
Code:
chart: {
events: {
load: function() {
this.clickedOnce = false;
},
click: function(e) {
const chart = this,
yAxis = chart.yAxis[0],
xAxis = chart.xAxis[0];
let x,
y,
rangeX,
rangeY;
if (!chart.clickedOnce) {
x = xAxis.toValue(e.chartX);
y = yAxis.toValue(e.chartY);
rangeX = xAxis.max - xAxis.min;
rangeY = yAxis.max - yAxis.min;
xAxis.setExtremes(x - rangeX / 10, x + rangeX / 10, false);
yAxis.setExtremes(y - rangeY / 10, y + rangeY / 10, false);
chart.redraw();
chart.clickedOnce = true;
} else {
chart.zoomOut();
chart.clickedOnce = false;
}
}
}
}
Demo:
https://jsfiddle.net/BlackLabel/fxm812k4/
API reference:
https://api.highcharts.com/class-reference/Highcharts.Axis#setExtremes
https://api.highcharts.com/class-reference/Highcharts.Chart#zoomOut
https://api.highcharts.com/highcharts/chart.events.click
Using a customEvents plugin (see: https://github.com/blacklabel/custom_events) and adding plotBand on the whole chart area you can register a callback on click and double click events. Using this approach you can make a zoom in on click event and zoom out on double click (not working on mobile devices).
Demo:
https://jsfiddle.net/BlackLabel/6tpb5q2z/
I am trying to add some annotations to a Google Candlestick chart. I noticed someone had already asked this same question (Adding annotations to Google Candlestick chart). The user Aperçu replied with a detailed solution to extend the chart and add annotations since the chart doesn't have any such feature built in. However, when I try this solution I get an error "TypeError: document.querySelectorAll(...)[0] is undefined"
Here is my code:
chartPoints = [
['Budget', 0, 0, 9999, 9999, 'foo1'],
['Sales', 0, 0, 123, 123, 'foo2'],
['Backlog', 123, 123, 456, 456, 'foo3'],
['Hard Forecast', 456, 456, 789, 789, 'foo4'],
['Sales to Budget', 789, 789, 1000, 1000, 'foo5']
];
var data = google.visualization.arrayToDataTable(chartPoints, true);
data.setColumnProperty(5, 'role', 'annotation');
var options = {
legend: 'none',
bar: { groupWidth: '40%', width: '100%' },
candlestick: {
fallingColor: { strokeWidth: 0, fill: '#a52714' },
risingColor: { strokeWidth: 0, fill: '#0f9d58' }
}
};
var chart = new google.visualization.CandlestickChart(document.getElementById('chart_div'));
chart.draw(data, options);
// attempt to use Aperçu's solution
const bars = document.querySelectorAll('#chart_div svg > g:nth-child(5) > g')[0].lastChild.children // this triggers a TypeError
for (var i = 0 ; i < bars.length ; i++) {
const bar = bars[i]
const { top, left, width } = bar.getBoundingClientRect()
const hint = document.createElement('div')
hint.style.top = top + 'px'
hint.style.left = left + width + 5 + 'px'
hint.classList.add('hint')
hint.innerText = rawData.filter(t => t[1])[i][0]
document.getElementById('chart_div').append(hint)
}
I want the chart to show the last piece of data next to the bars (i.e. "foo1", "foo2", etc)
each candle or bar will be represented by a <rect> element
we can use the rise and fall colors to separate the bars from other <rect> elements in the chart
there will be the same number of bars as rows in the data table
once we find the first bar, we can use rowIndex of zero to pull values from the data
we need to find the value of the rise / fall, to know where to place the annotation
then use chart methods to find the location for the annotation
getChartLayoutInterface() - Returns an object containing information about the onscreen placement of the chart and its elements.
getYLocation(position, optional_axis_index) - Returns the screen y-coordinate of position relative to the chart's container.
see following working snippet
two annotations are added
one for the difference in rise and fall
and the other for the value in the column with annotation role
google.charts.load('current', {
callback: drawChart,
packages: ['corechart']
});
function drawChart() {
var chartPoints = [
['Budget', 0, 0, 9999, 9999, 'foo1'],
['Sales', 0, 0, 123, 123, 'foo2'],
['Backlog', 123, 123, 456, 456, 'foo3'],
['Hard Forecast', 456, 456, 789, 789, 'foo4'],
['Sales to Budget', 789, 789, 1000, 1000, 'foo5']
];
var data = google.visualization.arrayToDataTable(chartPoints, true);
data.setColumnProperty(5, 'role', 'annotation');
var options = {
legend: 'none',
bar: { groupWidth: '40%', width: '100%' },
candlestick: {
fallingColor: { strokeWidth: 0, fill: '#a52714' },
risingColor: { strokeWidth: 0, fill: '#0f9d58' }
}
};
var container = document.getElementById('chart_div');
var chart = new google.visualization.CandlestickChart(container);
google.visualization.events.addListener(chart, 'ready', function () {
var annotation;
var bars;
var chartLayout;
var formatNumber;
var positionY;
var positionX;
var rowBalance;
var rowBottom;
var rowIndex;
var rowTop;
var rowValue;
var rowWidth;
chartLayout = chart.getChartLayoutInterface();
rowIndex = 0;
formatNumber = new google.visualization.NumberFormat({
pattern: '#,##0'
});
bars = container.getElementsByTagName('rect');
for (var i = 0; i < bars.length; i++) {
switch (bars[i].getAttribute('fill')) {
case '#a52714':
case '#0f9d58':
rowWidth = parseFloat(bars[i].getAttribute('width'));
if (rowWidth > 2) {
rowBottom = data.getValue(rowIndex, 1);
rowTop = data.getValue(rowIndex, 3);
rowValue = rowTop - rowBottom;
rowBalance = Math.max(rowBottom, rowTop);
positionY = chartLayout.getYLocation(rowBalance) - 6;
positionX = parseFloat(bars[i].getAttribute('x'));
// row value
annotation = container.getElementsByTagName('svg')[0].appendChild(container.getElementsByTagName('text')[0].cloneNode(true));
annotation.textContent = formatNumber.formatValue(rowValue);
annotation.setAttribute('x', (positionX + (rowWidth / 2)));
annotation.setAttribute('y', positionY);
annotation.setAttribute('font-weight', 'bold');
// annotation column
annotation = container.getElementsByTagName('svg')[0].appendChild(container.getElementsByTagName('text')[0].cloneNode(true));
annotation.textContent = data.getValue(rowIndex, 5);
annotation.setAttribute('x', (positionX + (rowWidth / 2)));
annotation.setAttribute('y', positionY - 18);
annotation.setAttribute('font-weight', 'bold');
rowIndex++;
}
break;
}
}
});
chart.draw(data, options);
}
<script src="https://www.gstatic.com/charts/loader.js"></script>
<div id="chart_div"></div>
I'm passing text arrays to my circleCreate function, which creates a wedge for each text. What I'm trying to do is add a click event to each wedge, so when the user clicks on a wedge, it throws an alert with each wedges text.
But it's not working. Only the outer circle is alerting text. And it always says the same text. Both inner circles alert undefined.
http://jsfiddle.net/Yushell/9f7JN/
var layer = new Kinetic.Layer();
function circleCreate(vangle, vradius, vcolor, vtext) {
startAngle = 0;
endAngle = 0;
for (var i = 0; i < vangle.length; i++) {
// WEDGE
startAngle = endAngle;
endAngle = startAngle + vangle[i];
var wedge = new Kinetic.Wedge({
x: stage.getWidth() / 2,
y: stage.getHeight() / 2,
radius: vradius,
angleDeg: vangle[i],
fill: vcolor,
stroke: 'black',
strokeWidth: 1,
rotationDeg: startAngle
});
/* CLICK NOT WORKING
wedge.on('click', function() {
alert(vtext[i]);
});*/
layer.add(wedge);
}
stage.add(layer);
}
This is a typical problem you'll run into with asynchronous JavaScript code such as event handlers. The for loop in your circleCreate() function uses a variable i which it increments for each wedge. This is fine where you use i to create the wedge:
angleDeg: vangle[i],
But it fails where you use it inside the click event handler:
alert(vtext[i]);
Why is that?
When you create the wedge using the new Kinetic.Wedge() call, this is done directly inside the loop. This code runs synchronously; it uses the value of i as it exists at the very moment that this particular iteration of the loop is run.
But the click event handler doesn't run at that time. It may not run at all, if you never click. When you do click a wedge, its event handler is called at that time, long after the original loop has finished running.
So, what is the value of i when the event handler does run? It's whatever value the code left in it when it ran originally. This for loop exits when i equals vangle.length—so in other words, i is past the end of the array, and therefore vangle[i] is undefined.
You can fix this easily with a closure, by simply calling a function for each loop iteration:
var layer = new Kinetic.Layer();
function circleCreate(vangle, vradius, vcolor, vtext) {
startAngle = 0;
endAngle = 0;
for (var i = 0; i < vangle.length; i++) {
addWedge( i );
}
stage.add(layer);
function addWedge( i ) {
startAngle = endAngle;
endAngle = startAngle + vangle[i];
var wedge = new Kinetic.Wedge({
x: stage.getWidth() / 2,
y: stage.getHeight() / 2,
radius: vradius,
angleDeg: vangle[i],
fill: vcolor,
stroke: 'black',
strokeWidth: 1,
rotationDeg: startAngle
});
wedge.on('click', function() {
alert(vtext[i]);
});
layer.add(wedge);
}
}
What happens now is that calling the addWedge() function captures the value of i individually for each loop iteration. As you know, every function can have its own local variables/parameters, and the i inside addWedge() is local to that function—and specifically, local to each individual invocation of that function. (Note that because addWedge() is a function of its own, the i inside that function is not the same as the i in the outer circleCreate() function. If this is confusing, it's fine to give it a different name.)
Updated fiddle
A better way
This said, I recommend a different approach to structuring your data. As I was reading your code, the angle and text arrays caught my eye:
var anglesParents = [120, 120, 120];
var parentTextArray = ['Parent1', 'Parent2', 'Parent3'];
There are similar but lengthier pairs of arrays for children and grandchildren.
You use the values from these arrays with the vtext[i] and vangle[i] references in circleCreate().
In general, unless there's a specific reason to use parallel arrays like this, your code will become cleaner if you combine them into a single array of objects:
[
{ angle: 120, text: 'Parent1' },
{ angle: 120, text: 'Parent2' },
{ angle: 120, text: 'Parent3' }
]
For your nested cirles, we can take this a step further and combine all three rings into a single large array of objects that describes the entire set of nested rings. Where you have these arrays:
var anglesParents = [120, 120, 120];
var anglesChildren = [120, 60, 60, 60, 60];
var anglesGrandchildren = [
33.33, 20, 23.33, 43.33, 22.10, 25.26,
12.63, 28, 32, 33, 27, 36, 14.4, 9.6
];
var grandchildrenTextArray = [
'GrandCHild1', 'GrandCHild2', 'GrandCHild3', 'GrandCHild4',
'GrandCHild5', 'GrandCHild6', 'GrandCHild7', 'GrandCHild8',
'GrandCHild9', 'GrandCHild10', 'GrandCHild11', 'GrandCHild12',
'GrandCHild13', 'GrandCHild14', 'GrandCHild15', 'GrandCHild16'
];
var childrenTextArray = [
'Child1', 'Child2', 'Child3', 'Child4', 'Child5'
];
var parentTextArray = ['Parent1', 'Parent2', 'Parent3'];
It would be:
var rings = [
{
radius: 200,
color: 'grey',
slices: [
{ angle: 33.33, text: 'GrandChild1' },
{ angle: 20, text: 'GrandChild2' },
{ angle: 23.33, text: 'GrandChild3' },
{ angle: 43.33, text: 'GrandChild4' },
{ angle: 22.10, text: 'GrandChild5' },
{ angle: 25.26, text: 'GrandChild6' },
{ angle: 12.63, text: 'GrandChild7' },
{ angle: 28, text: 'GrandChild8' },
{ angle: 32, text: 'GrandChild9' },
{ angle: 33, text: 'GrandChild10' },
{ angle: 27, text: 'GrandChild10' },
{ angle: 36, text: 'GrandChild12' },
{ angle: 14.4, text: 'GrandChild13' },
{ angle: 9.6, text: 'GrandChild14' }
]
},
{
radius: 150,
color: 'darkgrey',
slices: [
{ angle: 120, text: 'Child1' },
{ angle: 60, text: 'Child2' },
{ angle: 60, text: 'Child3' },
{ angle: 60, text: 'Child4' },
{ angle: 60, text: 'Child5' }
]
},
{
radius: 100,
color: 'lightgrey',
slices: [
{ angle: 120, text: 'Parent1' },
{ angle: 120, text: 'Parent2' },
{ angle: 120, text: 'Parent3' }
]
}
];
Now this is longer than the original, what with all the angle: and text: property names, but that stuff compresses out very nicely with the gzip compression that servers and browsers use.
More importantly, it helps simplify and clarify the code and avoid errors. Did you happen to notice that your anglesGrandchildren and grandchildrenTextArray are not the same length? :-)
Using a single array of objects instead of parallel arrays prevents an error like that.
To use this data, remove the circleCreate() function and these calls to it:
circleCreate(anglesGrandchildren, 200, "grey", grandchildrenTextArray);
circleCreate(anglesChildren, 150, "darkgrey", childrenTextArray);
circleCreate(anglesParents, 100, "lightgrey", parentTextArray);
and replace them with:
function createRings( rings ) {
var startAngle = 0, endAngle = 0,
x = stage.getWidth() / 2,
y = stage.getHeight() / 2;
rings.forEach( function( ring ) {
ring.slices.forEach( function( slice ) {
startAngle = endAngle;
endAngle = startAngle + slice.angle;
var wedge = new Kinetic.Wedge({
x: x,
y: y,
radius: ring.radius,
angleDeg: slice.angle,
fill: ring.color,
stroke: 'black',
strokeWidth: 1,
rotationDeg: startAngle
});
wedge.on('click', function() {
alert(slice.text);
});
layer.add(wedge);
});
});
stage.add(layer);
}
createRings( rings );
Now this code isn't really any shorter than the original, but some of the details are more clear: slice.angle and slice.text show clearly that the angle and text belong to the same slice object, where with the original vangle[i] and vtext[i] we're left hoping that the vangle and vtext arrays are the correct matching arrays and are properly lined up with each other.
I also used .forEach() instead of a for loop; since you're using Canvas we know you are on a modern browser. One nice thing is that forEach() uses a function call, so it automatically gives you a closure.
Also, I moved the calculations of x and y outside the loop since they are the same for every wedge.
Here's the latest fiddle with this updated code and data.
because each anonymous function you define as an event handler with each loop iteration will share the same scope, each function will reference the same var (i) as the array address for the text you are trying to display. Because your are redefining the var i with each loop, you will always see the last text message in your message array displayed on each click event because the last value assigned to i will have been the length of your array.
here is the solution:
var layer = new Kinetic.Layer();
function circleCreate(vangle, vradius, vcolor, vtext) {
startAngle = 0;
endAngle = 0;
for (var i = 0; i < vangle.length; i++) {
// WEDGE
startAngle = endAngle;
endAngle = startAngle + vangle[i];
var wedge = new Kinetic.Wedge({
x: stage.getWidth() / 2,
y: stage.getHeight() / 2,
radius: vradius,
angleDeg: vangle[i],
fill: vcolor,
stroke: 'black',
strokeWidth: 1,
rotationDeg: startAngle
});
(function(index) {
wedge.on('click', function() {
alert(vtext[i]);
});
})(i)
layer.add(wedge);
}
stage.add(layer);
}
Your problem is with your loop index. Try this:
(function(j) {
wedge.on('click', function() {
alert(vtext[j]);
});
})(i);
See here
The problem is that when your click handler gets called, i has the value that it had at the end of your loop, so vtext[i] is obviously undefined. By wrapping it in a closure, you can save the value of the loop index at the time the loop ran for your click handler.
I want to draw a marker on the last point. Data source is dynamic.
Have a look at following code
$(function() {
$("#btn").click(function() {
var l = chart.series[0].points.length;
var p = chart.series[0].points[l - 1];
p.marker = {
symbol: 'square',
fillColor: "#A0F",
lineColor: "A0F0",
radius: 5
};
a = 1;
chart.series[0].points[l - 1] = p;
chart.redraw(false);
});
var ix = 13;
var a = 0;
var chart = new Highcharts.Chart({
chart: {
renderTo: 'container',
events: {
load: function() {
var series = this.series[0];
setInterval(function() {
ix++;
var vv = 500 + Math.round(Math.random() * 40);
chart.series[0].data[0].remove();
var v;
if (a == 1) v = {
y: vv,
x: ix,
marker: {
symbol: 'square',
fillColor: "#A0F",
lineColor: "A0F0",
radius: 5
}
}
else v = {
y: vv,
x: ix
}
a = 0;
series.addPoint(v);
}, 1500);
}
}
},
plotOptions: {
series: {}
},
series: [{
data: [500, 510, 540, 537, 510, 540, 537, 500, 510, 540, 537, 510, 540, 537]}]
});
});
http://jsfiddle.net/9zNUP/
On button click event I am trying to draw marker on last point which is already added to chart.
Is there a way to do that??
$("#btn").click(function() {
var l = chart.series[0].points.length;
var p = chart.series[0].points[l - 1];
p.update({
marker: {
symbol: 'square',
fillColor: "#A0F",
lineColor: "A0F0",
radius: 5
}
});
a = 1;
});
solution # http://jsfiddle.net/jugal/zJZSx/
Also tidied up your code a little, removed the removal of point before adding one at the end, highcharts supports it inbuilt with the third param to addPoint as true, which denotes shift series, which removes first point and then adds the given point.
I didn't really understand what the a vv etc were, but well i didn't bother much either. I think this is enough based on what you asked for.