I've created a custom class called CheckBoxLegend which extends Legend. It sets the legendItemClass to be the flex.utils.ui.charts.CheckBoxLegendItem class which extends the default LegendItem to add the CheckBox on the left side of the legend item.
Clicking on the legend item toggles the CheckBox and updates the Chart to show or hide the corresponding series. The series is hidden by setting the alpha value to 0.
Here is a snippet of how you use it in MXML:
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:charts="flex.utils.ui.charts.*">
<mx:LineChart id="linechart" ... />
<charts:CheckBoxLegend dataProvider="{linechart}"
color="#000000" direction="horizontal"/>
</mx:Application>
xmlns:charts="flex.utils.ui.charts.*">
<mx:LineChart id="linechart" ... />
<charts:CheckBoxLegend dataProvider="{linechart}"
color="#000000" direction="horizontal"/>
</mx:Application>
Here is the example, right click to view source:
Note that the minimum and maximum values of the vertical axis don't get updated when you uncheck one or more series. The Axis calculates these values but doesn't take into account the visibility of the Series. So I've added a new Update Vertical Axis Min/Max CheckBox that will go through all the y axis number values and calculate the minimum and maximum values for the visible series. I haven't tested this out on complicated datasets or different chart types, but hopefully it will be a good starting point.
Originally I played around with actually removing the series from the chart (instead of hiding it), but that caused more problems because when you remove a series the chart will automatically re-color any of the remaining series (unless you specified the stroke/color/fill styles) and the legend gets re-populated without the unchecked series. So it was easiest to just hide the series.
Hi Chris,
ReplyDeletethanks for this information, this is really helpful.
Would you also post the source code for "flex.utils.ui.events.CheckBoxLegendItemChangedEvent" as this is referenced in your example.
Cheers,
Theo
Hi there,
ReplyDeleteThe source code is all there, if you right click and choose "View Source" then you'll be able to browse all the files. Expand the tree on the left to find the CheckBoxLegendItemChangedEvent.as file.
Here is the direct link to the class too:
http://keg.cs.uvic.ca/flexdevtips/checkboxlegend/srcview/source/flex/utils/ui/events/CheckBoxLegendItemChangedEvent.as.html
If i wanted to actually remove the series is that possible, so the checkbox and name is removed from the legend as well.
ReplyDeleteFrom looking at the code it looks like if i add the below line and remove the series.alpha = (legendItem.selected ? 1 : 0); it will remove the line from the graph, but cant figure out how to remove from the legend.
series.alpha = (legendItem.selected ? 1 : 0);
thanks
Hi there,
ReplyDeleteInstead of the line:
series.alpha = (legendItem.selected ? 1 : 0);
You could do something like this:
// Get the Chart which is the legend's dataProvider
var chart:ChartBase = (dataProvider as ChartBase);
// Get the Array of Series objects from the Chart
var seriesArray:Array = chart.series;
// Remove the current Series
var index:int = seriesArray.indexOf(series);
seriesArray.splice(index, 1);
// Set the modified series back on the Chart
chart.series = seriesArray;
Obviously doing it like this you wouldn't have any way to add the series back in since the CheckBox is removed also.
Cheers,
Chris
Hello Chris,
ReplyDeleteFirst of all thanks for this nice code. It is very helpful.
I just want to add a separate checkbox so that when I click on it, it uncheck or check all the legend checkboxes.
any idea on how the function on that new checkbox could look like?
Hi Mario,
ReplyDeleteI've updated the example above to include a CheckBox that selects all or none of the legend items.
To do that I added two new functions to the CheckBoxLegend class: selectAll(), and selectNone().
Chris
Hi Chris,
ReplyDeleteThanks for the checkbox.
But it seems that there is a small bug : when you deselect all/or one of the lineseries, and mouse over the chart area, the datatip of the deselected series are still showing!
Regards,
Marius
Hi Marius,
ReplyDeleteThanks for pointing that out. How embarrassing!
I obviously wasn't thinking very clearly when I originally posted this entry. Instead of setting series.alpha = 0 to hide the line it now sets series.visible = false instead. And this prevents the tooltips from showing up too,
Thanks again for the comment.
Chris
Hi Chris,
ReplyDeleteI change the CheckBoxLegend's direction to vertical. And added a verticalScrollPolicy to "on". When the height is reduced, the verticalScrollPolicy does not apply. The legend items show in two columns, one beside another instead of showing a scroll bar and one column.
Any idea on the cause of this?
Thanks in advance,
Marius
Hi Marius,
ReplyDeleteThe base Legend class doesn't support verticalScrollPolicy (even though it appears to). Instead I'd suggest wrapping the CheckBoxLegend inside a Canvas, VBox, or HBox and setting your scroll policies on it.
Chris
Hi Chris,
ReplyDeleteThanks for the tip.It works now!
Have a nice day,
Marius
Hi Chris,
ReplyDeleteThanks for this component - it rocks!
One quick question: is it possible to force the vertical chart axis to resize when a series is hidden or shown? For example, if one series has very large values, then it would be great for the axis to 'shrink' when that series is hidden.
Is this possible?
Thanks,
Jono
Hi Jono,
ReplyDeleteI've updated the example above to include the Update Vertical Axis Maximum CheckBox. If selected causes then when the legend items change the vertical axis (a LinearAxis) maximum value will be calculated using only the data points from visible series. Since the source code for the charting classes aren't available I don't know of a better way to do this.
Chris
Chris,
ReplyDeleteThanks, that works (almost) perfectly. I added in some code to take two additional conditions into account:
1. Calculate the minimum value too (since I sometimes have charts that go below the zero line).
2. Take into account series other than lineSeries (eg columnSeries).
I have a really weird bug though with column series. Have a look at the video that I uploaded to http://www.youtube.com/watch?v=S_3I5WUiDBA
Note that the Net Profit series are both based on the same data - the only difference is that one is a columnSeries and the other is a lineSeries. When the only visible series is a columnSeries and that is hidden and then made visible again, the chart is not updated properly. Debugging the code in this scenario (i.e. no series are shown, and then the column series is made visible) seems to show that there is no items being passed through to the getMinmax function.
private function updateVerticalAxis():void {
var min:Number = 0;
var max:Number = 0;
vaxis.autoAdjust = false;
trace('----start chart----');
for each (var series in profitChart.series) {
if (series.visible) {
var items:Array = series.items;
var seriesMinMax:Array = getMinMax(items);
min = Math.min(min, seriesMinMax[0]);
max = Math.max(max, seriesMinMax[1]);
}
}
//if (max != Number.MIN_VALUE) {
vaxis.maximum = max;
//}
//if (min != Number.MIN_VALUE) {
vaxis.minimum = min;
//}
vaxis.autoAdjust = true;
trace('----end chart----');
}
private function getMinMax(items:Array):Array {
var min:Number = 0;
var max:Number = 0;
var count:int = items.length;
var i:int = 1;
trace('--start series--');
for each (var item:* in items) {
var num:Number = item.yNumber;
min = Math.min(min, num);
max = Math.max(max, num);
trace('item ' + i + ' of ' + count);
trace('num:' + num + ' min:' + min + ' max:' + max);
i++;
}
var seriesMinMax:Array = new Array(min, max);
trace('minMax');
trace(seriesMinMax);
trace('--end series--');
return seriesMinMax;
}
I wonder if I am missing something blindingly obvious here...
OK, potential fix found. If there are no series visible, then the vertical axis is not resized.
ReplyDeleteprivate function updateVerticalAxis():void {
var min:Number = 0;
var max:Number = 0;
var count:int;
vaxis.autoAdjust = false;
trace('----start chart----');
for each (var series in profitChart.series) {
if (series.visible) {
count++;
var items:Array = series.items;
var seriesMinMax:Array = getMinMax(items);
min = Math.min(min, seriesMinMax[0]);
max = Math.max(max, seriesMinMax[1]);
}
}
if (count > 0) {
vaxis.maximum = max;
vaxis.minimum = min;
}
vaxis.autoAdjust = true;
trace('----end chart----');
}
This seems to work on initial testing..
This next upgrade is to round the min and max properly (i.e. make a ceiling and floor), so that the chart doesnt touch right to the top and right to the bottom. Eg: if max = 9800, then chart should round up to 10000. Will report back when I get there... :)
Thanks a lot Jon, that's great. I've updated the example above to take into account the minimum values too just like your code does. And I also added some really simple ceil/floor calculations to round the min/max up to the next interval. It seems to work nicely (at least it matches the original min/max values).
ReplyDeleteChris
Awesome. Very clever way of calculating the ceiling and floor.
ReplyDeleteI still have a problem with some series points 'dissapearing' with columnSeries for some strange reason...but otherwise its cool
Chris,
ReplyDeleteSorry to be back, but I've found another bug, which occurs when the dataprovider for the chart is changed - it seems that the axis is not reset properly until a legenditem is ticked/unticked
Check http://www.youtube.com/watch?v=zfNRayc40jA
Any ideas on this one?
OK, think I got this one figured out too.
ReplyDeleteWhen swapping dataproviders, then its necessary to use the callLater method before the ceil and floor is calculated. If you dont do this, the the ceiling and floor is based on the old data, and not the new data - so the intervals may be totally wrong. So, we have to let the chart calculate the new intervals first, and then increment them using ceil and floor.
private function updateVerticalAxisMinMax():void {
var max:Number = 0;
var min:Number = 0;
var count:int = 0;
for each (var series:LineSeries in linechart.series) {
if (series.visible) {
// contains LineSeriesItem objects
var items:Array = series.items;
// Get the min and max y values for each item from the yNumber property
var minMax:Array = getMinMax(items, "yNumber");
min = Math.min(min, minMax[0]);
max = Math.max(max, minMax[1]);
count++;
}
}
if (count > 0) {
var vaxis:LinearAxis = (linechart.verticalAxis as LinearAxis);
vaxis.maximum = max;
vaxis.minimum = min;
callLater(setInterval);
}
}
private function setInterval():void {
var vaxis:LinearAxis = (linechart.verticalAxis as LinearAxis);
vaxis.maximum = Math.ceil(vaxis.maximum / vaxis.interval) * vaxis.interval;;
vaxis.minimum = Math.floor(vaxis.minimum / vaxis.interval) * vaxis.interval;;
}
Hi Jon,
ReplyDeleteThis one took a while, but turned out to be very simple.
The reason why the data points disappear is because the vertical axis min/max values are wrong and so the line renderer doesn't draw the points that aren't visible (which makes the gaps in the line).
I've updated my example above to show an updated version which lets you change the data provider like yours. It also has the fix for the disappearing data points.
The two key changes are:
1. When the "Update Min/Max" CheckBox is un-selected, we have to set the vertical axis minimum/maximum values to NaN, this will cause the axis to properly calculate the min/max values (and we won't get disappearing points).
2. After changing the chart dataProvider, you must call linechart.validateNow() and then you can call the updateVerticalAxisMinMax() function. Otherwise you'll run into that problem where the chart hasn't been updated yet and so our calculated min/max values are wrong.
Chris
Excellent work - very clean and efficient.
ReplyDeleteAre there any restrictions on using this for commercial development?
Hi Kyle,
ReplyDeleteThere are no restrictions, it is open source (Creative Commons License).
Chris
I have to agree, this is an indispensable part of any chart with alot of series!
ReplyDeleteDoes anyone know if it's possible to control which series are shown at startup? Like can you default which checks are on for each series?
I figured it out! I modified the AddItem method to be picky about what got turned on and what didn't. I think disabled the Select All function call after startup!
ReplyDeleteprivate function legendItemAdded(event:ChildExistenceChangedEvent):void {
// add our change event listener
var defaults:ArrayCollection = new ArrayCollection(new Array("AECI","SWPP","MHEB","ONT","PJM"));
if (event.relatedObject is CheckBoxLegendItem) {
var item:CheckBoxLegendItem = (event.relatedObject as CheckBoxLegendItem);
item.addEventListener(Event.CHANGE, legendItemChanged);
if (defaults.contains(item.label)){
item.selected=true;
var element:IChartElement = item.element;
if (toggleChartSeries && (element is Series)) {
var series:Series = (element as Series);
series.visible = item.selected;
}
}
else {
item.selected = false;
var element:IChartElement = item.element;
if (toggleChartSeries && (element is Series)) {
var series:Series = (element as Series);
series.visible = false;
}
}
}
}
Looks good Paul, glad to help :)
ReplyDeleteHi Chris,
ReplyDeleteI would like to have the chart working like this :
On creationComplete, only some specific items are checked and shown.
for example, Profit, Expenses are checked. Then the user can check other items as he wish.
Any idea on how to do this ?
Thanks in advance
Marius
Hi Marius,
ReplyDeleteIf you add a creationComplete event handler, you can do this to show or hide various series:
legend.setSeriesShown(profitSeries, true);
legend.setSeriesShown(expensesSeries, true);
legend.setSeriesShown(amountSeries, false);
legend.setSeriesShown(stockSeries, false);
The only change I made to the example above besides adding the creation complete handler was adding the id property to each Series.
Chris
Hi Chris,
ReplyDeleteThanks for your answer.
Unfortunetely it doesn't work in my case as I'm adding lineseries in AS3 ont directly in mxml. (in a "for" loop, mylineserie = new LineSeries(); mylinechart.series[i] = mylineserie; )
Dou know how I can resolve this ?
Improvement: chart cursor(vertical line): http://flex.amcharts.com/examples/guides_as_areas
Any idea how to add this feature to your linechart ?
thanks in advance for your help.
Marius
Do you have access to the legend in your AS3 code when you create those series? If so the code I gave you before should work.
ReplyDeleteI just made an update that should do what you need for setting the initial visibility. Now, when you create the LineSeries, simply set mylineserie.visible = false; and the checkbox should be unchecked.
As for the Guides, no I have no idea, but it looks pretty cool.
ther's a possibility to install this on FLEX 4?
ReplyDeleteIt should work with Flex 4, give it a try.
ReplyDeleteThe fuction of updating Vertial Axis Min/Max is not right, the Min is always 0
ReplyDeleteThank you so much for this post, it saved me hours ;-)
ReplyDeleteI just have one question about the re-calculation of min and max values : the original calculated values of Flex includes a little tolerance. Without this tolerance, my series have sometimes truncated min and max point values.
Do someone know how to calculate this tolerance ? There might have a calculation rule based on the interval and the values ?
Cheers
Here is a screen capture to exploain my previous post (truncated max and mix points)
ReplyDeletehttp://www.hostingpics.net/viewer.php?id=598889ChartSeriesjpg.jpg
If riafan pointed out the min value is always 0. To fix this change the 2 places where it says:
ReplyDeletevar max:Number = 0;
var min:Number = 0;
to something like this:
var max:Number = int.MIN_VALUE;
var min:Number = int.MAX_VALUE;
As for the tolerance around the min/max values François, if you look at the code I do add some tolerance - it calculates the min/max values and then adjusts them to the next interval. But obviously if your point falls right on the interval, then there is no padding. I don't know how Flex calculates this since I can't see the source code. But I'd suggest trying some combination of the min vs the max and the interval. You could also look at this line in the CheckBoxLegendText.mxml class where I add in some padding:
max = Math.ceil(max / vAxis.interval) * vAxis.interval;
min = Math.floor(min / vAxis.interval) * vAxis.interval;
You could check if the old max equals the new max, in which case you'd need to add some more padding. Same for min.
Chris
My tolerance problem is finally due to the CircleItemRenderer used to render the plots on line series.
ReplyDeleteThe plot size is not included in the min/max values.
I found a (not perfect) solution to this problem, which works !
var plotMargin:Number = ( plotSize / ( graphSize / max )) /2;
(verticalAxis as LinearAxis).maximum = max + plotMargin;
(verticalAxis as LinearAxis).minimum = min - plotMargin;
With :
plotSize = the height of the plot point in pixel (around 9 px)
graphSize = The height of the chart in pixels from min to max
That's look good François. There's always some "fudge" factors involved with charting!
ReplyDeleteThis works great and was a real lifesaver! Thanks for posting this!
ReplyDeleteAn excellent development, thanks a lot for sharing such a great piece of code.
ReplyDeleteGreat stuff ! Thanks for sharing this !
ReplyDeleteI am using the Dateaxis to render a live value against time, I can get the min and Max set with minutes as my dataunits, but when the data starts to comeing, the graph doesnt show, When I remove the min and max, it shows. please help see code here
ReplyDeletehttp://pastebin.com/U6E3hzNm
Thanks in advance
Hi John,
ReplyDeleteSorry for not getting back to you sooner.
I'm not sure I understand your problem, or see how it relates to the checkbox legend? Your example only lists a single line series, so a checkbox legend wouldn't be of much use.
I haven't used DateAxis much, so I can't really help you much there.
Chris
This is an awesome gift of code/idea here but I am having one little problem.
ReplyDeleteI am getting "Import Flex could not be found" on ...
import flex.utils.ui.events.CheckBoxLegendItemChangedEvent;
I am guessing it is in my general environment setup or something.
What am I missing?
Thanks ... JohnH
Hi John,
ReplyDeleteIt sounds like you're missing the event class.
If you right click on the example app above and choose View Source. Then expand out the src tree item on the left, you'll see the class CheckBoxLegendItemChangedEvent.as under flex.utils.ui.events. That class needs to be in your project, perhaps it was missed?
Chris
Ah, I see.
ReplyDeleteSo adding these will get me there?
This is fantastic, thanks.
JohnH
Sorry ... but I am getting a
ReplyDelete"Type was not found or was not a compile time constant: CheckBoxLegendItem" in the CheckBoxLegendItemChangedEvent.as file
Thoughts?
JohnH
Same thing - make sure you add the flex.utils.ui.charts.CheckBoxLegendItem class. Every class that is included in my example above needs to be in your project, except the main application if you've got your own application.
ReplyDeleteThank you for sticking with me on this.
ReplyDeleteI have that problem straightened out but now I am getting this error in my main app file .... "Type was not found or was not a compile time constant: CheckBoxLegend."
This has got to be one silly syntax problem or something equally simple. But I am stumped.
What baffels me is I was not getting this until I got that last issue resolved.
JohnH
Make sure you have the flex.utils.ui.charts.CheckBoxLegend class inside your project.
ReplyDeleteThen make sure your MXML includes the namespace charts:
<s:Application xmlns:charts="flex.utils.ui.charts.*">
<charts:CheckBoxLegend/>
Please look at the CheckBoxLegendTest.mxml file and you'll see how it works.
Flex sometimes only shows one error, and when you fix it then it will be able to continue compiling and find other errors.
The crux of this issue looks like a Flex bug. Specifically it looks like the _transforms in AxisBase.describeData() are incorrect/out-of-date. Does anyone know if this has been reported in Adobe's JIRA? I couldn't find it.
ReplyDeleteHey Chris, thanks again for the help getting this going back in Aug.
ReplyDeleteNow I am wondering how to effect the font on the legend labels. No problems with effecting the size of the marks but I just can't seem to get the label text size to change.
I have tried "fontSize" within an mx:Style as well as within the charts:CheckBoxLegend at the end of the chart.
My goal is to increase the size of the font.
Thoughts?
JohnH
Hi John,
ReplyDeleteMy apologies for not replying sooner. If you still need help with the Legend labels, here is what I did.
I believe the styles must be set on the LegendItem (or in our case the CheckBoxLegendItem).
So try adding something like this to a Style block in your application (this will apply to all CheckBoxLegendItems):
Flex 3:
CheckBoxLegendItem {
color: #009900;
fontWeight: normal;
}
Flex 4:
@namespace charts "flex.utils.ui.charts.*";
charts|CheckBoxLegendItem {
color: #009900;
fontWeight: normal;
}
Hope that works for you.
very nice example - Thank You
ReplyDeleteHello, I have a line chart in Flex 3 with two series. It feeds on an XML file. How I can do to make each series have tooltips with different data? That is, the series 1, data1 tooltip, tooltip Series 2 with built data2.
ReplyDeletedata1 and data2 come in the xml. Thank you!
I explain better: The data I have nothing to do with the axes, are any data.
ReplyDeletehii,
ReplyDeletei called id_graph_fastLog.validateNow(); before calling updateVerticalAxisMinMax(); still i am uable to see updated value at graph after legend is uncheck
please help me to solve this