This tutorial is for Dojo 1.8 and may be out of date.
Up to date tutorials are available.
Advanced Charting with Dojo
While most developers only need basic charts, dojox/charting
is capable of highly advanced charts: charts with animations, charts that respond to changes in data, and charts that respond to events. In this tutorial, you will learn about using some these advanced capabilities within dojox/charting
.
This tutorial explores advanced dojo charting topics. If you aren't familiar with basic dojo charting, please read the basic charting tutorial first. This tutorial will also use Dojo's data stores.
Getting Started
Creating advanced, dynamic charts with dojox/charting
is probably easier than you would believe. Creating charts that handle changes in data, zooming, panning, and scrolling is simple, thanks to the Dojo Toolkit!
Dojo Stores and Charting
Dojo provides an outstanding, flexible Store API which allows developers to manage (add, edit, delete, query, etc.) data in an efficient manner. Due to the prevalence of Store usage within Dojo applications, dojox/charting/StoreSeries
has incorporated solutions for creating data series from Store data.
StoreSeries
dojox/charting/StoreSeries
was specifically created to incorporate data stores within charts. The first step in using store data within a chart is creating the store:
require(["dojo/store/Observable", "dojo/store/Memory"], function(ObservableStore, MemoryStore) { // Initial data var data = [ // This information, presumably, would come from a database or web service // Note that the values for site are either 1 or 2 { id: 1, value: 20, site: 1 }, { id: 2, value: 16, site: 1 }, { id: 3, value: 11, site: 1 }, { id: 4, value: 18, site: 1 }, { id: 5, value: 26, site: 1 }, { id: 6, value: 19, site: 2 }, { id: 7, value: 20, site: 2 }, { id: 8, value: 28, site: 2 }, { id: 9, value: 12, site: 2 }, { id: 10, value: 4, site: 2 } ]; // Create the data store // Store information in a data store on the client side var store = new ObservableStore(new MemoryStore({ data: { identifier: "id", label: "Users Online", items: data } })); });
Wrapping the store in Observable is important, as it allows notifications to be sent to the store, which in turn notifies the StoreSeries instance we will create.
With the store in place, the chart, plot, and axes should be added just as they were in the basic tutorial. With the chart, plot, and axes created, it's time to implement StoreSeries:
// Adds a StoreSeries to the y axis, queries for all site 1 items chart.addSeries("y", new StoreSeries(store, { query: { site: 1 } }, "value"));
With the StoreSeries in place, each time the data store is notified of a change, the series is re-rendered on the chart.
View DemoCharting Animation: Zooming, Scrolling, and Panning
Dojo's charting solution is flexible enough to allow a change in data at any time, so it's only natural that charts would also need to be flexible enough to accommodate those changes in data. Zooming, scrolling, and panning in charts allows just that.
The role that each animation plays is straight-forward:
- Zooming - Allows developers to enlarge elements within the chart without enlarging the chart itself
- Scrolling - Allows the user to click and drag their way around the chart
- Panning - Allows the user to elegantly pan to a different view of the chart
These effects can be accomplished with two chart methods: setAxisWindow
and setWindow
.
setAxisWindow(name,scale,offset)
setAxisWindow
defines a window on the named axis with a scale factor, which starts at the set offset in data coordinates. The setAxisWindow
method accepts three arguments:
- name - The name of the axis
- scale - Scale which the chart should change to
- offset - The chart's destination offset
Usage of setAxisWindow
would look like:
// Changes the x axis to twice the scale, offset by 100 chart.setAxisWindow("x",2,100).render();
setWindow(sx,sy,dx,dy)
setWindow
sets scale and offsets on all plots of the chart. The setWindow
method accepts four arguments:
- sx - The magnification factor on horizontal axes
- sy - The magnification factor on vertical axes
- dx - The offset of horizontal axes in pixels
- dy - The offset of vertical axes in pixels
Usage of setWindow
would look like:
// Returns the chart to it original position chart.setWindow(1, 1, 0, 0).render();
Each method requires the chart's render
method to be called for changes to be reflected on the chart.
Example: Zooming, Scrolling, and Panning
The following example allows the user to zoom, pan, and scroll the chart using sliders.
<script> // Require the dependencies require(["dijit/form/HorizontalSlider", "dijit/form/HorizontalRule", "dijit/form/HorizontalRuleLabels", "dojox/charting/Chart", "dojox/charting/themes/Claro", "dojox/lang/functional/object", "dijit/registry", "dojo/on", "dojo/dom", "dojo/_base/event", "dojo/parser", "dojox/charting/axis2d/Default", "dojox/charting/plot2d/Areas", "dojox/charting/plot2d/Grid", "dojo/domReady!"], function(HorizontalSlider, HorizontalRule, HorizontalRuleLabels, Chart, Claro, functionalObject, registry, on, dom, baseEvent, parser) { // Initialize chart, scales, and offsets var chart, moveable, scaleX = 1, scaleY = 1, offsetX = 0, offsetY = 0; // Updates the slider values, animates the change in scale and offsets var reflect = function(){ functionalObject.forIn(chart.axes, function(axis){ var scale = axis.getWindowScale(), offset = Math.round(axis.getWindowOffset() * axis.getScaler().bounds.scale); if(axis.vertical){ scaleY = scale; offsetY = offset; }else{ scaleX = scale; offsetX = offset; } }); setTimeout(function(){ registry.byId("scaleXSlider").set("value", scaleX); registry.byId("offsetXSlider").set("value", offsetX); registry.byId("scaleYSlider").set("value", scaleY); registry.byId("offsetYSlider").set("value", offsetY); }, 25); }; // Update the scale and offsets of *all* plots on the chart var update = function(){ chart.setWindow(scaleX, scaleY, offsetX, offsetY, { duration: 1500 }).render(); reflect(); }; // The following four methods are fired when the corresponding sliders are changed var scaleXEvent = function(value){ scaleX = value; dom.byId("scaleXValue").innerHTML = value; update(); }; var scaleYEvent = function(value){ scaleY = value; dom.byId("scaleYValue").innerHTML = value; update(); }; var offsetXEvent = function(value){ offsetX = value; dom.byId("offsetXValue").innerHTML = value; update(); }; var offsetYEvent = function(value){ offsetY = value; dom.byId("offsetYValue").innerHTML = value; update(); }; // Function called when the mouse goes down var _init = null; var onMouseDown = function(e){ console.warn("mousedown"); _init = {x: e.clientX, y: e.clientY, ox: offsetX, oy: offsetY}; baseEvent.stop(e); }; // Function called when the mouse is released var onMouseUp = function(e){ if(_init){ // Clears the click/drag, updates the chart console.warn("mouseup"); _init = null; reflect(); baseEvent.stop(e); } }; // Create the base chart chart = new Chart("chart"); chart.setTheme(Claro); chart.addAxis("x", {fixLower: "minor", natural: true, stroke: "grey", majorTick: {stroke: "black", length: 4}, minorTick: {stroke: "gray", length: 2}}); chart.addAxis("y", {vertical: true, min: 0, max: 30, majorTickStep: 5, minorTickStep: 1, stroke: "grey", majorTick: {stroke: "black", length: 4}, minorTick: {stroke: "gray", length: 2}}); chart.addPlot("default", {type: "Areas", animate: {duration: 1800}}); chart.addSeries("Series A", [0, 25, 5, 20, 10, 15, 5, 20, 0, 25]); chart.addAxis("x2", {fixLower: "minor", natural: true, leftBottom: false, stroke: "grey", majorTick: {stroke: "black", length: 4}, minorTick: {stroke: "gray", length: 2}}); chart.addAxis("y2", {vertical: true, min: 0, max: 20, leftBottom: false, stroke: "grey", majorTick: {stroke: "black", length: 4}, minorTick: {stroke: "gray", length: 2}}); chart.addPlot("plot2", {type: "Areas", hAxis: "x2", vAxis: "y2", animate: {duration: 1800}}); chart.addSeries("Series B", [15, 0, 15, 0, 15, 0, 15, 0, 15, 0, 15, 0, 15, 0, 15, 0, 15], {plot: "plot2"}); chart.addPlot("grid", { type: "Grid", hMinorLines: true }); chart.render(); parser.parse(); // Add change events to the sliders to know when chart changes should be triggered registry.byId("scaleXSlider").on("Change", scaleXEvent, true); registry.byId("scaleYSlider").on("Change", scaleYEvent, true); registry.byId("offsetXSlider").on("Change", offsetXEvent, true); registry.byId("offsetYSlider").on("Change", offsetYEvent, true); // Add mouse events to the chart to allow click and drag var chartNode = dom.byId("chart"); on(chartNode, "mousedown", onMouseDown); on(chartNode, "mouseup", onMouseUp); }); </script> <!-- create the sliders to control chart scale and offsets --> <table> <tr><td align="center" class="pad">Scale X (<span id="scaleXValue">1</span>)</td></tr> <tr><td> <div id="scaleXSlider" data-dojo-type="dijit/form/HorizontalSlider" data-dojo-props=" value: 1, minimum: 1, maximum: 5, discreteValues: 5, showButtons: false" style="width: 600px;"> <div data-dojo-type="dijit/form/HorizontalRule" data-dojo-props=" container: 'bottomDecoration', count: 5" style="height:5px;"></div> <div data-dojo-type="dijit/form/HorizontalRuleLabels" data-dojo-props=" container: 'bottomDecoration', count: 5, minimum: 1, maximum: 5, constraints: {pattern: '##'}" style="height:1.2em;font-size:75%;color:gray;"></div> </div> </td></tr> <tr><td align="center" class="pad">Scale Y (<span id="scaleYValue">1</span>)</td></tr> <tr><td> <div id="scaleYSlider" data-dojo-type="dijit/form/HorizontalSlider" data-dojo-props=" value: 1, minimum: 1, maximum: 5, discreteValues: 5, showButtons: false" style="width: 600px;"> <div data-dojo-type="dijit/form/HorizontalRule" data-dojo-props=" container: 'bottomDecoration', count: 5" style="height:5px;"></div> <div data-dojo-type="dijit/form/HorizontalRuleLabels" data-dojo-props=" container: 'bottomDecoration', count: 5, minimum: 1, maximum: 5, constraints: {pattern: '##'}" style="height:1.2em;font-size:75%;color:gray;"></div> </div> </td></tr> <tr><td align="center" class="pad">Offset X (<span id="offsetXValue">0</span>)</td></tr> <tr><td> <div id="offsetXSlider" data-dojo-type="dijit/form/HorizontalSlider" data-dojo-props=" value: 1, minimum: 0, maximum: 500, discreteValues: 501, showButtons: false" style="width: 600px;"> <div data-dojo-type="dijit/form/HorizontalRule" data-dojo-props=" container: 'bottomDecoration', count: 6" style="height:5px;"></div> <div data-dojo-type="dijit/form/HorizontalRuleLabels" data-dojo-props=" container: 'bottomDecoration', count: 6, minimum: 0, maximum: 500, constraints: {pattern: '####'}" style="height:1.2em;font-size:75%;color:gray;"></div> </div> </td></tr> <tr><td align="center" class="pad">Offset Y (<span id="offsetYValue">0</span>)</td></tr> <tr><td> <div id="offsetYSlider" data-dojo-type="dijit/form/HorizontalSlider" data-dojo-props=" value: 1, minimum: 0, maximum: 500, discreteValues: 501, showButtons: false" style="width: 600px;"> <div data-dojo-type="dijit/form/HorizontalRule" data-dojo-props=" container: 'bottomDecoration', count: 6" style="height:5px;"></div> <div data-dojo-type="dijit/form/HorizontalRuleLabels" data-dojo-props=" container: 'bottomDecoration', count: 6, minimum: 0, maximum: 500, constraints: {pattern: '####'}" style="height:1.2em;font-size:75%;color:gray;"></div> </div> </td></tr> </table><br /><br /> <!-- the chart node --> <div id="chart" style="width: 800px; height: 400px;"></div>View Demo
dojox/charting Events
Event connections within all interactive interfaces are important. It's important that they are effectively and efficiently relayed, as well as plainly available. With those goals in mind, an API by which developers can connect and respond to user-triggered events has been created.
Event listeners are assigned to specific plots on a given chart with the connectToPlot
method:
// Connect an event to the "default" plot chart.connectToPlot("default", function(evt) { // Use console to output information about the event console.info("Chart event on default plot!",evt); console.info("Event type is: ",evt.type); console.info("The element clicked was: ",evt.element); });
The connectToPlot's event
object is very different from a traditional DOM event. This event object contains the following key properties:
- type - The type of event (
onclick
,onmouseover
, oronmouseleave
) - element - The type of element hovered (
marker
,bar
,column
,circle
,slice
) - x - The
x
value of the point - y - The
y
value of the point - shape - The gfx shape object that represents a data point
A full listing of event properties can be found at the dojox/charting reference guide.
The plugins covered within the basic charting tutorial use charting event solutions to trigger movement in shapes.
Example: Using Chart Events
This example illustrates using charting events to change the color pie piece when hovered over, and rotate the piece 360 degrees when clicked:
// Require the basic 2d chart resource: Chart // Retrieve the Tooltip class // Require the theme of our choosing require(["dojox/charting/Chart", "dojox/charting/action2d/Tooltip", "dojox/charting/themes/Claro", "dojox/charting/plot2d/Pie", "dojox/charting/axis2d/Default", "dojo/domReady!"], function(Chart, Tooltip, Claro) { // Define the data var chartData = [10000,9200,11811,12000,7662,13887,14200,12222,12000,10009,11288,12099]; // Create the chart within it's "holding" node var chart = new Chart("chartNode"); // Set the theme chart.setTheme(Claro); // Add the only/default plot chart.addPlot("default", { type: "Pie", markers: true }); // Add axes chart.addAxis("x"); chart.addAxis("y", { min: 5000, max: 30000, vertical: true, fixLower: "major", fixUpper: "major" }); // Add the series of data chart.addSeries("Monthly Sales - 2010", chartData); // Create the tooltip var tip = new Tooltip(chart, "default"); // Render the chart! chart.render(); // Add a mouseover event to the plot chart.connectToPlot("default",function(evt) { // Output some debug information to the console console.warn(evt.type," on element ",evt.element," with shape ",evt.shape); // Get access to the shape and type var shape = evt.shape, type = evt.type; // React to click event if(type == "onclick") { // Update its fill var rotateFx = new gfxFx.animateTransform({ duration: 1200, shape: shape, transform: [ { name: "rotategAt", start: [0,240,240], end: [360,240,240] } ] }).play(); } // If it's a mouseover event else if(type == "onmouseover") { // Store the original color if(!shape.originalFill) { shape.originalFill = shape.fillStyle; } // Set the fill color to pink shape.setFill("pink"); } // If it's a mouseout event else if(type == "onmouseout") { // Set the fill the original fill shape.setFill(shape.originalFill); } }); });View Demo
It's important to realize that every element within a chart is just a GFX graphic, so elements of the chart may be treated and animated as such, allowing for us to create some unique effects.
Conclusion
Creating basic Dojo charts isn't always enough. Dynamic data calls for dynamic charts, and the Dojo Toolkit's dojox/charting
library provides all the tools to make your charts as flexible as your data.
dojox/charting Resources
Looking for more detail about Dojo's charting library? Check out these great resources: