/////////////////////////////////////////////////////////////////////////// // // // NOTICE OF COPYRIGHT // // // // Moodle - Modular Object-Oriented Dynamic Learning Environment // // http://moodle.org // // // // Copyright (C) 1999 onwards Martin Dougiamas http://moodle.com // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU General Public License as published by // // the Free Software Foundation; either version 2 of the License, or // // (at your option) any later version. // // // // This program is distributed in the hope that it will be useful, // // but WITHOUT ANY WARRANTY; without even the implied warranty of // // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // // GNU General Public License for more details: // // // // http://www.gnu.org/copyleft/gpl.html // // // /////////////////////////////////////////////////////////////////////////// /** * This is the flex with flare based visualizer for the Moodle 2.x visual * grade book plug-in. This should load the grade book data for a given * visualization from the report/visual plug-in based on a set of flashvars * passed to it from Moodle and display a visual repersenation. */ package { //Flare imports import flare.animate.Transitioner; import flare.data.DataSet; import flare.data.DataSource; import flare.display.TextSprite; import flare.vis.Visualization; import flare.vis.controls.HoverControl; import flare.vis.data.Data; import flare.vis.data.DataSprite; import flare.vis.legend.Legend; import flare.vis.legend.LegendItem; import flare.vis.operator.encoder.ColorEncoder; import flare.vis.operator.encoder.ShapeEncoder; import flare.vis.operator.layout.AxisLayout; import flare.vis.scale.ScaleType; import flare.vis.util.Filters; import flash.display.DisplayObject; import flash.display.DisplayObjectContainer; import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import flash.filters.GlowFilter; import flash.geom.Rectangle; import flash.net.URLLoader; import flash.text.TextFormat; [SWF(width="800", height="600", backgroundColor="#ffffff", frameRate="30")] /** * Main class for handling grade book data and greatating a visualization. */ public class flare_visualization extends Sprite { /** * The visualization object to be used in creating the visualization. */ private var vis:Visualization; /** * A refernce to the currently displayed dialog box. If null no dialog * box is currently being displayed. */ private var lastBox:Sprite = null; /** * A refernce to the data sprite witch contains the data for witch the * currently displayed dialog box is based on and a child of. */ private var lastBoxData:DataSprite = null; /** * The data feild the X axis is based on. * @default data.student */ private var axisX:String = "data.student"; /** * The data field the Y axis is based on. * @default data.grade */ private var axisY:String = "data.grade"; /** * A container for the legends witch will be displayed on the righ hand * side. */ private var legends:Sprite; /** * The hover control for the dialog box. */ private var boxhc:HoverControl = new HoverControl(); /** * The constucter for the flare_visualization class. * Calls on harvest_data and sets up the varibles from the flashvars. */ public function flare_visualization() { // Call harvest_data, loading needed visualization data from moodle. // The Moodle wwwroot, course id, users sessionid, users session cookie // and session test data are needed to get the data from moodle are // loaded threw flashvars. harvest_data(loaderInfo.parameters['wwwroot'] + '/grade/report/visual/data.php?id=' + escape(loaderInfo.parameters['courseid']) + '&sessioncookie=' + escape(loaderInfo.parameters['sessioncookie']) + '&sessionid=' + escape(loaderInfo.parameters['sessionid']) + '&sessiontest=' + escape(loaderInfo.parameters['sessiontest'])); //harvest_data('http://localhost/moodle/grade/report/visual/data.php?id=3&sessioncookie=&sessionid=ebb79f5984c0fa4c9b0d85814c573a81&sessiontest=jHKFJhsOPf'); } /** * Harvests the data from Moodle and calls on buildVis to build the * visualization once the data has been loaded. * TODO: Add a loading bar and more feed back about the loading process. * @param url The url from witch to load the tab formated data for the visualization. */ public function harvest_data(url:String):void { var ds:DataSource = new DataSource(url, "tab"); var loader:URLLoader = ds.load(); loader.addEventListener(Event.COMPLETE, function(evt:Event):void { var ds:DataSet = loader.data as DataSet; buildVis(Data.fromDataSet(ds)); }); } /** * Find the max width between a container and all of it's decendence * This dose not find the width of a container but the greatest width * of an invdual component in it's decenedences. * @param d The display container to find the max width of. * @return the max width value of the display objects. */ private function getMaxWidth(d:DisplayObjectContainer):int { var max:int = d.width; for(var k:uint = 0; k < d.numChildren; k++ ) { var width:int = 0; if(d.getChildAt(k) is DisplayObjectContainer) { width = getMaxWidth(DisplayObjectContainer(d.getChildAt(k))); } else { width = d.getChildAt(k).width; } if(width > max) { max = width; } } return max; } /** * Simple function to retrun the greatest of two ints. * @param num1 the first number to test * @param num2 the second number to test * @return the largest value between num1 and num2. */ private function max(num1:int, num2:int):int { if(num1 > num2) { return num1; } else { return num2; } } /** * Find the max height between a container and all of it's decendence * This dose not find the width of a container but the greatest height * of an invdual component in it's decenedences. * @param d The display container to find the max height of. * @return the max height value of the display objects. */ private function getMaxHeight(d:DisplayObjectContainer):int { var max:int = d.height; for(var k:uint = 0; k < d.numChildren; k++ ) { var height:int = 0; if(d.getChildAt(k) is DisplayObjectContainer) { height = getMaxHeight(DisplayObjectContainer(d.getChildAt(k))); } else { height = d.getChildAt(k).height; } if(height > max) { max = height; } } return max; } /** * Builds the visualization based on the loaded data. * Also sets up the legends, buttons and controls. * @param data The data that was loaded in from moodle. */ private function buildVis(data:Data):void { vis = new Visualization(data); legends = new Sprite(); // Set the functions to be called when a dialog box is hovered over. boxhc.onRollOver = boxRollOver; boxhc.onRollOut = boxRollOut; // Set up the encoders and layout var colorEncoder:ColorEncoder = new ColorEncoder("data.item", Data.NODES, "lineColor", ScaleType.CATEGORIES); var shapeEncoder:ShapeEncoder = new ShapeEncoder("data.group"); var al:AxisLayout = new AxisLayout(axisX, axisY); // Add the encoders and layout to the visualization. vis.operators.add(al); vis.operators.add(colorEncoder); vis.operators.add(shapeEncoder); // Set up the layout of the axes. vis.xyAxes.xAxis.horizontalAnchor = TextSprite.LEFT; vis.xyAxes.xAxis.verticalAnchor = TextSprite.MIDDLE; vis.xyAxes.xAxis.labelAngle = Math.PI / 2; vis.xyAxes.xAxis.fixLabelOverlap = false; vis.xyAxes.yAxis.fixLabelOverlap = false; // Update the visualization so the widths and other values are correct. vis.update(); // Initalize the X and Y axis labels and the visualizations title. var labelX:TextSprite = new TextSprite("Student", new TextFormat("mono", 20)); var labelY:TextSprite = new TextSprite("Grade", new TextFormat("mono", 20)); var title:TextSprite = new TextSprite("Normalized Grades vs Students", new TextFormat("mono", 25)); // Find the largest width out of the X axis labels so it can used for positing sprites. var xLabelsHeight:int = getMaxHeight(vis.xyAxes.xAxis.labels); var yLabelsWidth:int = getMaxWidth(vis.xyAxes.yAxis.labels); // Position the visualization. vis.y = title.height + 10; vis.x = labelY.height + -vis.xyAxes.yAxis.labelOffsetX + yLabelsWidth; // Set up the legends. var itemLegend:Legend = new Legend("data.item", colorEncoder.scale, colorEncoder.colors, null, null); var groupLegend:Legend = new Legend("data.group", shapeEncoder.scale, null, shapeEncoder.shapes, null); // Set the bounds of the visualization based on the hieght and width of the flash application, // and the other components so the visualization is takes up the unused space. vis.bounds = new Rectangle(0, 0, loaderInfo.width - (max(itemLegend.width, groupLegend.width) + 15 + vis.x), loaderInfo.height - (vis.y + xLabelsHeight + labelX.height + vis.xyAxes.xAxis.labelOffsetY)); // Add the visualization to the main sprite. addChild(vis); // Set up the properitys of the data sprites and add a eventlistener to check for // clicks on them. vis.data.visit(function(d:DataSprite):void { d.fillColor = 0x018888ff; d.fillAlpha = 0.2; d.lineWidth = 2; d.addEventListener(MouseEvent.CLICK, mouseClicked); }); // Position the legends. legends.x = vis.bounds.width + 10; itemLegend.x = 0; itemLegend.y = 0; groupLegend.x = 0; groupLegend.y = itemLegend.y + itemLegend.height; // Add a listener for mouse clicks on the legends. itemLegend.items.addEventListener(MouseEvent.CLICK, legendClick); groupLegend.items.addEventListener(MouseEvent.CLICK, legendClick); // Add the legends to the legends container. legends.addChild(itemLegend); legends.addChild(groupLegend); // Set up and add a hover control to each legend. var itemHC:HoverControl = new HoverControl(itemLegend.items); itemHC.onRollOver = legendRollOver; itemHC.onRollOut = legendRollOut; var groupHC:HoverControl = new HoverControl(groupLegend.items); groupHC.onRollOver = legendRollOver; groupHC.onRollOut = legendRollOut; // Position and add the labels and title to the axes. labelX.x = vis.bounds.width/2 - labelX.width/2; labelX.y = vis.bounds.height + vis.xyAxes.xAxis.labelOffsetY + xLabelsHeight; vis.xyAxes.xAxis.addChild(labelX); labelY.x = -vis.x; labelY.y = vis.bounds.height/2 - labelY.width/2; labelY.rotation = -90; vis.xyAxes.yAxis.addChild(labelY); title.x = vis.bounds.width/2 - title.width/2; title.y = -vis.y; vis.xyAxes.addChild(title); // Add the legeneds container to the visualization. vis.addChild(legends); // Set up the hovercontrol for the marks on the chart var hc:HoverControl = new HoverControl(vis, Filters.isDataSprite); hc.onRollOver = rollOver; hc.onRollOut = rollOut; // Set up the buttons and a container for them. var controls:Sprite = new Sprite(); var bInvert:Button = new Button("Invert Axes"); var bHideAxis:Button = new Button("Hide Axes"); var bHideXLabel:Button = new Button("Hide X Labels"); var bHideYLabel:Button = new Button("Hide Y Labels"); var hideXLabelTransitioner:Transitioner = new Transitioner(2); bHideXLabel.addEventListener(MouseEvent.CLICK, function(evt:MouseEvent):void { if(!hideXLabelTransitioner.running) { hideXLabelTransitioner.reset(); if(bHideXLabel.text == "Show X Labels") { bHideXLabel.text = "Hide X Labels"; vis.xyAxes.xAxis.showLabels = true; vis.bounds = new Rectangle(0, 0, loaderInfo.width - (legends.width + 15 + vis.x), loaderInfo.height - (vis.y + xLabelsHeight + labelX.height + vis.xyAxes.xAxis.labelOffsetY)); } else { bHideXLabel.text = "Show X Labels"; vis.xyAxes.xAxis.showLabels = false; vis.bounds = new Rectangle(0, 0, loaderInfo.width - (legends.width + 15 + vis.x), loaderInfo.height - (vis.y + labelX.height)); } hideXLabelTransitioner.$(labelY).x = -vis.x; hideXLabelTransitioner.$(labelY).y = vis.bounds.height/2 - labelY.height/2; vis.update(hideXLabelTransitioner).play(); } }); var hideYLabelTransitioner:Transitioner = new Transitioner(2); bHideYLabel.addEventListener(MouseEvent.CLICK, function(evt:MouseEvent):void { if(!hideYLabelTransitioner.running) { var t:Transitioner = new Transitioner(2); var newX:int; hideYLabelTransitioner.reset(); if(bHideYLabel.text == "Show Y Labels") { bHideYLabel.text = "Hide Y Labels"; vis.xyAxes.yAxis.showLabels = true; newX = labelY.width + -vis.xyAxes.yAxis.labelOffsetX + yLabelsWidth; } else { bHideYLabel.text = "Show Y Labels"; vis.xyAxes.yAxis.showLabels = false; newX = labelY.width; } t.$(vis).x = newX; vis.bounds = new Rectangle(0, 0, loaderInfo.width - (legends.width + 15 + newX), loaderInfo.height - (vis.y + xLabelsHeight + labelX.height + vis.xyAxes.xAxis.labelOffsetY)); // Reposition the labels and title. t.$(title).x = vis.bounds.width/2 - title.width/2; t.$(labelX).x = vis.bounds.width/2 - labelX.width/2; t.$(labelX).y = vis.bounds.height + vis.xyAxes.xAxis.labelOffsetY + xLabelsHeight; t.$(labelY).x = -newX; t.$(labelY).y = vis.bounds.height/2 - labelY.height/2; // Keep the legends in there place. t.$(legends).x = vis.bounds.width + 10; t.play(); vis.update(hideYLabelTransitioner).play(); } }); // Set up the transitioner to be used when inverting the axes var updateTransitioner:Transitioner = new Transitioner(2); updateTransitioner.onEnd = function():void { updateMarkVisiblity(); vis.xyAxes.xAxis.labels.visible = true; vis.xyAxes.yAxis.labels.visible = true; }; updateTransitioner.onStart = function():void { updateMarkVisiblity(); vis.xyAxes.xAxis.labels.visible = false; vis.xyAxes.yAxis.labels.visible = false; } // The function to invert the axes. bInvert.addEventListener(MouseEvent.CLICK, function(evt:MouseEvent):void { // If we are not allready in the process of inverting the axes. if(!updateTransitioner.running) { var t:Transitioner = new Transitioner(2); var tempText:String = labelX.text; var tempOffset:int = vis.xyAxes.xAxis.labelOffsetX; var tempWidth:uint = vis.bounds.width; var tempLabels:int = xLabelsHeight; var currentXLabelsHeight:int = getMaxWidth(vis.xyAxes.yAxis.labels); var tempShowLabels:Boolean = vis.xyAxes.xAxis.showLabels; // Rest the transitioner for a clean transition. updateTransitioner.reset(); // Flip the axis feilds. al.xField = axisY; al.yField = axisX; axisX = al.xField; axisY = al.yField; xLabelsHeight = yLabelsWidth; yLabelsWidth = tempLabels; vis.xyAxes.xAxis.showLabels = vis.xyAxes.yAxis.showLabels; vis.xyAxes.yAxis.showLabels = tempShowLabels; if(vis.xyAxes.yAxis.showLabels) { bHideYLabel.text = "Hide Y Labels"; } else { bHideYLabel.text = "Show Y Labels"; } if(vis.xyAxes.xAxis.showLabels) { bHideXLabel.text = "Hide X Labels"; } else { bHideXLabel.text = "Show X Labels"; } // Flip the labels labelX.text = labelY.text; labelY.text = tempText; // Find the new X value for the visualization. var newX:int = labelY.width + vis.xyAxes.xAxis.labelOffsetY + getMaxHeight(vis.xyAxes.xAxis.labels); // Reposition and set the bounds of the visualization. t.$(vis).x = newX; vis.bounds = new Rectangle(0, 0, loaderInfo.width - (legends.width + 15 + newX), loaderInfo.height - (vis.y + currentXLabelsHeight + labelX.height + vis.xyAxes.xAxis.labelOffsetY)); // Reposition the labels and title. t.$(title).x = vis.bounds.width/2 - title.width/2; t.$(labelX).x = vis.bounds.width/2 - labelX.width/2; t.$(labelX).y = vis.bounds.height + vis.xyAxes.xAxis.labelOffsetY + currentXLabelsHeight; t.$(labelY).x = -newX; t.$(labelY).y = vis.bounds.height/2 - labelY.width/2; // Keep the legends in there place. t.$(legends).x = vis.bounds.width + 10; //Play the transition. t.play(); vis.update(updateTransitioner).play(); } }); // Set up the transitioner for the hide axes button. var hideAxisTrans:Transitioner = new Transitioner(1); // Function for hidding the axes. bHideAxis.addEventListener(MouseEvent.CLICK, function(evt:MouseEvent):void { // If we are not allready in the process of hidding the axes if(!hideAxisTrans.running) { // Reset the transitoner for a clean transiton. hideAxisTrans.reset(); // Hide or show the axes. if(bHideAxis.text == "Show Axes") { hideAxisTrans.$(bHideAxis).text = "Hide Axes"; al.showAxes(hideAxisTrans).play(); } else { hideAxisTrans.$(bHideAxis).text = "Show Axes"; al.hideAxes(hideAxisTrans).play(); } } }); // Position the buttons inside there container. bHideXLabel.x = 0; bHideXLabel.y = 0; bHideAxis.x = legends.width - bHideAxis.width - 5; bHideAxis.y = bHideXLabel.y; bInvert.x = legends.width - bInvert.width - 5; bInvert.y = bHideXLabel.y + bHideXLabel.height + 2; bHideYLabel.x = 0; bHideYLabel.y = bHideXLabel.y + bHideXLabel.height + 2; // Poistion the buttons container. controls.x = legends.x + vis.x; controls.y = legends.y + legends.height + vis.y + 20; // Add the buttons to the container and the container to the main sprite. controls.addChild(bInvert); controls.addChild(bHideAxis); controls.addChild(bHideXLabel); controls.addChild(bHideYLabel); addChild(controls); // Set the marks on the chart to the higest deepth. vis.setChildIndex(vis.marks, vis.numChildren - 1); // Update. vis.update(); updateMarkVisiblity(); } /** * Roll over function witch makes the object 0.5 units bigger and adds a glow filter. * @param ob the object witch was rolled over. */ private function rollOver(ob:Object):void { ob.filters = [new GlowFilter(0xFFFF55, 0.8, 6, 6, 10)]; ob.size += 0.5; } /** * Roll out function witch removes the filters and makes the object 0.5 units smaller. * @param ob the object witch was rolled out of. */ private function rollOut(ob:Object):void { ob.filters = null; ob.size -= 0.5; } /** * Roll over function for the dialog box. * Adds a glow filter to the curently active dialog box. * @param ob a child of the dialog box. */ private function boxRollOver(ob:Object):void { if(lastBoxData != null) { lastBoxData.filters = [new GlowFilter(0xFFFF55, 0.8, 6, 6, 10)]; } } /** * Roll out function for the dialog box. * Removes filters on the curently active dialog box. * @param ob a child of the dialog box. */ private function boxRollOut(ob:Object):void { if(lastBoxData != null) { lastBoxData.filters = null; } } /** * Finds the Legend belonging to the LegendItem passed. * TODO: See if this can be replaced by a .parent call. * @param item a LegendItem to find the Legend of. * @return the Legend that contains the passed LegendItem. */ private function findLegendByItem(item:LegendItem):Legend { for(var i:uint = 0; i < legends.numChildren; i++ ) { if(Legend(legends.getChildAt(i)).items.contains(item)) { return Legend(legends.getChildAt(i)); } } return null; } /** * Roll over function for legends. * Adds a glow filter to the legend's item aswell as all the markers on the chart * that are realted to the legend item and incrases there size by 1 unit. * @param ob the LegendItem being rolled over. */ private function legendRollOver(ob:LegendItem):void { var legend:Legend = findLegendByItem(ob); var dataName:String = legend.dataField.substr(legend.dataField.lastIndexOf('.') + 1); ob.filters = [new GlowFilter(0xFFFF55, 0.8, 6, 6, 10)]; vis.data.visit(function(d:DataSprite):void { if(d.data.hasOwnProperty(dataName) && ob.value == d.data[dataName]) { d.filters = [new GlowFilter(0xFFFF55, 0.8, 6, 6, 10)]; d.size += 1; } }, 3, Filters.isDataSprite); } /** * Roll out function for legends. * Removes filters to the legend's item aswell as all the markers on the chart * that are realted to the legend item and decrases there size by 1 unit. * @param ob the LegendItem being rolled out of. */ private function legendRollOut(ob:LegendItem):void { var legend:Legend = findLegendByItem(ob); var dataName:String = legend.dataField.substr(legend.dataField.lastIndexOf('.') + 1); ob.filters = null; vis.data.visit(function(d:DataSprite):void { if(d.data.hasOwnProperty(dataName) && ob.value == d.data[dataName]) { d.filters = null; d.size -= 1; } }, 3, Filters.isDataSprite); } /** * Creates and returns a dialog box containing information on the passed data sprite. * @param data the DataSprite containing the information to display. * @returns the Sprite containing the dialog box. */ private function dataDialogBox(data:DataSprite):Sprite { var box:Sprite = new Sprite; var backGround:Sprite = new Sprite; backGround.graphics.beginFill(0x7777ff, 0.60); backGround.graphics.lineStyle(3, 0x0000ff, 0.3); var text:Sprite = new Sprite; var x:int = 5; var y:int = 0; for(var property:Object in data.data) { var temp:TextSprite = new TextSprite(property.toString() + ": " + data.data[property], new TextFormat("mono", 12, null, true)); temp.x = x; temp.y = y; text.addChild(temp); y += temp.height; } /*var name:TextSprite = new TextSprite(data.data.student, new TextFormat("mono", 20, null, true)); var item:TextSprite = new TextSprite("Item: " + data.data.item, new TextFormat("mono", 12, null, true)); var grade:TextSprite = new TextSprite("Grade: " + data.data.grade + "%", new TextFormat("mono", 12, null, true)); var group:TextSprite = new TextSprite("Group: " + data.data.group, new TextFormat("mono", 12, null, true)); text.addChild(name); text.addChild(item); text.addChild(group); text.addChild(grade); name.x = text.width/2 - name.width/2; item.x = 5; grade.x = 5; group.x = 5; name.y = 0; item.y = name.y + name.height; group.y = item.y + item.height; grade.y = group.y + group.height;*/ backGround.graphics.drawRoundRect(0, 0, text.width + 10, text.height, 30, 30); box.addChild(backGround); box.addChild(text); return box; } /** * Check if a mark on the chart is visible based on the related LegendItems states. * @param d the DataSprite to check the visiblility of. * @returns true if the mark is visible. */ private function markIsVisible(d:DataSprite):Boolean { var items:Array = getLegendItems(d); for each(var item:LegendItem in items) { if(item.alpha != 1) { return false; } } return true; } /** * Gets all LegenedItems realted to a given DataSprite/mark. * @params d the DataSprite on the chart. * @returns Array of LegendItems that are realted to the given DataSprite. */ private function getLegendItems(d:DataSprite):Array { var items:Array = new Array(); for(var i:uint = 0; i < legends.numChildren; i++) { var legend:Legend = Legend(legends.getChildAt(i)); for(var k:uint = 0; k < legend.items.numChildren; k++) { var item:LegendItem = LegendItem(legend.items.getChildAt(k)); if(d.data[legend.dataField.substr(legend.dataField.lastIndexOf('.') + 1)] == item.value) { items.push(item); break; } } } return items; } /** * Sets the visible atrubute of the marks/DataSprites based on the status of the LegendItems realted to it. * @param item if set, only updates marks for that LegendItem. Otherwise updates all marks. */ private function updateMarkVisiblity(item:LegendItem=null):void { var legend:Legend; var dataName:String; if(item != null) { legend = findLegendByItem(item); dataName = legend.dataField.substr(legend.dataField.lastIndexOf('.') + 1); } vis.data.visit(function(d:DataSprite):void { if(item == null || (d.data.hasOwnProperty(dataName) && item.value == d.data[dataName])) { if(markIsVisible(d)) { //d.alpha = 1.0; d.visible = true; } else { //d.alpha = 0.0; d.visible = false; } } }, 3, Filters.isDataSprite); } /** * Function to be called when a LegendItem is clicked. * Changes the legendItems alpah value and updates mark visiblity. * @param evt the mouse event. */ private function legendClick(evt:MouseEvent):void { var item:LegendItem = LegendItem(evt.target); if(item.alpha == 1) { item.alpha = 0.4; } else { item.alpha = 1.0; } updateMarkVisiblity(item); } /** * Function called when a click happens on a mark on the chart. * Creates and adds a dialog box for that mark/DataSprite when clicked or removes the dialog box if * the mark allready has one. * @param the mouse event. */ private function mouseClicked(evt:MouseEvent):void { if(DisplayObject(evt.target).parent == vis.marks) { if(lastBox != null && lastBoxData != null) { lastBoxData.removeChild(lastBox); boxhc.detach(); } if(evt.target != lastBoxData) { lastBox = dataDialogBox(DataSprite(evt.target)); lastBoxData = DataSprite(evt.target); Sprite(evt.target).addChild(lastBox); vis.marks.setChildIndex(Sprite(evt.target), vis.marks.numChildren - 1); boxhc.attach(Sprite(evt.target)); } else { lastBoxData = null; lastBox = null; } } } } }