├── dashboard.js ├── README.md ├── bandline.css ├── index.html ├── LICENSE ├── interactions.js ├── dashboard.css ├── du.js ├── bandline.js ├── data.js ├── model.js └── render.js /dashboard.js: -------------------------------------------------------------------------------- 1 | var root = d3.selectAll('svg') 2 | 3 | function stopScrolling(touchEvent) {touchEvent.preventDefault();} 4 | document.addEventListener( 'touchstart' , stopScrolling , false ) 5 | document.addEventListener( 'touchmove' , stopScrolling , false ) 6 | 7 | window.setTimeout(render, 0) 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sf-student-dashboard 2 | Implementation of Stephen Few's Student Dashboard with Bandlines in d3.js 3 | 4 | See it in action: http://bl.ocks.org/monfera/raw/0b4b038fd4dd3a046168/ 5 | 6 | Article: http://www.perceptualedge.com/blog/?p=2138 7 | 8 | Introducing Bandlines by Stephen Few: https://www.perceptualedge.com/articles/visual_business_intelligence/introducing_bandlines.pdf 9 | 10 | 11 | License: https://opensource.org/licenses/BSD-3-Clause 12 | 13 | Dashboard design: © Stephen Few 14 | 15 | Code: © Robert Monfera 16 | -------------------------------------------------------------------------------- /bandline.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Bandline styling 3 | * 4 | * Implementation of Stephen Few's bandlines 5 | * 6 | * Copyright Robert Monfera 7 | * Design: Stephen Few 8 | * Design documentation: https://www.perceptualedge.com/articles/visual_business_intelligence/introducing_bandlines.pdf 9 | */ 10 | 11 | g.bandLine .band, 12 | g.sparkStrip .band { 13 | fill: black 14 | } 15 | 16 | g.bands .band.s0 {fill-opacity: 0.06} 17 | g.bands .band.s1 {fill-opacity: 0.1} 18 | g.bands .band.s2 {fill-opacity: 0.16} 19 | g.bands .band.s3 {fill-opacity: 0.22} 20 | g.bands .band.s4 {fill-opacity: 0.3} 21 | g.bands .band.s5 {stroke: white; stroke-width: 1px; fill: none;} /* for more prominent color: hsl(120, 100%, 38%) */ 22 | 23 | g.bandLine .valueLine, 24 | g.sparkStrip .valueBox, 25 | g.sparkStrip .valuePoints { 26 | stroke: rgb(226, 60, 180) 27 | } 28 | 29 | g.bandLine .valueLine { 30 | stroke-width: 2; 31 | } 32 | g.sparkStrip .valueBox, 33 | g.sparkStrip .valuePoints { 34 | stroke-width: 1; 35 | } 36 | 37 | g.sparkStrip .valueBox { 38 | fill: white; 39 | } 40 | 41 | g.sparkStrip .valuePoints { 42 | fill: none; 43 | } 44 | 45 | g.bandLine .valuePoints { 46 | fill: rgb(226, 60, 180); 47 | } 48 | 49 | g.bandLine .valuePoints .point.highOutlier { 50 | fill: white; 51 | stroke: rgb(226, 60, 180); 52 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Student Overview Dashboard 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Robert Monfera. 2 | Dashboard visual design Copyright (c) 2013, Stephen Few. 3 | 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of sf-student-dashboard nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | -------------------------------------------------------------------------------- /interactions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dashboard interactions 3 | * 4 | * Copyright Robert Monfera 5 | */ 6 | 7 | function setTableSortOrder(sortVariable) { 8 | //if(d3.event) d3.event.stopPropagation() 9 | var sortSettings = dashboardSettings.table.sort 10 | if(sortSettings.lastTimedSort) { 11 | window.clearTimeout(sortSettings.lastTimedSort) 12 | sortSettings.lastTimedSort = null 13 | } 14 | if(sortVariable && sortVariable !== sortSettings.sortVariable) { // fixme this doesn't detect identical sortings based on template based variables 15 | sortSettings.sortVariable = sortVariable 16 | render() 17 | sortSettings.lastSortTime = new Date() 18 | } 19 | } 20 | 21 | function setDelayedTableSortOrder(variable, d) { 22 | d.delayed = window.setTimeout(function() { 23 | setTableSortOrder(variable) 24 | }, 300) 25 | } 26 | 27 | function setGroupHeaderTableSortOrder(className, d) { 28 | var variable = findWhere('groupAlias', className)(dashboardSettings.variables) 29 | setDelayedTableSortOrder(variable, d) 30 | } 31 | 32 | function setPetiteHeaderTableSortOrder(headerName, d) { 33 | var variable = findWhere('petiteHeaderAlias', headerName)(dashboardSettings.variables) 34 | setDelayedTableSortOrder(variable, d) 35 | } 36 | 37 | function resetTableSortOrder(d) { 38 | setTableSortOrder(defaultSortVariable) 39 | if(d.delayed) { 40 | window.clearTimeout(d.delayed) 41 | delete d.delayed 42 | } 43 | } 44 | 45 | function setGroupLegendTableSortOrder(legendName) { 46 | var variable = findWhere('legendAlias', legendName)(dashboardSettings.variables) 47 | setTableSortOrder(variable) 48 | } 49 | 50 | function rowMouseDown(d) { 51 | var e = d3.event 52 | var studentSelection = dashboardSettings.table.studentSelection 53 | if(!studentSelection.brushable) { 54 | studentSelection.brushable = true 55 | } 56 | var cond = studentSelection.selectedStudents[d.key] 57 | studentSelection.currentSelectedStudents = {} 58 | if(cond) { 59 | studentSelection.selectedStudents = {} 60 | cancelSelection() 61 | } else { 62 | studentSelection.currentSelectedStudents.from = d.key 63 | studentSelection.currentSelectedStudents.to = d.key 64 | studentSelection.brushInProgress = true 65 | if(!(e.metaKey || e.ctrlKey) ) { 66 | studentSelection.selectedStudents = {} 67 | } 68 | studentSelection.selectedStudents[d.key] = true 69 | 70 | render() 71 | } 72 | } 73 | 74 | function rowMouseOver(d) { 75 | var studentSelection = dashboardSettings.table.studentSelection 76 | var selectedStudents = studentSelection.selectedStudents 77 | var currentSelectedStudents = studentSelection.currentSelectedStudents 78 | if(!studentSelection.brushable) { 79 | return 80 | } 81 | 82 | if(!studentSelection.brushInProgress) { 83 | if(Object.keys(selectedStudents).length > 0) { 84 | return; 85 | } 86 | currentSelectedStudents.from = d.key 87 | currentSelectedStudents.to = d.key 88 | } 89 | currentSelectedStudents.to = d.key 90 | var names = dashboardData['Student Data'].map(key) 91 | var indices = [studentSelection.currentSelectedStudents.from, studentSelection.currentSelectedStudents.to].map(function(name) {return names.indexOf(name)}) 92 | var extent = d3.extent(indices) 93 | Object.keys(selectedStudents).forEach(function(key) {if(selectedStudents[key] === 'maybe') delete selectedStudents[key]}) 94 | for(var i = extent[0]; i <= extent[1]; i++) {studentSelection.selectedStudents[names[i]] = 'maybe'} 95 | render() 96 | } 97 | 98 | function rowMouseUp() { 99 | var studentSelection = dashboardSettings.table.studentSelection 100 | var selectedStudents = studentSelection.selectedStudents 101 | Object.keys(selectedStudents).forEach(function(key) {selectedStudents[key] = true}) 102 | if(!studentSelection.brushable) { 103 | return 104 | } 105 | if(studentSelection.brushInProgress) { 106 | studentSelection.brushInProgress = false 107 | } 108 | } 109 | 110 | function cancelSelection() { 111 | var studentSelection = dashboardSettings.table.studentSelection 112 | 113 | d3.event.preventDefault(); d3.event.stopPropagation(); 114 | studentSelection.selectedStudents = {} 115 | studentSelection.brushInProgress = false 116 | studentSelection.brushable = false 117 | render() 118 | } 119 | 120 | var rowInteractions = { 121 | mousedown: rowMouseDown, 122 | mouseover: rowMouseOver, 123 | mouseup: rowMouseUp 124 | } -------------------------------------------------------------------------------- /dashboard.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | * { 4 | -webkit-user-select: none; 5 | -moz-user-select: none; 6 | -ms-user-select: none; 7 | user-select: none; 8 | } 9 | 10 | body, html { 11 | width: 100%; 12 | height: 100%; 13 | } 14 | 15 | svg { 16 | shape-rendering: geometricPrecision; 17 | } 18 | 19 | html { 20 | overflow: hidden 21 | } 22 | 23 | body { 24 | margin: 0px; 25 | } 26 | 27 | text { 28 | cursor: default; 29 | } 30 | 31 | line.point-marker-v, line.point-marker-h { 32 | stroke: red; 33 | } 34 | 35 | path { 36 | fill: none; 37 | } 38 | 39 | 40 | /* GROUPS */ 41 | 42 | /* Top groups */ 43 | 44 | g.lastAssignmentScoresGroup { 45 | text-anchor: end; 46 | } 47 | 48 | /* Rows */ 49 | 50 | rect.rowBackground { 51 | stroke: grey; 52 | stroke-width: 2px; 53 | stroke-opacity: 0.03; 54 | } 55 | 56 | rect.rowCaptureZone { 57 | stroke: none; 58 | fill: white; 59 | fill-opacity: 0.001; 60 | } 61 | 62 | /* Interactions */ 63 | 64 | .row *, .headerTitle, .headerText.interactive { 65 | cursor: pointer; 66 | } 67 | 68 | .helpButton * { 69 | cursor: help; 70 | } 71 | 72 | /** 73 | * Cells 74 | */ 75 | 76 | /* Flags */ 77 | g.flagGroup circle.nonNativeEnglish, 78 | g.namesGroup g.groupLegends g.english circle { 79 | fill: rgb(252, 177, 86); 80 | } 81 | g.flagGroup circle.special, 82 | g.namesGroup g.groupLegends g.special circle { 83 | fill: rgb(237, 61, 44); 84 | } 85 | 86 | /* Course Grades */ 87 | 88 | g.courseGradesGroup g.groupLegends g.previous line { 89 | opacity: 0.1; 90 | stroke: black; 91 | } 92 | 93 | /* Class Attendance */ 94 | 95 | line.violationBaseline { 96 | opacity: 0.2; 97 | } 98 | line.attendanceLine, g.classAttendanceGroup g.groupLegends line { 99 | stroke: rgb(164, 55, 18); 100 | stroke-width: 11; 101 | } 102 | 103 | /** 104 | * Legends 105 | */ 106 | 107 | /* Legends in general */ 108 | 109 | g.groupLegends text { 110 | font-size: 10px; 111 | } 112 | 113 | g.groupLegends .legend1 { 114 | fill: gray; 115 | } 116 | g.groupLegends .legend2 { 117 | fill: rgb(226, 60, 180); /* Steve Magenta */ 118 | } 119 | 120 | g.groupLegends rect.legendCaptureZone { 121 | opacity: 0.0; 122 | stroke: black; 123 | } 124 | 125 | /* Small column headings */ 126 | 127 | g.smallColumnHeader text { 128 | font-size: 12px; 129 | text-anchor: end; 130 | } 131 | g.namesGroup g.smallColumnHeader text { 132 | text-anchor: start; 133 | } 134 | 135 | /* Course grade legends */ 136 | 137 | g.courseGradesGroup g.groupLegends g.current line { 138 | stroke-width: 1; 139 | stroke: black; 140 | } 141 | 142 | g.courseGradesGroup g.groupLegends g.target line { 143 | stroke: red; 144 | stroke-width: 1; 145 | -webkit-transform: rotate(90deg); 146 | transform: rotate(90deg); 147 | -webkit-transform-origin: center; 148 | transform-origin: center; 149 | } 150 | 151 | g.courseGradesGroup g.groupLegends g.previous line { 152 | stroke-width: 8; 153 | } 154 | 155 | /* Attendance legends */ 156 | 157 | g.classAttendanceGroup g.groupLegends line { 158 | stroke-width: 8; 159 | } 160 | 161 | g.classAttendanceGroup g.groupLegends g.tardy line { 162 | opacity: 0.1; 163 | } 164 | g.classAttendanceGroup g.groupLegends g.absent line { 165 | opacity: 0.4; 166 | } 167 | g.classAttendanceGroup g.groupLegends g.referrals line { 168 | opacity: 0.7; 169 | } 170 | g.classAttendanceGroup g.groupLegends g.detentions line { 171 | opacity: 1; 172 | } 173 | 174 | /* Score legends */ 175 | 176 | g.assignmentScoresGroup g.groupLegends g.above circle { 177 | fill: green; 178 | } 179 | g.assignmentScoresGroup g.groupLegends g.below circle { 180 | fill: red; 181 | } 182 | g.assignmentScoresGroup g.groupLegends g.current circle { 183 | fill: grey; 184 | } 185 | g.assignmentScoresGroup g.groupLegends g.past circle { 186 | fill: grey; 187 | opacity: 0.3; 188 | } 189 | 190 | /* Benchmark legends */ 191 | 192 | g.benchmarkGroup g.groupLegends line { 193 | stroke-width: 2px; 194 | -webkit-transform: scaleX(1.5) translateY(-1.5px); 195 | transform: scaleX(1.5) translateY(-1.5px); 196 | } 197 | 198 | /** 199 | * Background of charts 200 | */ 201 | 202 | rect.background { 203 | fill: black; 204 | } 205 | 206 | /** 207 | * Axes in general 208 | */ 209 | 210 | .tick text, .axisLegend, .legendText { 211 | fill: rgb(96, 96, 96); 212 | font-size: 10px; 213 | } 214 | 215 | .courseGradesGroup .tick text { 216 | font-size: 12px; 217 | } 218 | 219 | .tick line, path.domain { 220 | stroke: rgb(192, 192, 192); 221 | } 222 | 223 | text.axisLegend { 224 | } 225 | /** 226 | * Column axes 227 | */ 228 | 229 | g.columnAxis { 230 | font-size: 12px; 231 | } 232 | 233 | /** 234 | * Distributions - histogram 235 | */ 236 | 237 | g.distributionsGroup g.gradeHistogram g.xAxis .tick line { 238 | -webkit-transform: translateX(13px); 239 | transform: translateX(13px); 240 | } 241 | 242 | g.distributionsGroup g.gradeHistogram rect.barAes { 243 | fill: lightgray; 244 | } 245 | 246 | /** 247 | * Distributions - boxplot 248 | */ 249 | 250 | g.distributionsGroup g.assignmentDistributions g.yAxis .tick line { 251 | -webkit-transform: translateY(12px); 252 | transform: translateY(12px); 253 | } 254 | 255 | g.distributionsGroup g.assignmentDistributions circle.bee { 256 | /* fill: black; */ 257 | stroke: none; 258 | opacity: 0.05; 259 | } 260 | 261 | g.distributionsGroup g.assignmentDistributions line.beeLine { 262 | stroke-width: 1px; 263 | stroke: chocolate; 264 | opacity: 0.7; 265 | } 266 | 267 | g.distributionsGroup g.assignmentDistributions g.boxplot line.quintile2, 268 | g.distributionsGroup g.assignmentDistributions g.boxplot line.quintile4, 269 | g.distributionsGroup g.assignmentDistributions g.boxplot line.median { 270 | stroke-width: 2px; 271 | opacity: 0.67; 272 | } 273 | /** 274 | * Tser group 275 | */ 276 | 277 | g.tserGroup line.bar { 278 | stroke: black; 279 | stroke-width: 1.5px; 280 | } 281 | 282 | /** 283 | * Benchmark group 284 | */ 285 | 286 | g.benchmarkGroup g.chart g.chartLine path.line { 287 | stroke: inherit; 288 | fill: none; 289 | } 290 | g.benchmarkGroup g.chart g.chartLine circle.linePoint { 291 | fill: inherit; 292 | } 293 | 294 | g.benchmarkGroup g.xAxis .tick line { 295 | -webkit-transform: translateX(38px); 296 | transform: translateX(38px); 297 | } 298 | 299 | 300 | /** 301 | * Median group 302 | */ 303 | 304 | g.medianGroup g.tableColumn text { 305 | font-size: 14px; 306 | text-anchor: end; 307 | } -------------------------------------------------------------------------------- /du.js: -------------------------------------------------------------------------------- 1 | /** 2 | * D3 utilities 3 | * 4 | * Copyright Robert Monfera 5 | */ 6 | 7 | function tupleSorter(t1, t2) { 8 | var a = t1[0], b = t2[0] 9 | return a < b ? -1 : a > b ? 1 : 0 10 | } 11 | 12 | function last(a) { 13 | return a[a.length - 1] 14 | } 15 | 16 | function add(x, y) { 17 | return x + y 18 | } 19 | 20 | function constant(value) { 21 | return function() { 22 | return value 23 | } 24 | } 25 | 26 | function identity(x) { 27 | return x 28 | } 29 | 30 | function compose(fun1, fun2) { 31 | if(arguments.length === 2) { 32 | return function (/*args*/) { 33 | return fun1(fun2.apply(null, arguments)) 34 | } 35 | } else { 36 | var functions = Array.prototype.slice.call(arguments) 37 | var len = functions.length 38 | return function(/*args*/) { 39 | var value = (functions[len - 1]).apply(null, arguments) 40 | var i 41 | for(i = Math.max(0, len - 2); i >= 0; i--) { 42 | value = (functions[i]).call(null, value) 43 | } 44 | return value 45 | } 46 | } 47 | } 48 | 49 | function property(key) { 50 | return function(thing) { 51 | return thing[key] 52 | } 53 | } 54 | 55 | function pluck(key) { 56 | return function(array) { 57 | var i 58 | var result = [] 59 | for(i = 0; i < array.length; i++) { 60 | result.push(array[i][key]) 61 | } 62 | return result 63 | } 64 | } 65 | 66 | function object(keyValuePairs) { 67 | var result = {} 68 | var i 69 | for(i = 0; i < keyValuePairs.length; i++) { 70 | result[keyValuePairs[i][0]] = keyValuePairs[i][1] 71 | } 72 | return result 73 | } 74 | 75 | function pairs(object) { 76 | var keys = Object.keys(object) 77 | var result = [] 78 | var key 79 | var i 80 | for(i = 0; i < keys.length; i++) { 81 | key = keys[i] 82 | result.push([key, object[key]]) 83 | } 84 | return result 85 | } 86 | 87 | function findWhere(key, value) { 88 | // works for objects now... 89 | return function(obj) { 90 | var keys = Object.keys(obj) 91 | var i 92 | for(i = 0; i < keys.length; i++) { 93 | if(obj[[keys[i]]][key] === value) { 94 | return obj[keys[i]] 95 | } 96 | } 97 | return void(0) 98 | } 99 | } 100 | 101 | function sortBy(obj, comparisonAccessor) { 102 | // stable sort inspired by the underscore.js implementation 103 | return obj.map(function(element, index) { 104 | return { 105 | value: element, 106 | index: index, 107 | comparedValue: comparisonAccessor(element) 108 | } 109 | }).sort(function(left, right) { 110 | var a = left.comparedValue 111 | var b = right.comparedValue 112 | return a < b ? -1 : a > b ? 1 : left.index - right.index 113 | }).map(function(obj) {return obj.value}) 114 | }; 115 | 116 | function countBy(array, accessorFunction) { 117 | var accessor = accessorFunction || identity 118 | var result = {} 119 | var value 120 | var i 121 | for(i = 0; i < array.length; i++) { 122 | value = accessor(array[i]) 123 | if(result[value]) { 124 | result[value]++ 125 | } else { 126 | result[value] = 1 127 | } 128 | } 129 | return result 130 | } 131 | 132 | function always() { 133 | return true 134 | } 135 | 136 | function key(obj) { 137 | return obj.key 138 | } 139 | 140 | function value(obj) { 141 | return obj.value 142 | } 143 | 144 | function window2(a) { 145 | return a.map(function(value, index, array) { 146 | return [value, array[index + 1]] 147 | }).slice(0, a.length - 1) 148 | } 149 | 150 | function tuple(/*args*/) { 151 | var functions = Array.prototype.slice.call(arguments) 152 | return function (x) { 153 | return functions.map(function (elem, i, funs) { 154 | return funs[i](x) 155 | }) 156 | } 157 | } 158 | 159 | function clamp(base, array) { 160 | var min = base[0] 161 | var max = base[1] 162 | var result = array.map(function(d) {return Math.min(max, Math.max(min, d))}) 163 | return result 164 | } 165 | 166 | function bind0(rootSelection, cssClass, element, dataFlow) { 167 | cssClass = cssClass || 'boundingBox' 168 | element = element || 'g' 169 | dataFlow = typeof dataFlow === 'function' ? dataFlow : (dataFlow === void(0) ? du.repeat : constant(dataFlow)) 170 | var classesToClassAttr = function (classNames) { 171 | return classNames.join(' ') 172 | }, 173 | classesToCssSelector = function (classNames) { 174 | return (['']).concat(classNames).join(' .') 175 | }, 176 | cssClasses = classesToCssSelector([cssClass]), 177 | binding = rootSelection.selectAll(cssClasses).data(dataFlow, key) 178 | 179 | binding.entered = binding.enter().append(element) 180 | binding.entered.attr('class', classesToClassAttr([cssClass])) 181 | 182 | return binding 183 | } 184 | 185 | function bind(object, key) { 186 | var result = bind0.apply(null, arguments) 187 | object[key] = result 188 | return result 189 | } 190 | 191 | function bindd() { 192 | var result = bind.apply(null, arguments) 193 | du.pointMarker(result, [{key: 0}]) 194 | return result 195 | } 196 | 197 | function translate(funX, funY) { 198 | return function (d, i) { 199 | return 'translate(' + (typeof funX === 'function' ? funX(d, i) : funX) + ',' + (typeof funY === 'function' ? funY(d, i) : funY) + ')'; 200 | } 201 | } 202 | 203 | function translateX(funX) { 204 | return function (d, i) { 205 | return 'translate(' + (typeof funX === 'function' ? funX(d, i) : funX) + ', 0)' 206 | } 207 | } 208 | 209 | function translateY(funY) { 210 | return function (d, i) { 211 | return 'translate(0, ' + (typeof funY === 'function' ? funY(d, i) : funY) + ')' 212 | } 213 | } 214 | 215 | var du = { 216 | 217 | repeat: tuple(identity), 218 | descend: identity, 219 | bind: bind, 220 | pointMarker: function(selection, data) { 221 | selection.each(function(d) { 222 | var c = bind0(selection, 'point-marker-c', 'circle', data) 223 | c.entered.attr({r: '10px', fill: 'red', 'opacity': 0.2}) 224 | c.exit().remove() 225 | 226 | var h = bind0(selection, 'point-marker-h', 'line', data) 227 | h.entered.attr({x1: -10000, x2: 10000, 'stroke-dasharray': '2 4', stroke: 'red', 'opacity': 0.5}) 228 | h.exit().remove() 229 | 230 | var v = bind0(selection, 'point-marker-v', 'line', data) 231 | v.entered.attr({y1: -10000, y2: 10000, 'stroke-dasharray': '2 4', stroke: 'red', 'opacity': 0.5}) 232 | v.exit().remove() 233 | }) 234 | } 235 | } -------------------------------------------------------------------------------- /bandline.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bandline renderer 3 | * 4 | * Implementation of Stephen Few's bandlines 5 | * 6 | * Copyright Robert Monfera 7 | * Design: Stephen Few 8 | * Design documentation: https://www.perceptualedge.com/articles/visual_business_intelligence/introducing_bandlines.pdf 9 | */ 10 | 11 | function defined(x) { 12 | return !isNaN(x) && isFinite(x) && x !== null 13 | } 14 | 15 | function rectanglePath(xr, yr) { 16 | return d3.svg.line()([[xr[0], yr[0]], [xr[1], yr[0]], [xr[1], yr[1]], [xr[0], yr[1]]]) + 'Z' 17 | } 18 | 19 | function bandLinePath(valueAccessor, xScale, yScaler, d) { 20 | var drawer = d3.svg.line().defined(compose(defined, property(1))) 21 | return drawer(valueAccessor(d).map(function(s, i) {return [xScale(i), yScaler(d)(s)]})) 22 | } 23 | 24 | function bandData(bands, yScaler, d) { 25 | var yScale = yScaler(d) 26 | return bands.map(function(band, i) { 27 | return {key: i, value: band, yScale: yScale} 28 | }) 29 | } 30 | 31 | function renderBands(root, bands, yScaler, xRanger, yRanger) { 32 | bind(bind(root, 'bands'), 'band', 'path', bandData.bind(0, bands, yScaler)) 33 | .transition().duration(duration) 34 | .attr('class', function(d, i) {return 'band s' + i}) 35 | .attr('d', function(d) {return rectanglePath(xRanger(d), yRanger(d))}) 36 | } 37 | 38 | function pointData(valueAccessor, d) { 39 | return valueAccessor(d).map(function(value, i) {return {key: i, value: value, dd: d}}).filter(compose(defined, value)) 40 | } 41 | 42 | function renderPoints(root, valueAccessor, pointStyleAccessor, rScale, xSpec, ySpec) { 43 | bind(root, 'valuePoints', 'g', pointData.bind(0, valueAccessor)) 44 | .entered 45 | .attr('transform', translate(xSpec, ySpec)) 46 | root['valuePoints'] 47 | .transition().duration(duration) 48 | .attr('transform', translate(xSpec, ySpec)) 49 | bind(root['valuePoints'], 'point', 'circle') 50 | .attr('class', function(d) {return 'point ' + pointStyleAccessor(d.value)}) 51 | .attr('r', function(d) {return rScale(pointStyleAccessor(d.value))}) 52 | root['valuePoints'].exit().remove() 53 | } 54 | 55 | function valuesExtent(valueAccessor, d) { 56 | return d3.extent(valueAccessor(d).filter(defined)) 57 | } 58 | 59 | function sparkStripBoxPath(valueAccessor, xScale, yRange, d) { 60 | var midY = d3.mean(yRange) 61 | var halfHeight = (yRange[1] - yRange[0]) / 2 62 | return rectanglePath( 63 | valuesExtent(valueAccessor, d).map(xScale), 64 | [midY - halfHeight / 2, midY + halfHeight / 2] 65 | ) 66 | } 67 | 68 | function renderExtent(root, valueAccessor, xScale, yRange) { 69 | bind(root, 'valueBox', 'path') 70 | .transition().duration(duration) 71 | .attr('d', sparkStripBoxPath.bind(0, valueAccessor, xScale, yRange)) 72 | } 73 | 74 | function renderValueLine(root, valueAccessor, xScale, yScaler) { 75 | bind(root, 'valueLine', 'path') 76 | .transition().duration(duration) 77 | .attr('d', bandLinePath.bind(0, valueAccessor, xScale, yScaler)) 78 | } 79 | 80 | function renderAxis(root, yAxis, xScale, yScaler) { 81 | bind(root, 'axes', 'g', yAxis ? du.repeat : []) 82 | .each(function(d) { 83 | yAxis.scale(yScaler(d))(d3.select(this).transition().duration(duration)) 84 | }) 85 | .entered 86 | .attr('transform', translateX(xScale.range()[['left', 'right'].indexOf(yAxis && yAxis.orient())])) 87 | } 88 | 89 | function bandLine() { 90 | function renderBandLine(root) { 91 | 92 | var bandLine = bind(root, 'bandLine') 93 | renderBands(bandLine, _bands, _yScalerOfBandLine, constant(_xScaleOfBandLine.range()), function (d) { 94 | return d.value.map(d.yScale) 95 | }) 96 | renderValueLine(bandLine, _valueAccessor, _xScaleOfBandLine, _yScalerOfBandLine) 97 | renderAxis(bandLine, _yAxis, _xScaleOfBandLine, _yScalerOfBandLine) 98 | renderPoints(bandLine, _valueAccessor, _pointStyleAccessor, _rScaleOfBandLine, compose(_xScaleOfBandLine, key), function (d) { 99 | return _yScalerOfBandLine(d.dd)(d.value) 100 | }) 101 | } 102 | 103 | function renderSparkStrip(root) { 104 | 105 | var sparkStrip = bind(root, 'sparkStrip') 106 | renderBands(sparkStrip, _bands, _yScalerOfSparkStrip, function (d) { 107 | return d.value.map(_xScaleOfSparkStrip) 108 | }, constant(_yRange)) 109 | renderExtent(sparkStrip, _valueAccessor, _xScaleOfSparkStrip, _yRange) 110 | renderPoints(sparkStrip, _valueAccessor, _pointStyleAccessor, _rScaleOfSparkStrip, compose(_xScaleOfSparkStrip, value), _yScalerOfSparkStrip()) 111 | } 112 | 113 | function yScalerOfBandLineCalc() { 114 | return function (d) { 115 | return d3.scale.linear().domain(valuesExtent(_valueAccessor, d)).range(_yRange).clamp(true) 116 | } 117 | } 118 | 119 | var _bands = [[0, 0.25], [0.25, 0.5], [0.5, 0.75], [0.75, 1]] 120 | var bands = function(spec) { 121 | if(spec !== void(0)) { 122 | _bands = spec 123 | return obj 124 | } else { 125 | return bands 126 | } 127 | } 128 | 129 | var _valueAccessor = value 130 | var valueAccessor = function(spec) { 131 | if(spec !== void(0)) { 132 | _valueAccessor = spec 133 | _yScalerOfBandLine = yScalerOfBandLineCalc() 134 | return obj 135 | } else { 136 | return _valueAccessor 137 | } 138 | } 139 | 140 | var _xScaleOfBandLine = d3.scale.linear() 141 | var xScaleOfBandLine = function(spec) { 142 | if(spec !== void(0)) { 143 | _xScaleOfBandLine = spec 144 | return obj 145 | } else { 146 | return _xScaleOfBandLine 147 | } 148 | } 149 | 150 | var _xScaleOfSparkStrip = d3.scale.linear() 151 | var xScaleOfSparkStrip = function(spec) { 152 | if(spec !== void(0)) { 153 | _xScaleOfSparkStrip = spec 154 | return obj 155 | } else { 156 | return _xScaleOfSparkStrip 157 | } 158 | } 159 | 160 | var _rScaleOfBandLine = constant(2) 161 | var rScaleOfBandLine = function(spec) { 162 | if(spec !== void(0)) { 163 | _rScaleOfBandLine = spec 164 | return obj 165 | } else { 166 | return _rScaleOfBandLine 167 | } 168 | } 169 | 170 | var _rScaleOfSparkStrip = constant(2) 171 | var rScaleOfSparkStrip = function(spec) { 172 | if(spec !== void(0)) { 173 | _rScaleOfSparkStrip = spec 174 | return obj 175 | } else { 176 | return _rScaleOfSparkStrip 177 | } 178 | } 179 | 180 | var _yRange = [0, 1] 181 | var _yScalerOfSparkStrip 182 | var _yScalerOfBandLine 183 | var yRange = function(spec) { 184 | if(spec !== void(0)) { 185 | _yRange = spec 186 | _yScalerOfSparkStrip = constant(d3.mean(_yRange)) 187 | _yScalerOfBandLine = yScalerOfBandLineCalc() 188 | return obj 189 | } else { 190 | return _yRange 191 | } 192 | } 193 | 194 | var _yAxis = false 195 | var yAxis = function(spec) { 196 | if(spec !== void(0)) { 197 | _yAxis = spec 198 | return obj 199 | } else { 200 | return _yAxis 201 | } 202 | } 203 | 204 | var _pointStyleAccessor = constant('normal') 205 | var pointStyleAccessor = function(spec) { 206 | if(spec !== void(0)) { 207 | _pointStyleAccessor = spec 208 | return obj 209 | } else { 210 | return _pointStyleAccessor 211 | } 212 | } 213 | 214 | var obj = { 215 | renderBandLine: renderBandLine, 216 | renderSparkStrip: renderSparkStrip, 217 | bands: bands, 218 | valueAccessor: valueAccessor, 219 | xScaleOfBandLine: xScaleOfBandLine, 220 | xScaleOfSparkStrip: xScaleOfSparkStrip, 221 | rScaleOfBandLine: rScaleOfBandLine, 222 | rScaleOfSparkStrip: rScaleOfSparkStrip, 223 | yRange: yRange, 224 | yAxis: yAxis, 225 | pointStyleAccessor: pointStyleAccessor 226 | } 227 | 228 | return obj 229 | } -------------------------------------------------------------------------------- /data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Student performance data 3 | * 4 | * Data for Stephen Few's Student Performance Dashboard 5 | * 6 | * Used with permission from Stephen Few 7 | */ 8 | 9 | var dashboardData = { 10 | key: 'full-dashboard-data', 11 | contextScores: [ 12 | {key: "District", median: 0.719, distribution: [0.116, 0.114, 0.254, 0.234, 0.196, 0.086]}, 13 | {key: "School", median: 0.742, distribution: [0.093, 0.104, 0.231, 0.254, 0.222, 0.096]}, 14 | {key: "Other Classes", median: 0.774, distribution: [0.096, 0.097, 0.224, 0.247, 0.233, 0.103]}, 15 | {key: "This Class", median: 0.79, distribution: [0.1, 0.035, 0.17, 0.2675, 0.2675, 0.16]} 16 | ], 17 | "Student Data": { 18 | "Student Name": [ 19 | "Alison Perry", 20 | "Amala Singh", 21 | "Anthony Harper", 22 | "Bae Kim", 23 | "Blaine Harper", 24 | "Brian Francis", 25 | "Britta Jones", 26 | "Christopher Murphy", 27 | "David Chenowith", 28 | "Donald Chase", 29 | "Fariah Jackson", 30 | "Fiona Reeves", 31 | "Frederick Chandler", 32 | "George Smith", 33 | "Hannah Li", 34 | "Holly Norton", 35 | "Jaime Goss", 36 | "James Martin", 37 | "James Snow", 38 | "Jose Domingo", 39 | "Kirsten Holmes", 40 | "Lawrence Parker", 41 | "Maria Garcia", 42 | "Nikolas Mikhailovich", 43 | "Regan Potrero", 44 | "Roshawn Dawson", 45 | "Samuel Miller", 46 | "Sarah Jameson", 47 | "Scott Ortiz", 48 | "Xu Mei" 49 | ], 50 | "Days Absent This Term Count": [ 51 | 3, 52 | 4, 53 | 3, 54 | 6, 55 | 4, 56 | 9, 57 | 2, 58 | 2, 59 | 3, 60 | 0, 61 | 2, 62 | 8, 63 | 0, 64 | 1, 65 | 1, 66 | 0, 67 | 4, 68 | 4, 69 | 1, 70 | 3, 71 | 1, 72 | 0, 73 | 3, 74 | 1, 75 | 1, 76 | 6, 77 | 0, 78 | 3, 79 | 1, 80 | 2 81 | ], 82 | "Days Tardy This Term Count": [ 83 | 0, 84 | 1, 85 | 0, 86 | 8, 87 | 1, 88 | 3, 89 | 1, 90 | 3, 91 | 5, 92 | 0, 93 | 0, 94 | 0, 95 | 0, 96 | 3, 97 | 2, 98 | 2, 99 | 0, 100 | 2, 101 | 0, 102 | 1, 103 | 0, 104 | 0, 105 | 4, 106 | 1, 107 | 2, 108 | 0, 109 | 1, 110 | 4, 111 | 0, 112 | 0 113 | ], 114 | "Disciplinary Referrals This Term Count": [ 115 | 0, 116 | 0, 117 | 0, 118 | 3, 119 | 0, 120 | 0, 121 | 0, 122 | 0, 123 | 0, 124 | 0, 125 | 0, 126 | 1, 127 | 2, 128 | 1, 129 | 0, 130 | 0, 131 | 1, 132 | 0, 133 | 0, 134 | 0, 135 | 0, 136 | 1, 137 | 0, 138 | 0, 139 | 0, 140 | 1, 141 | 0, 142 | 1, 143 | 0, 144 | 0 145 | ], 146 | "Disciplinary Referrals Last Term Count": [ 147 | 0, 148 | 1, 149 | 0, 150 | 0, 151 | 1, 152 | 0, 153 | 0, 154 | 0, 155 | 1, 156 | 1, 157 | 0, 158 | 1, 159 | 2, 160 | 1, 161 | 0, 162 | 0, 163 | 1, 164 | 0, 165 | 0, 166 | 0, 167 | 1, 168 | 0, 169 | 2, 170 | 0, 171 | 0, 172 | 1, 173 | 0, 174 | 0, 175 | 0, 176 | 0 177 | ], 178 | "Detentions This Term Count": [ 179 | 0, 180 | 0, 181 | 0, 182 | 2, 183 | 0, 184 | 0, 185 | 0, 186 | 0, 187 | 0, 188 | 0, 189 | 0, 190 | 1, 191 | 1, 192 | 0, 193 | 0, 194 | 0, 195 | 0, 196 | 0, 197 | 0, 198 | 0, 199 | 0, 200 | 1, 201 | 0, 202 | 0, 203 | 0, 204 | 0, 205 | 0, 206 | 1, 207 | 0, 208 | 0 209 | ], 210 | "Detentions Last Term Count": [ 211 | 0, 212 | 0, 213 | 0, 214 | 0, 215 | 1, 216 | 0, 217 | 0, 218 | 0, 219 | 0, 220 | 1, 221 | 0, 222 | 0, 223 | 1, 224 | 1, 225 | 0, 226 | 0, 227 | 1, 228 | 0, 229 | 0, 230 | 0, 231 | 0, 232 | 0, 233 | 1, 234 | 0, 235 | 0, 236 | 0, 237 | 0, 238 | 0, 239 | 0, 240 | 0 241 | ], 242 | scoreHeadings: [ 243 | "Latest Standardized Math Assessment Score", 244 | "9th Grade Standardized Math Assessment Score", 245 | "8th Grade Standardized Math Assessment Score", 246 | "7th Grade Standardized Math Assessment Score", 247 | "6th Grade Standardized Math Assessment Score", 248 | "Assignment 1 Score", 249 | "Assignment 2 Score", 250 | "Assignment 3 Score", 251 | "Assignment 4 Score", 252 | "Assignment 5 Score"], 253 | scores: [ 254 | [0.85, 0.89, 0.84, 0.82, 0.85, 0.87, 0.91, 0.98, 0.78, 0.91], 255 | [0.91, 0.94, 0.93, 0.89, 0.87, 0.90, 0.87, 0.93, 0.90, 0.99], 256 | [0.62, 0.67, 0.70, 0.73, 0.79, 0.65, 0.79, 0.55, 0.70, 0.78], 257 | [0.39, 0.71, 0.74, 0.78, 0.75, 0.46, 0.68, 0.71, 0.51, 0.61], 258 | [0.71, 0.68, 0.73, 0.74, 0.69, 0.69, 0.73, 0.77, 0.81, 0.74], 259 | [0.67, 0.66, 0.71, 0.78, 0.81, 0.57, 0.66, 0.70, 0.62, 0.69], 260 | [0.85, 0.81, 0.82, 0.78, 0.75, 0.81, 0.87, 0.79, 0.80, 0.77], 261 | [0.55, 0.49, 0.51, 0.58, 0.77, 0.55, 0.73, 0.70, 0.72, 0.78], 262 | [0.80, 0.76, 0.82, 0.84, 0.85, 0.81, 0.88, 0.92, 0.84, 0.97], 263 | [0.92, 0.89, 0.91, 0.96, 0.93, 0.97, 0.93, 0.90, 0.97, 0.95], 264 | [0.84, 0.81, 0.81, 0.83, 0.80, 0.89, 0.92, 0.85, 0.84, 0.88], 265 | [0.47, 0.41, 0.52, 0.47, 0.50, 0.61, 0.69, 0.54, 0.75, 0.64], 266 | [0.41, 0.62, 0.64, 0.67, 0.51, 0.71, 0.65, NaN, 0.60, 0.68], 267 | [0.76, 0.71, 0.74, 0.73, 0.71, 0.78, NaN, 0.81, NaN, 0.76], 268 | [0.94, 0.91, 0.94, 0.93, 0.95, 0.91, 0.93, 0.98, NaN, 0.94], 269 | [0.98, 0.99, 0.97, 0.97, 0.96, 0.98, 1.00, 0.97, 0.95, 1.00], 270 | [0.82, 0.81, 0.78, 0.83, 0.84, 0.88, 0.81, 0.78, 0.85, 0.86], 271 | [0.71, 0.74, 0.75, 0.75, 0.73, 0.71, 0.74, 0.69, 0.79, 0.75], 272 | [0.91, 0.86, 0.90, 0.92, 0.90, 0.91, 0.94, 0.89, 0.99, 0.97], 273 | [0.84, 0.90, 0.91, 0.89, 0.93, 0.87, 0.84, 0.89, 0.88, 0.84], 274 | [0.67, 0.72, 0.73, 0.75, 0.77, 0.79, 0.70, 0.66, 0.71, 0.72], 275 | [0.80, 0.79, 0.78, 0.83, 0.84, 0.81, 0.84, 0.88, 0.89, 0.91], 276 | [0.72, 0.75, 0.76, 0.71, 0.76, 0.90, 0.81, 0.89, 0.82, 0.88], 277 | [0.63, 0.57, 0.55, 0.64, 0.63, 0.66, 0.71, 0.73, 0.70, 0.79], 278 | [0.67, 0.70, 0.68, 0.71, 0.73, 0.76, 0.79, 0.78, 0.89, 0.72], 279 | [0.78, 0.79, 0.82, 0.80, 0.77, 0.64, 0.71, 0.75, 0.78, 0.71], 280 | [0.81, 0.76, 0.83, 0.84, 0.81, 0.81, 0.88, 0.77, 0.91, 0.84], 281 | [0.78, 0.85, 0.89, 0.92, 0.91, 0.87, 0.83, 0.92, 0.90, 0.89], 282 | [0.82, 0.71, 0.74, 0.73, 0.78, 0.89, 0.78, 0.84, 0.79, 0.81], 283 | [0.83, 0.86, 0.89, 0.88, 0.91, 0.92, 0.87, 0.65, 0.88, 0.85] 284 | ], 285 | "Assignments Completed Late Count": [ 286 | 0, 287 | 0, 288 | 1, 289 | 3, 290 | 0, 291 | 2, 292 | 0, 293 | 1, 294 | 0, 295 | 0, 296 | 0, 297 | 3, 298 | 2, 299 | 1, 300 | 0, 301 | 0, 302 | 0, 303 | 1, 304 | 0, 305 | 0, 306 | 0, 307 | 0, 308 | 0, 309 | 0, 310 | 1, 311 | 0, 312 | 0, 313 | 1, 314 | 1, 315 | 0 316 | ], 317 | gradeHeadings: ["Current Course Grade", "Student's Course Grade Goal", "Previous Math Course Grade"], 318 | grades: [ 319 | ["B", "A", "A"], 320 | ["A", "A", "A"], 321 | ["D", "C", "C"], 322 | ["F", "C", "C"], 323 | ["C", "B", "D"], 324 | ["D", "C", "C"], 325 | ["B", "B", "B"], 326 | ["D", "C", "F"], 327 | ["B", "B", "C"], 328 | ["A", "B", "A"], 329 | ["B", "A", "B"], 330 | ["D", "C", "F"], 331 | ["F", "C", "C"], 332 | ["C", "B", "C"], 333 | ["A", "A", "A"], 334 | ["A", "A", "A"], 335 | ["B", "B", "B"], 336 | ["C", "C", "C"], 337 | ["A", "A", "B"], 338 | ["B", "B", "A"], 339 | ["C", "C", "C"], 340 | ["B", "A", "B"], 341 | ["B", "B", "B"], 342 | ["C", "C", "F"], 343 | ["C", "B", "C"], 344 | ["C", "C", "C"], 345 | ["B", "B", "C"], 346 | ["B", "A", "A"], 347 | ["B", "C", "C"], 348 | ["B", "B", "B"] 349 | ], 350 | "English Language Proficiency": [ 351 | "Y", 352 | "Y", 353 | "Y", 354 | "N", 355 | "Y", 356 | "Y", 357 | "Y", 358 | "Y", 359 | "Y", 360 | "Y", 361 | "Y", 362 | "Y", 363 | "Y", 364 | "Y", 365 | "Y", 366 | "Y", 367 | "Y", 368 | "Y", 369 | "Y", 370 | "Y", 371 | "Y", 372 | "Y", 373 | "Y", 374 | "N", 375 | "Y", 376 | "Y", 377 | "Y", 378 | "Y", 379 | "Y", 380 | "Y" 381 | ], 382 | "Special Ed Status": [ 383 | "N", 384 | "N", 385 | "Y", 386 | "N", 387 | "N", 388 | "N", 389 | "N", 390 | "N", 391 | "N", 392 | "N", 393 | "N", 394 | "Y", 395 | "N", 396 | "N", 397 | "N", 398 | "N", 399 | "N", 400 | "N", 401 | "N", 402 | "N", 403 | "N", 404 | "N", 405 | "N", 406 | "N", 407 | "N", 408 | "N", 409 | "N", 410 | "N", 411 | "N", 412 | "N" 413 | ], 414 | "Problematic": [ 415 | "N", 416 | "N", 417 | "N", 418 | "Y", 419 | "N", 420 | "Y", 421 | "N", 422 | "N", 423 | "N", 424 | "N", 425 | "N", 426 | "N", 427 | "Y", 428 | "N", 429 | "N", 430 | "N", 431 | "N", 432 | "N", 433 | "N", 434 | "N", 435 | "N", 436 | "N", 437 | "N", 438 | "N", 439 | "N", 440 | "N", 441 | "N", 442 | "N", 443 | "N", 444 | "N" 445 | ] 446 | 447 | 448 | }, 449 | "Absences": [ 450 | ["Alison Perry", "2012-02-09"], 451 | ["Alison Perry", "2012-03-05"], 452 | ["Alison Perry", "2012-03-06"], 453 | ["Amala Singh", "2012-01-30"], 454 | ["Amala Singh", "2012-02-17"], 455 | ["Amala Singh", "2012-02-23"], 456 | ["Amala Singh", "2012-02-24"], 457 | ["Anthony Harper", "2012-02-15"], 458 | ["Anthony Harper", "2012-03-13"], 459 | ["Anthony Harper", "2012-04-09"], 460 | ["Bae Kim", "2012-02-16"], 461 | ["Bae Kim", "2012-03-05"], 462 | ["Bae Kim", "2012-03-19"], 463 | ["Bae Kim", "2012-03-30"], 464 | ["Bae Kim", "2012-04-09"], 465 | ["Bae Kim", "2012-04-17"], 466 | ["Blaine Harper", "2012-01-09"], 467 | ["Blaine Harper", "2012-01-12"], 468 | ["Blaine Harper", "2012-02-15"], 469 | ["Blaine Harper", "2012-04-09"], 470 | ["Brian Francis", "2012-01-10"], 471 | ["Brian Francis", "2012-01-17"], 472 | ["Brian Francis", "2012-01-18"], 473 | ["Brian Francis", "2012-01-19"], 474 | ["Brian Francis", "2012-02-22"], 475 | ["Brian Francis", "2012-02-23"], 476 | ["Brian Francis", "2012-03-28"], 477 | ["Brian Francis", "2012-04-09"], 478 | ["Brian Francis", "2012-04-20"], 479 | ["Britta Jones", "2012-02-01"], 480 | ["Britta Jones", "2012-04-13"], 481 | ["Christopher Murphy", "2012-01-20"], 482 | ["Christopher Murphy", "2012-04-30"], 483 | ["David Chenowith", "2012-01-24"], 484 | ["David Chenowith", "2012-01-25"], 485 | ["David Chenowith", "2012-02-21"], 486 | ["Fariah Jackson", "2012-02-07"], 487 | ["Fariah Jackson", "2012-03-22"], 488 | ["Fiona Reeves", "2012-01-19"], 489 | ["Fiona Reeves", "2012-02-07"], 490 | ["Fiona Reeves", "2012-02-08"], 491 | ["Fiona Reeves", "2012-02-09"], 492 | ["Fiona Reeves", "2012-03-14"], 493 | ["Fiona Reeves", "2012-04-17"], 494 | ["Fiona Reeves", "2012-04-19"], 495 | ["Fiona Reeves", "2012-04-20"], 496 | ["George Smith", "2012-03-08"], 497 | ["Hannah Li", "2012-01-24"], 498 | ["Jaime Goss", "2012-02-08"], 499 | ["Jaime Goss", "2012-03-05"], 500 | ["Jaime Goss", "2012-04-09"], 501 | ["Jame Goss", "2012-03-12"], 502 | ["James Martin", "2012-01-18"], 503 | ["James Martin", "2012-04-26"], 504 | ["James Martin", "2012-04-27"], 505 | ["James Martin", "2012-04-30"], 506 | ["James Snow", "2012-02-09"], 507 | ["Jose Domingo", "2012-02-08"], 508 | ["Jose Domingo", "2012-03-13"], 509 | ["Jose Domingo", "2012-03-14"], 510 | ["Kirsten Holmes", "2012-03-13"], 511 | ["Maria Garcia", "2012-03-08"], 512 | ["Maria Garcia", "2012-03-09"], 513 | ["Maria Garcia", "2012-04-23"], 514 | ["Nikolas Mikhailovich", "2012-01-17"], 515 | ["Regan Potrero", "2012-03-06"], 516 | ["Roshawn Dawson", "2012-01-09"], 517 | ["Roshawn Dawson", "2012-01-10"], 518 | ["Roshawn Dawson", "2012-01-11"], 519 | ["Roshawn Dawson", "2012-01-12"], 520 | ["Roshawn Dawson", "2012-01-13"], 521 | ["Roshawn Dawson", "2012-04-17"], 522 | ["Sarah Jameson", "2012-02-07"], 523 | ["Sarah Jameson", "2012-03-27"], 524 | ["Sarah Jameson", "2012-04-17"], 525 | ["Scott Ortiz", "2012-03-14"], 526 | ["Xu Mei", "2012-03-08"], 527 | ["Xu Mei", "2012-04-16"] 528 | ], 529 | "Tardies": [ 530 | ["Amala Singh", "2012-01-11"], 531 | ["Bae Kim", "2012-01-09"], 532 | ["Bae Kim", "2012-01-12"], 533 | ["Bae Kim", "2012-01-30"], 534 | ["Bae Kim", "2012-02-21"], 535 | ["Bae Kim", "2012-02-29"], 536 | ["Bae Kim", "2012-04-11"], 537 | ["Bae Kim", "2012-04-23"], 538 | ["Bae Kim", "2012-04-27"], 539 | ["Blaine Harper", "2012-03-19"], 540 | ["Brian Francis", "2012-01-11"], 541 | ["Brian Francis", "2012-02-09"], 542 | ["Brian Francis", "2012-04-17"], 543 | ["Britta Jones", "2012-01-11"], 544 | ["Christopher Murphy", "2012-02-09"], 545 | ["Christopher Murphy", "2012-03-06"], 546 | ["Christopher Murphy", "2012-03-30"], 547 | ["David Chenowith", "2012-01-31"], 548 | ["David Chenowith", "2012-02-15"], 549 | ["David Chenowith", "2012-02-22"], 550 | ["David Chenowith", "2012-04-12"], 551 | ["David Chenowith", "2012-04-23"], 552 | ["George Smith", "2012-01-13"], 553 | ["George Smith", "2012-04-19"], 554 | ["George Smith", "2012-04-30"], 555 | ["Hannah Li", "2012-01-25"], 556 | ["Hannah Li", "2012-03-19"], 557 | ["Holly Norton", "2012-02-21"], 558 | ["Holly Norton", "2012-03-09"], 559 | ["James Martin", "2012-01-26"], 560 | ["James Martin", "2012-03-21"], 561 | ["Jose Domingo", "2012-01-24"], 562 | ["Maria Garcia", "2012-01-14"], 563 | ["Maria Garcia", "2012-01-25"], 564 | ["Maria Garcia", "2012-04-10"], 565 | ["Maria Garcia", "2012-04-25"], 566 | ["Nikolas Mikhailovich", "2012-03-19"], 567 | ["Regan Potrero", "2012-01-25"], 568 | ["Regan Potrero", "2012-02-01"], 569 | ["Samuel Miller", "2012-02-27"], 570 | ["Sarah Jameson", "2012-01-19"], 571 | ["Sarah Jameson", "2012-02-08"], 572 | ["Sarah Jameson", "2012-03-06"], 573 | ["Sarah Jameson", "2012-04-23"] 574 | ] 575 | } -------------------------------------------------------------------------------- /model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dashboard model - data reshaping, variables, scales and configuration 3 | * 4 | * Copyright Robert Monfera 5 | */ 6 | 7 | dashboardData['Student Data'] = dashboardData['Student Data']['Student Name'].map(function(name, i) { 8 | var d = dashboardData['Student Data'] 9 | var studentModel = { 10 | key: name, 11 | absentCount: d["Days Absent This Term Count"][i], 12 | tardyCount: d["Days Tardy This Term Count"][i], 13 | absences: dashboardData['Absences'].filter(function(event) {return event[0] === name}).map(function(event) {return new Date(event[1])}), 14 | tardies: dashboardData['Tardies'].filter(function(event) {return event[0] === name}).map(function(event) {return new Date(event[1])}), 15 | currentReferralCount: d["Disciplinary Referrals This Term Count"][i], 16 | pastReferralCount: d["Disciplinary Referrals Last Term Count"][i], 17 | currentDetentionCount: d["Detentions This Term Count"][i], 18 | pastDetentionCount: d["Detentions Last Term Count"][i], 19 | assignmentsLateCount: d["Assignments Completed Late Count"][i], 20 | english: d["English Language Proficiency"][i] === 'Y', 21 | special: d["Special Ed Status"][i] === 'Y', 22 | problematic: d["Problematic"][i] === 'Y', 23 | assignmentScores: d["scores"][i].slice(5), 24 | meanAssignmentScore: d3.mean(d["scores"][i].slice(5).filter(identity)), 25 | standardScores: d["scores"][i].slice(0, 5).reverse(), 26 | grades: object(d["grades"][i].map(function(g, j) { 27 | function indexToKey(index) { 28 | switch(index) { 29 | case 0: return 'current' 30 | case 1: return 'goal' 31 | case 2: return 'previous' 32 | } 33 | } 34 | return [indexToKey(j), g] 35 | })) 36 | } 37 | studentModel.allScores = studentModel.standardScores.concat(studentModel.assignmentScores) 38 | return studentModel 39 | }) 40 | 41 | var dashboardVariables = { 42 | name: { 43 | key: 'name', 44 | groupAlias: 'namesGroup', 45 | headerAlias: 'Student', 46 | helpText: 'Student name\n[Click and hold for sorting]', 47 | smallHeaderAlias: 'Student name', 48 | dataType: 'string', 49 | variableType: 'nominal', 50 | defaultOrder: 'ascending', 51 | plucker: key 52 | }, 53 | currentGrade: { 54 | key: 'currentGrade', 55 | groupAlias: 'courseGradesGroup', 56 | petiteHeaderAlias: 'YTD', 57 | headerAlias: 'Overall Course Grade', 58 | helpText: 'Current course grade and score\n[Default sorting is by score]', 59 | legendAlias: 'Current', 60 | smallHeaderAlias: 'Current grade', 61 | dataType: 'string', 62 | variableType: 'ordinal', 63 | defaultOrder: 'descending', 64 | plucker: function(student) {return student.grades.current}, 65 | sorter: function(student) {return -d3.mean(student.assignmentScores.filter(identity))} 66 | }, 67 | negligence: { 68 | key: 'negligence', 69 | headerAlias: 'Behavior', 70 | groupAlias: 'classDisciplineGroup', 71 | helpText: 'Behavioral problem counts\n[Click and hold for sorting based on detention (with double weight) plus referral]', 72 | dataType: 'numeric', 73 | variableType: 'cardinal', 74 | defaultOrder: 'descending', 75 | plucker: function(student) {return student.currentReferralCount + 2 * student.currentDetentionCount} 76 | }, 77 | punishment: { 78 | key: 'punishment', 79 | groupAlias: 'behaviorGroup', 80 | dataType: 'numeric', 81 | variableType: 'cardinal', 82 | defaultOrder: 'descending', 83 | plucker: function(student) {return 5 * student.currentReferralCount + 15 * student.currentDetentionCount} 84 | }, 85 | attendance: { 86 | key: 'negligence', 87 | groupAlias: 'classAttendanceGroup', 88 | headerAlias: 'Attendance', 89 | helpText: 'Attendance problem counts\n[Click and hold for sorting based on absent (with double weight) plus tardy]', 90 | dataType: 'numeric', 91 | variableType: 'cardinal', 92 | defaultOrder: 'descending', 93 | plucker: function(student) {return student.tardyCount + 2 * student.absentCount} 94 | }, 95 | lastAssignmentScore: { 96 | key: 'lastAssignmentScore', 97 | groupAlias: 'assignmentScoresGroup', 98 | smallHeaderAlias: 'Last assign.', 99 | petiteHeaderAlias: 'Last', 100 | helpText: 'Last grade score (circle) and average (bar)\n[Click and hold for sorting based on last grade]', 101 | dataType: 'numeric', 102 | variableType: 'cardinal', 103 | defaultOrder: 'ascending', 104 | plucker: function(student) {return last(student.assignmentScores)} 105 | }, 106 | lastAssessmentScore: { 107 | key: 'lastAssessmentScore', 108 | groupAlias: 'assessmentScoresGroup', 109 | headerAlias: 'Assessments', 110 | petiteHeaderAlias: 'Last ', 111 | helpText: 'Last assessment scores\n[Click and hold for sorting]', 112 | dataType: 'numeric', 113 | variableType: 'cardinal', 114 | defaultOrder: 'ascending', 115 | plucker: function(student) {return last(student.standardScores)} 116 | }, 117 | meanAssignmentScore: { 118 | key: 'meanAssignmentScore', 119 | headerAlias: 'Assignments', 120 | helpText: 'Year to date assignment scores\n[Click and hold for sorting based on the average score]', 121 | dataType: 'numeric', 122 | variableType: 'cardinal', 123 | defaultOrder: 'ascending', 124 | plucker: function(student) {return d3.mean(student.assignmentScores.filter(identity))} 125 | }, 126 | assignmentSpread: { 127 | key: 'assignmentSpread', 128 | petiteHeaderAlias: 'Spread', 129 | helpText: 'Spread of assignment scores\n[Click and hold for sorting based on the standard deviation]', 130 | dataType: 'numeric', 131 | variableType: 'cardinal', 132 | defaultOrder: 'descending', 133 | plucker: function(student) {return d3.deviation(student.assignmentScores.filter(identity))} 134 | }, 135 | targetGrade: { 136 | key: 'targetGrade', 137 | legendAlias: 'Target', 138 | dataType: 'string', 139 | variableType: 'ordinal', 140 | defaultOrder: 'descending', 141 | plucker: function(student) {return student.grades.goal} 142 | }, 143 | previousGrade: { 144 | key: 'previousGrade', 145 | legendAlias: 'Previous course', 146 | dataType: 'string', 147 | variableType: 'ordinal', 148 | defaultOrder: 'descending', 149 | plucker: function(student) {return student.grades.previous} 150 | }, 151 | tardy: { 152 | key: 'tardy', 153 | legendAlias: 'Tardy', 154 | dataType: 'numeric', 155 | variableType: 'cardinal', 156 | defaultOrder: 'descending', 157 | plucker: function(student) {return student.tardyCount} 158 | }, 159 | absent: { 160 | key: 'absent', 161 | legendAlias: 'Absent', 162 | dataType: 'numeric', 163 | variableType: 'cardinal', 164 | defaultOrder: 'descending', 165 | plucker: function(student) {return student.absentCount} 166 | }, 167 | referrals: { 168 | key: 'referrals', 169 | legendAlias: 'Referrals', 170 | petiteHeaderAlias: 'Ref', 171 | helpText: 'Referrals count\n[Click and hold for sorting]', 172 | dataType: 'numeric', 173 | variableType: 'cardinal', 174 | defaultOrder: 'descending', 175 | plucker: function(student) {return student.currentReferralCount} 176 | }, 177 | detentions: { 178 | key: 'detentions', 179 | legendAlias: 'Detentions', 180 | petiteHeaderAlias: 'Det', 181 | helpText: 'Detentions count\n[Click and hold for sorting]', 182 | dataType: 'numeric', 183 | variableType: 'cardinal', 184 | defaultOrder: 'descending', 185 | plucker: function(student) {return student.currentDetentionCount} 186 | }, 187 | lateAssignment: { 188 | key: 'lateAssignment', 189 | smallHeaderAlias: 'Late assign.', 190 | petiteHeaderAlias: 'Late', 191 | helpText: 'Late assignment count\n[Click and hold for sorting]', 192 | dataType: 'numeric', 193 | variableType: 'cardinal', 194 | defaultOrder: 'descending', 195 | plucker: function(student) {return student.assignmentsLateCount} 196 | }, 197 | aboveHighThreshold: { 198 | key: 'aboveHighThreshold', 199 | legendAlias: 'Above 85%', 200 | dataType: 'numeric', 201 | variableType: 'cardinal', 202 | defaultOrder: 'ascending', 203 | plucker: function(student) {return countBy(student.assignmentScores, function(x) {return x > 0.85})['true'] || 0} 204 | }, 205 | belowLowThreshold: { 206 | key: 'belowLowThreshold', 207 | legendAlias: 'Below 67%', 208 | dataType: 'numeric', 209 | variableType: 'cardinal', 210 | defaultOrder: 'descending', 211 | plucker: function(student) {return countBy(student.assignmentScores, function(x) {return x !== 0 && x < 0.67})['true'] || 0} 212 | }, 213 | currentYearMeanAssignmentScore: { 214 | key: 'currentYearMeanAssignmentScore', 215 | legendAlias: 'Assignments', 216 | dataType: 'numeric', 217 | variableType: 'cardinal', 218 | defaultOrder: 'ascending', 219 | plucker: function(student) {return d3.mean(student.assignmentScores)} 220 | }, 221 | pastYearsMeanAssignmentScore: { 222 | key: 'pastYearsMeanAssignmentScore', 223 | legendAlias: 'Past assessmts', 224 | petiteHeaderAlias: 'Last 5', 225 | helpText: "Past 5 years' assignment scores\n[Click and hold for sorting based on the average]", 226 | dataType: 'numeric', 227 | variableType: 'cardinal', 228 | defaultOrder: 'ascending', 229 | plucker: function(student) {return d3.mean(student.standardScores)} 230 | }, 231 | specialEducation: { 232 | key: 'specialEducation', 233 | legendAlias: 'Special education', 234 | dataType: 'boolean', 235 | variableType: 'cardinal', 236 | defaultOrder: 'descending', 237 | plucker: function(student) {return student.special} 238 | }, 239 | languageDifficulties: { 240 | key: 'languageDifficulties', 241 | legendAlias: 'Language difficulties', 242 | dataType: 'boolean', 243 | variableType: 'cardinal', 244 | defaultOrder: 'descending', 245 | plucker: function(student) {return !student.english} 246 | }, 247 | assignmentScoreTemplate: { 248 | key: 'assignmentScoreTemplate', 249 | axisAlias: 'assignmentScoresGroup', 250 | dataType: 'numeric', 251 | variableType: 'cardinal', 252 | defaultOrder: 'ascending', 253 | pluckerMaker: function(i) {return function(student) {return student.allScores[i]}} 254 | } 255 | } 256 | 257 | var defaultSortVariable = dashboardVariables['currentGrade'] 258 | 259 | var dashboardSettings = { 260 | variables: dashboardVariables, 261 | table: { 262 | sort: { 263 | sortVariable: defaultSortVariable 264 | }, 265 | studentSelection: { 266 | selectedStudents: {}, 267 | brushable: false, 268 | brushInProgress: false 269 | } 270 | } 271 | 272 | } 273 | 274 | function sortedByThis(identifierType, identifier) { 275 | var sortSettings = dashboardSettings.table.sort 276 | var sortVariable = sortSettings.sortVariable 277 | return sortVariable && sortVariable[identifierType] === identifier 278 | } 279 | 280 | function rowSorter(sortSettings) { 281 | var v = sortSettings.sortVariable 282 | var order = v.defaultOrder 283 | var plucker = v.sorter || v.plucker 284 | return { 285 | sorter: function rowSorterClosure(d) {return plucker(d)}, 286 | variable: v, 287 | order: order 288 | } 289 | } 290 | 291 | function keptStudentFilterFunction(d) { 292 | var selectedStudents = dashboardSettings.table.studentSelection.selectedStudents 293 | return selectedStudents[d] || !Object.keys(selectedStudents).length 294 | } 295 | 296 | function keptStudentData(d) { 297 | return d['Student Data'].filter(function(student) {return keptStudentFilterFunction(student.key)}) 298 | } 299 | 300 | function makeRowData(d) { 301 | var rowData = d["Student Data"] 302 | var sorter = rowSorter(dashboardSettings.table.sort) 303 | var needsToReverse = sorter.order === 'descending' 304 | var ascendingRowData = sortBy(needsToReverse ? rowData.reverse() : rowData, sorter.sorter) 305 | var sortedRowData = needsToReverse ? ascendingRowData.reverse() : ascendingRowData 306 | d["Student Data"] = sortedRowData // this retains the stable sorting (sortBy is stable, but if we don't persist it, then it's just stable sorting relative to the original order, rather than the previous order 307 | 308 | ;(function formBlocks() { 309 | // fixme extract, break up, refactor etc. this monstrous block 310 | var k, currentStudent, valueMaker, currentValue, prevValue = NaN, rowGroupOffsetCount = -1, groupable; 311 | var quantiles = [0.2, 0.4, 0.6, 0.8]; 312 | if (sorter.variable.variableType === 'ordinal' || sorter.variable.dataType === 'boolean') { 313 | groupable = true 314 | valueMaker = sorter.variable.plucker 315 | } else if (sorter.variable.variableType === 'cardinal') { 316 | groupable = true 317 | var rawValues = sortedRowData.map(sorter.variable.plucker) 318 | if (Object.keys(countBy(rawValues, identity)).length <= 5) { 319 | valueMaker = sorter.variable.plucker 320 | } else { 321 | var quantileValues = quantiles.map(function (p) { 322 | return d3.quantile(rawValues, p) 323 | }) 324 | valueMaker = function (d) { 325 | var result = 0; 326 | for (var n = 0; n < quantileValues.length; n++) { 327 | if (sorter.variable.plucker(d) >= quantileValues[n]) { 328 | result = n + 1 329 | } else { 330 | break 331 | } 332 | } 333 | return result 334 | } 335 | } 336 | } 337 | 338 | for (k = 0; k < d["Student Data"].length; k++) { 339 | currentStudent = d["Student Data"][k] 340 | if (!groupable) { 341 | currentStudent.rowGroupOffsetCount = 0 342 | continue 343 | } 344 | currentValue = valueMaker(currentStudent) 345 | if (currentValue !== prevValue) { 346 | rowGroupOffsetCount++ 347 | prevValue = currentValue 348 | } 349 | currentStudent.rowGroupOffsetCount = rowGroupOffsetCount 350 | } 351 | })() 352 | 353 | return sortedRowData 354 | } 355 | 356 | /** 357 | * Scales: bridging the view / viewModel boundary 358 | */ 359 | 360 | function calculateScales() { 361 | 362 | var s = {} 363 | 364 | s.rowPitch = 28 365 | s.rowBandRange = s.rowPitch / 1.3 366 | 367 | s.gradesDomain = ['F', 'D', 'C', 'B', 'A'] 368 | 369 | var shamiksCellWidthRange = [0, 104] 370 | 371 | var gradesRange = [0, 80] 372 | var gradesRangeExtent = gradesRange[1] - gradesRange[0] 373 | 374 | s.gradeScale = d3.scale.ordinal() 375 | .domain(s.gradesDomain) 376 | .rangePoints(gradesRange) 377 | 378 | s.scoreToGrade = function(d) { 379 | return s.gradesDomain[Math.floor((d - 0.50001) / 0.1)] 380 | } 381 | 382 | s.gradeOverlayScale = d3.scale.linear() 383 | .domain([0.5, 1]) 384 | .range([gradesRange[0] - gradesRangeExtent / 10, gradesRange[1] + gradesRangeExtent / 10]) 385 | 386 | s.violationDayScale = d3.scale.linear() 387 | .domain([0, 20]) 388 | .range(shamiksCellWidthRange) 389 | 390 | s.violationSeverityOpacityScale = d3.scale.linear() 391 | .domain([0, 3]) 392 | .range([1, 0.1]) 393 | 394 | s.scoreTemporalScale = d3.scale.linear() 395 | .domain([0, 9]) // fixme adapt the scale for the actual number of scores 396 | .range(shamiksCellWidthRange) 397 | 398 | var bandThresholds = [0.4, 0.6, 0.7, 0.8, 0.9, 1] 399 | 400 | function sortedNumbers(population) { 401 | return population.filter(defined).sort(d3.ascending) 402 | } 403 | 404 | var outlierClassifications = ['lowOutlier', 'normal', 'highOutlier'] 405 | var outlierClassificationIndex = function(classification) { 406 | return outlierClassifications.indexOf(classification) 407 | } 408 | 409 | function makeOutlierScale(population) { 410 | var iqrDistanceMultiplier = 1 // Stephen Few's Introduction of Bandlines requires a multiplier of 1.5; we deviate here to show outliers on the dashboard 411 | var values = sortedNumbers(population) 412 | var iqr = [d3.quantile(values, 0.25), d3.quantile(values, 0.75)] 413 | var midspread = iqr[1] - iqr[0] 414 | return d3.scale.threshold() 415 | .domain([ 416 | iqr[0] - iqrDistanceMultiplier * midspread, 417 | iqr[1] + iqrDistanceMultiplier * midspread ]) 418 | .range(outlierClassifications) 419 | } 420 | 421 | function medianLineBand(population) { 422 | var median = d3.median(population) 423 | return [median, median] 424 | } 425 | 426 | var assignmentScores = [].concat.apply([], dashboardData['Student Data'].map(property('assignmentScores'))) 427 | var assessmentScores = [].concat.apply([], dashboardData['Student Data'].map(property('standardScores'))) 428 | 429 | s.assignmentOutlierScale = makeOutlierScale(assignmentScores) 430 | s.assessmentOutlierScale = makeOutlierScale(assessmentScores) 431 | 432 | s.assignmentBands = window2(bandThresholds).concat([medianLineBand(assignmentScores)]) 433 | s.assessmentBands = window2(bandThresholds).concat([medianLineBand(assessmentScores)]) 434 | 435 | s.bandLinePointRScale = function(classification) { 436 | return [2.5, 1.5, 3][outlierClassificationIndex(classification)] 437 | } 438 | s.sparkStripPointRScale = function(classification) { 439 | return 2 // r = 2 on the spark strip irrespective of possible outlier status 440 | } 441 | 442 | var assignmentScoreVerticalDomain = d3.extent(bandThresholds) // fixme adapt the scale for the actual score domain 443 | 444 | var assignmentScoreCount = 7 // 5 past assignments and 2 future assignments 445 | 446 | var assignmentScoreDomain = [0, assignmentScoreCount - 1] 447 | 448 | s.assignmentScoreTemporalScale = d3.scale.linear() 449 | .domain(assignmentScoreDomain) // fixme adapt the scale for the actual number of scores 450 | .range([2, 74]) 451 | 452 | s.assignmentScoreTemporalScale2 = d3.scale.linear() 453 | .domain(assignmentScoreVerticalDomain) 454 | .range([2, 50]) 455 | 456 | s.assessmentScoreTemporalScale = d3.scale.linear() 457 | .domain([0, 4]) // fixme adapt the scale for the actual number of scores 458 | .range([0, 58]) 459 | 460 | var scoreRange = [s.rowBandRange / 2 , -s.rowBandRange / 2] 461 | 462 | s.assessmentScoreScale = d3.scale.linear() 463 | .domain([0.5, 1]) // fixme adapt the scale for the actual score domain 464 | .range(scoreRange) 465 | 466 | s.assignmentScoreVerticalScale = d3.scale.linear() 467 | .domain(assignmentScoreVerticalDomain) 468 | .range(scoreRange) 469 | 470 | s.assignmentScoreVerticalScaleLarge = d3.scale.linear() 471 | .domain(assignmentScoreVerticalDomain) 472 | .range([s.rowBandRange , -s.rowBandRange]) 473 | 474 | s.assignmentScoreHorizontalScale = d3.scale.linear() 475 | .domain(assignmentScoreVerticalDomain) 476 | .range([0, 98]) 477 | 478 | s.scoreBandScale = d3.scale.ordinal() 479 | .domain(d3.range(6)) 480 | .rangePoints([0, 100], 1) 481 | 482 | s.studentRatioScale = d3.scale.linear() 483 | .domain([0, 0.3001]) // 0 to 30%; the small increment is added as the interval is open on the right 484 | .range([0, -42]) 485 | 486 | s.temporalScale = d3.time.scale() 487 | .domain([new Date('2012-01-09'), new Date('2012-05-25')]) 488 | .range([0, 200]) 489 | 490 | var absentTardyDomain = [0, 5.0001] 491 | 492 | var absentAbsoluteRange = 26 493 | 494 | s.absentScale = d3.scale.linear() 495 | .domain(absentTardyDomain) 496 | .range([0, -absentAbsoluteRange]) 497 | 498 | s.tardyScale = d3.scale.linear() 499 | .domain(absentTardyDomain) 500 | .range([0, absentAbsoluteRange]) 501 | 502 | var histogramChartRangeX = [0, 98] 503 | var boxplotChartRangeX = [0, 100] 504 | var upperRightChartsRangeY = [0, -40] 505 | 506 | s.histogramGradeScale = d3.scale.ordinal() 507 | .domain(s.gradesDomain) 508 | .rangePoints(histogramChartRangeX, 1) 509 | 510 | s.histogramStudentCountScale = d3.scale.linear() 511 | .domain([0, 13.0001]) 512 | .range(upperRightChartsRangeY) 513 | 514 | s.boxplotScoreScale = d3.scale.linear() 515 | .domain([0.4, 1.0001]) 516 | .range([boxplotChartRangeX[0], boxplotChartRangeX[1] - 5] /* pointRadius + 1 */) 517 | 518 | s.boxplotAssignmentNumberScale = d3.scale.ordinal() 519 | .domain([1, 2, 3, 4, 5]) 520 | .rangePoints(upperRightChartsRangeY, 1) 521 | 522 | return s 523 | } 524 | -------------------------------------------------------------------------------- /render.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dashboard renderer 3 | * 4 | * Implementation of Stephen Few's Student Performance Dashboard with Mike Bostock's d3.js library 5 | * 6 | * Copyright Robert Monfera 7 | * Copyright on the design of the Student Performance Dashboard: Stephen Few 8 | */ 9 | 10 | var duration = 200 11 | var UNICODE_UP_DOWN_ARROW = ['\u2b0d', '\u21d5'][1] 12 | var UNICODE_NO_BREAK_SPACE = '\u00A0' 13 | 14 | var palette = { 15 | lineGray: 'rgb(166, 166, 166)', 16 | layoutGray: 'rgb(231, 231, 233)', 17 | magenta: 'rgb(226, 60, 180)', 18 | gradePalette: [0.06, 0.1, 0.16, 0.22, 0.3] 19 | } 20 | 21 | var benchmarkStyleSet = { 22 | "District": {stroke: 'black', fill: 'black', opacity: 0.3, 'stroke-width': 1}, 23 | "School": {stroke: 'black', fill: 'black', opacity: 0.5, 'stroke-width': 1}, 24 | "Other Classes": {stroke: 'black', fill: 'black', opacity: 0.5, 'stroke-width': 2}, 25 | "This Class": {stroke: palette.magenta, fill: palette.magenta, 'stroke-width': 1.5} 26 | } 27 | 28 | function renderImpactfulPoint(root) { 29 | bind(root, 'impactfulPointAes', 'circle') 30 | .entered 31 | .attr({ 32 | r: 3.75, 33 | fill: palette.magenta, 34 | stroke: 'black' 35 | }) 36 | } 37 | 38 | function renderPointLegendGrayBackground(root) { 39 | bind(root, 'markerBar', 'rect') 40 | .entered 41 | .attr({ 42 | width: 10, 43 | height: 18, 44 | x: -5, 45 | y: -9, 46 | stroke: 'none', 47 | fill: 'lightgrey' 48 | }) 49 | } 50 | 51 | function complexStyler(styleSet) { 52 | // fixme generalise the attributes via just iterating over the key 53 | return function(selection) { 54 | selection.attr({ 55 | stroke: function(d) {return styleSet[d.key].stroke}, 56 | fill: function(d) {return styleSet[d.key].fill}, 57 | opacity: function(d) {return styleSet[d.key].opacity}, 58 | 'stroke-width': function(d) {return styleSet[d.key]['stroke-width']} 59 | }) 60 | } 61 | } 62 | 63 | function benchmarkStyler() { 64 | return complexStyler(benchmarkStyleSet) 65 | } 66 | 67 | var layout, l 68 | 69 | function calculateGlobals() { 70 | 71 | layout = { 72 | mainRectangleTop: 38 73 | } 74 | 75 | l = { 76 | 77 | mainRectangleLeft: 48, 78 | mainRectangleWidth: 1260, 79 | 80 | fontFamily: 'Arial, sans-serif', 81 | basicFontSize: 14, 82 | 83 | titleTextColor: 'rgb(96, 96, 96)', 84 | 85 | mainTitleAnchor: 'beginning', 86 | mainTitlePosition: [0, -10], 87 | mainTitleFontSize: 18, 88 | mainTitleText: 'First Period: Algebra 1', 89 | mainTitleLetterSpacing: 0, 90 | 91 | mainTitleDecoratorColor: palette.layoutGray, 92 | mainTitleDecoratorHeight: layout.mainRectangleTop, 93 | mainTitleDecoratorY: -layout.mainRectangleTop, 94 | mainTitleDecoratorStrokeWidth: 0, 95 | 96 | groupTitleFontSize: 18 97 | } 98 | } 99 | 100 | function renderPetiteHeader(root, vm, fontSize) { 101 | bind(root, 'petiteColumnHeader') 102 | .entered 103 | .attr('transform', translateY(-25)) 104 | bind(root['petiteColumnHeader'], 'group', 'g', vm) 105 | bind(root['petiteColumnHeader']['group'], 'helpText', 'title') 106 | .entered 107 | .text(function(d) { 108 | var variable = findWhere('petiteHeaderAlias', d.key)(dashboardVariables) 109 | return variable ? variable.helpText : '' 110 | }) 111 | bind(root['petiteColumnHeader']['group'], 'headerText', 'text') 112 | .text(function(d) {return sortedByThis('petiteHeaderAlias', d.key) ? d.key + '' + UNICODE_UP_DOWN_ARROW : d.key}) 113 | .entered 114 | .classed('interactive', property('interactive')) 115 | .on('mousedown', function(d) {setPetiteHeaderTableSortOrder(d.key, d)}) 116 | .on('mouseup', resetTableSortOrder) 117 | .attr({ 118 | x: value 119 | }) 120 | .attr('opacity', 1) 121 | root['petiteColumnHeader']['group']['headerText'] 122 | .entered 123 | .filter(property('fontSize')) 124 | .attr('font-size', property('fontSize')) 125 | } 126 | 127 | function render() { 128 | 129 | function renderCross(root) { 130 | bind(root, 'cross', 'path') 131 | .entered 132 | .attr({ 133 | stroke: 'black', 134 | 'stroke-width': 0.7, 135 | d: function () { 136 | var xo = s.rowPitch * 0.125 137 | var yo = xo * 1.8 138 | return [ 139 | 'M', -xo, -yo, 'L', xo, yo, 140 | 'M', -xo, yo, 'L', xo, -yo 141 | ].join(' ') 142 | } 143 | }) 144 | } 145 | 146 | function renderMarkerBar(root) { 147 | bind(root, 'markerBar', 'line') 148 | .entered 149 | .attr({ 150 | y1: - s.rowPitch * 0.25, 151 | y2: s.rowPitch * 0.25, 152 | stroke: 'white', 153 | 'stroke-width': 2 154 | }) 155 | } 156 | 157 | function renderMeanLine(root) { 158 | bind(root, 'mean', 'line') 159 | .entered 160 | .attr({ 161 | stroke: 'black' 162 | }) 163 | .attr({ 164 | y1: -s.rowPitch / 4, 165 | y2: s.rowPitch / 4 166 | }) 167 | } 168 | 169 | function renderGradeHistogram(root, valueAccessor) { 170 | 171 | var xScale = s.histogramGradeScale 172 | var yScale = null; 173 | 174 | bind(root, 'gradeHistogram', 'g', function(d) { 175 | var currentGrades = keptStudentData(d).map(valueAccessor) 176 | var countsPlusOne = countBy(currentGrades.concat(s.gradesDomain)) 177 | var counts = pairs(countsPlusOne).sort(tupleSorter).map(function(t) {return {key: t[0], value: t[1] - 1}}).reverse() 178 | var domainBase = 0 179 | yScale = d3.scale.linear().domain([domainBase, counts.reduce(function(prev, next) {return Math.max(prev, next.value)}, 0)]).range(s.histogramStudentCountScale.range()) 180 | 181 | return [{key: 0, value: counts}] 182 | }) 183 | .entered 184 | .attr('transform', translateX(33)) 185 | 186 | bind(root['gradeHistogram'], 'xAxis') 187 | .entered 188 | .call(d3.svg.axis().scale(xScale).orient('bottom')) 189 | bind(root['gradeHistogram'], 'yAxis') 190 | .transition().duration(duration) 191 | .call(d3.svg.axis().scale(yScale).orient('left').ticks(3).tickFormat(function(d) {return d === Math.round(d) ? d : ''})) 192 | 193 | // style the axis ticks 194 | root.selectAll('g.tick text') 195 | .attr('font-size', 10) 196 | 197 | bind(root['gradeHistogram'], 'bars', 'g', property('value')) 198 | .entered 199 | .attr({transform: translateX(function(d) {return xScale(d.key)})}) 200 | bind(root['gradeHistogram'], 'lineAes', 'path') 201 | .entered 202 | .attr({ 203 | stroke: palette.magenta, 204 | 'stroke-width': 1.5 205 | }) 206 | root['gradeHistogram']['lineAes'] 207 | .transition().duration(duration) 208 | .attr({ 209 | d: function(d) { 210 | return d3.svg.line() 211 | .x(compose(xScale, key)) 212 | .y(compose(yScale, value)) 213 | .defined(always)(d.value) 214 | } 215 | }) 216 | } 217 | 218 | var distributionStyleSet = [ 219 | {key: "Grade", renderer: renderImpactfulPoint}, 220 | {key: "Prior", renderer: renderCross}, 221 | {key: "Goal", renderer: renderMarkerBar, backgroundRenderer: renderPointLegendGrayBackground} 222 | ] 223 | 224 | var lastAssignmentDistributionStyleSet = [ 225 | {key: "Grade", renderer: renderImpactfulPoint}, 226 | {key: "Average", renderer: renderMeanLine} 227 | ] 228 | 229 | var s = calculateScales() 230 | calculateGlobals() 231 | 232 | /** 233 | * Root 234 | */ 235 | 236 | var svgWidth = 1280 237 | var svgHeight = 1025 238 | 239 | root 240 | .style({ 241 | 'background-color': 'rgb(255, 255, 251)', 242 | width: '100%', 243 | height: '100%' 244 | }) 245 | .attr({viewBox: [0, 0, svgWidth, svgHeight].join(' ')}) 246 | 247 | var dashboard = bind(root, 'dashboard', 'g', [dashboardData]) 248 | dashboard 249 | .entered 250 | .attr({ 251 | 'font-family': l.fontFamily 252 | }) 253 | .attr({ 254 | 'font-size': l.basicFontSize 255 | }) 256 | 257 | /** 258 | * Main dashboard rectangle 259 | */ 260 | 261 | var mainRectangleTop = bind(dashboard, 'mainRectangleTop', 'g') 262 | mainRectangleTop 263 | .entered 264 | .attr({transform: translateY(layout.mainRectangleTop)}) 265 | 266 | var mainRectangleTopLeft = bind(mainRectangleTop, 'mainRectangleTop', 'g') 267 | mainRectangleTopLeft 268 | .entered 269 | .attr({transform: translateX(l.mainRectangleLeft)}) 270 | 271 | 272 | /** 273 | * Dashboard title and date 274 | */ 275 | 276 | bind(mainRectangleTopLeft, 'mainRectangleTopBar', 'rect') 277 | .entered 278 | .attr({ 279 | width: l.mainRectangleWidth + l.mainRectangleLeft - 24, 280 | height: l.mainTitleDecoratorHeight, 281 | x: -l.mainRectangleLeft - 2, 282 | y: l.mainTitleDecoratorY, 283 | stroke: l.mainTitleDecoratorColor, 284 | fill: l.mainTitleDecoratorColor, 285 | 'stroke-width': l.mainTitleDecoratorStrokeWidth 286 | }) 287 | 288 | var topOfRows = 45 289 | var bottomOfRows = 896 290 | var bottomOfReport = 986 291 | var leftOfColumns = -l.mainRectangleLeft 292 | var rightOfColumns = 1280 + leftOfColumns 293 | 294 | bind(mainRectangleTopLeft, 'verticalGridBars', 'line', [ 295 | {key: 'student', value: leftOfColumns , size: 2}, 296 | {key: 'special', value: 143, size: 1}, 297 | {key: 'grade', value: 194, size: 2}, 298 | {key: 'assignments', value: 392, size: 2}, 299 | {key: 'lastLeft', value: 560, size: 1}, 300 | {key: 'lastMiddle', value: 608, size: 0}, 301 | {key: 'lastRight', value: 658, size: 1}, 302 | {key: 'assessments', value: 726, size: 2}, 303 | {key: 'attendance', value: 868, size: 2}, 304 | {key: 'nowLine', value: 1055, size: 0}, 305 | {key: 'behavior', value: 1115, size: 2}, 306 | {key: 'rightEdge', value: rightOfColumns, size: 2} 307 | ]).entered 308 | .attr({ 309 | x1: value, 310 | x2: value, 311 | y1: function(d) {return d.size === 2 ? 0 : topOfRows}, 312 | y2: function(d) {return (d.size === 2 || d.key === 'nowLine' ? bottomOfReport : bottomOfRows)}, 313 | 'stroke-width': function(d) {return [0.8, 2, 4][d.size]}, 314 | stroke: palette.layoutGray 315 | }) 316 | 317 | bind(mainRectangleTopLeft, 'horizontalGridBars', 'line', [ 318 | {key: 'topOfRows', value: topOfRows , size: 2}, 319 | {key: 'bottomOfRows', value: bottomOfRows, size: 2}, 320 | {key: 'bottomOfReport', value: bottomOfReport, size: 1} 321 | ]).entered 322 | .attr({ 323 | y1: value, 324 | y2: value, 325 | x1: leftOfColumns, 326 | x2: rightOfColumns, 327 | 'stroke-width': function(d) {return [1, 2, 4][d.size]}, 328 | stroke: palette.layoutGray 329 | }) 330 | 331 | var mainTitle = bind(mainRectangleTopLeft, 'mainTitle') 332 | mainTitle 333 | .entered 334 | .attr({transform: translate.apply(null, l.mainTitlePosition)}) 335 | 336 | bind(mainTitle, 'mainTitleText', 'text') 337 | .entered 338 | .text(l.mainTitleText) 339 | .attr({ 340 | fill: l.titleTextColor, 341 | 'font-size': l.mainTitleFontSize, 342 | 'text-anchor': l.mainTitleAnchor, 343 | 'letter-spacing': l.mainTitleLetterSpacing 344 | }) 345 | 346 | 347 | /** 348 | * Dashboard date 349 | */ 350 | 351 | var dateBlock = bind(mainRectangleTopLeft, 'dateBlock') 352 | dateBlock 353 | .entered 354 | .attr({transform: translate(501, -10)}) 355 | 356 | bind(dateBlock, 'dateText', 'text') 357 | .entered 358 | .text(('As of ') + 'May 1, 2012' + (UNICODE_NO_BREAK_SPACE + UNICODE_NO_BREAK_SPACE + '(80% complete)')) 359 | .attr({ 360 | x: 1, 361 | 'font-size': 14, 362 | fill: l.titleTextColor 363 | }) 364 | 365 | 366 | /** 367 | * Upper right help elements 368 | */ 369 | 370 | var mainRectangleTopRight = bind(mainRectangleTopLeft, 'mainRectangleTopRight', 'g') 371 | mainRectangleTopRight 372 | .entered 373 | .attr({transform: translateX(l.mainRectangleWidth - 18)}) 374 | 375 | var helpSet = bind(mainRectangleTopRight, 'helpSet') 376 | .entered 377 | .attr({transform: translateY(-18)}) 378 | 379 | var noteBlock = bind(helpSet, 'noteBlock') 380 | .entered 381 | .attr({transform: translateX(-70 )}) 382 | 383 | var helpButtonWidth = 84 384 | var helpButtonOffsetX = -30 385 | var helpButtonHeight = 28 386 | 387 | var helpText = 388 | 'Row Sorting:\n' + 389 | '\u2022 Hover over headers for help text\n' + 390 | '\u2022 Click and hold header for temporary sorting\n\n' + 391 | 'Row Selections:\n' + 392 | '\u2022 Click and brush over rows\n' + 393 | '\u2022 Control/Command key for multiple selections\n\n' + 394 | '\u00a9 Design: Stephen Few, code: Robert Monfera\n' + 395 | 'Data visualization library: d3.js from M. Bostock' 396 | 397 | var helpButton = bind(noteBlock, 'helpButton') 398 | helpButton 399 | .entered 400 | .attr({transform: translateX(helpButtonOffsetX + helpButtonWidth / 2)}) 401 | .on('click', function() {window.alert(helpText)}) 402 | 403 | bind(helpButton, 'helpText', 'title') 404 | .entered 405 | .text(helpText) 406 | bind(helpButton, 'helpButtonRectangle', 'rect') 407 | .entered 408 | .attr({ 409 | fill: 'rgb(217, 217, 217)', 410 | stroke: 'rgb(167, 167, 167)', 411 | 'stroke-width': 2 412 | }) 413 | .attr({ 414 | x: - helpButtonWidth / 2, 415 | y: - helpButtonHeight / 2, 416 | rx: 15, 417 | ry: 15, 418 | width: helpButtonWidth, 419 | height: helpButtonHeight 420 | }) 421 | 422 | bind(helpButton, 'helpButtonText', 'text') 423 | .entered 424 | .text('Help') 425 | .attr({ 426 | x: 0, 427 | y: '0.35em', 428 | 'text-anchor': 'middle', 429 | 'letter-spacing': 0.5, 430 | fill: 'rgb(96, 96, 96)' 431 | }) 432 | .attr({ 433 | 'font-size': 14 434 | }) 435 | 436 | 437 | /** 438 | * Headers 439 | */ 440 | 441 | function renderHeader(root, text, sortedByThis, aggregate) { 442 | 443 | var header = bind(root, 'header') 444 | 445 | bind(header, 'headerTitle', 'text') 446 | .text(sortedByThis ? text + '' + UNICODE_UP_DOWN_ARROW : text) 447 | .entered 448 | .attr({ 449 | y: -6 450 | }) 451 | .attr({ 452 | fill: l.titleTextColor, 453 | 'letter-spacing': 0, 454 | 'font-size': l.groupTitleFontSize, 455 | opacity: aggregate ? 0 : 1 456 | }) 457 | bind(header, 'helpText', 'title') 458 | .entered 459 | .text(function() { 460 | var variable = findWhere('headerAlias', text)(dashboardVariables) 461 | return variable ? variable.helpText : '' 462 | }) 463 | 464 | return header 465 | } 466 | 467 | function renderGroupHolder(selection, className, title, x, y, y2, aggregate) { 468 | 469 | var group = bind(selection, className) 470 | group 471 | .entered 472 | .attr('transform', translate(x, y)) 473 | 474 | var groupHeader = renderHeader(group, title, sortedByThis('groupAlias', className), aggregate) 475 | groupHeader 476 | .entered 477 | .on('mousedown', setGroupHeaderTableSortOrder.bind(0, className)) 478 | .on('mouseup', resetTableSortOrder) 479 | 480 | var fullClassName = className + '_contents' 481 | 482 | bind(group, fullClassName) 483 | .entered 484 | .classed('groupContents', true) 485 | .attr('transform', translateY(y2)) 486 | 487 | return { 488 | group: group[fullClassName], 489 | className: className, 490 | legendGroup: groupHeader 491 | } 492 | } 493 | 494 | /** 495 | * Contents 496 | */ 497 | 498 | var contents = bind(mainRectangleTopLeft, 'contents') 499 | contents 500 | .entered 501 | .classed('globalContentPlacementY', true) 502 | .attr('transform', translateY(24.5)) 503 | 504 | 505 | /** 506 | * Top header rows 507 | */ 508 | 509 | var topGroups = bind(contents, 'topGroups') 510 | 511 | var courseGradesGroupX = 204.5 512 | var courseGradeBulletOffsetX = 42 513 | var assignmentScoresGroupX = 408.5 514 | var topGroupContentsY = 38 515 | var tserOffsetX = 858 516 | var behaviorOffsetX = 1140 517 | var classAttendanceX = tserOffsetX + 300 518 | var classAssessmentGroupX = 747.5 519 | var namesGroup = renderGroupHolder(topGroups, 'namesGroup', 'Student', 0, 0, topGroupContentsY) 520 | var courseGradesGroup = renderGroupHolder(topGroups, 'courseGradesGroup', 'Overall Course Grade' , courseGradesGroupX, 0, topGroupContentsY) 521 | var classAttendanceGroup = renderGroupHolder(topGroups, 'classAttendanceGroup', 'Attendance', classAttendanceX + (90 - 300), 0, topGroupContentsY) 522 | var assignmentScoresGroup = renderGroupHolder(topGroups, 'assignmentScoresGroup', 'Assignments', classAssessmentGroupX - 230, 0, topGroupContentsY) 523 | var assessmentScoresGroup = renderGroupHolder(topGroups, 'assessmentScoresGroup', 'Assessments', classAssessmentGroupX, 0, topGroupContentsY) 524 | var behaviorGroup = renderGroupHolder(topGroups, 'behaviorGroup', 'Behavior', behaviorOffsetX, 0, topGroupContentsY) 525 | 526 | /** 527 | * Side header rows 528 | */ 529 | 530 | var aggregateGroupY = 926 531 | 532 | var sideGroups = bind(contents, 'sideGroups') 533 | sideGroups 534 | .entered 535 | .attr('transform', translate(0, aggregateGroupY)) 536 | 537 | var distributionsGroup = renderGroupHolder(sideGroups, 'distributionsGroup', 'Grade and Assignment Score Distribution', 204, 0, 0, true) 538 | var tserGroup = renderGroupHolder(sideGroups, 'tserGroup', 'Attendance Problem Counts', tserOffsetX, 54 - 164, 90, true) 539 | var benchmarkGroup = renderGroupHolder(sideGroups.entered, 'benchmarkGroup', 'Standardized Math Assessment Score Distribution', 730, -40 - 164, 205, true) 540 | var medianGroup = renderGroupHolder(sideGroups.entered, 'medianGroup', 'Standardized Math Assessment Median Score', 0, 722, 34, true) 541 | var behaviorAggregateGroup = renderGroupHolder(sideGroups.entered, 'behaviorAggregateGroup', '', 0, -42, 0, true) 542 | var assignmentScoresAggregateGroup = renderGroupHolder(sideGroups, 'assignmentScoresAggregateGroup', '', 0, -21, 0, true) 543 | 544 | 545 | /** 546 | * Legends 547 | */ 548 | 549 | var legendRowPitch = 10 550 | 551 | function renderLegend(text, circle) { 552 | return function renderThisLegend(root) { 553 | var referenceSize = 8 554 | if(!circle) 555 | bind(root, 'legendIcon', 'line') 556 | .entered 557 | .attr({ 558 | x2: referenceSize, 559 | transform: translateY(-2) 560 | }) 561 | bind(root, 'legendText', 'text') 562 | .text(sortedByThis('legendAlias', text) ? text + ' ' + UNICODE_UP_DOWN_ARROW : text) 563 | .entered 564 | .attr({ 565 | x: referenceSize * 2, 566 | y: '0.25em' 567 | }) 568 | bind(root, 'legendCaptureZone', 'rect') 569 | .entered 570 | .attr({ 571 | x: - referenceSize / 2, 572 | y: - legendRowPitch / 2, 573 | height: legendRowPitch, 574 | width: 100, 575 | transform: translateY(-1.5) 576 | }) 577 | .on('click', setGroupLegendTableSortOrder.bind(0, text)) 578 | } 579 | } 580 | 581 | function offsetLegends(selection) {selection.entered.attr('transform', translate(2, -130))} 582 | function offsetLegends_benchmark(selection) {selection.entered.attr('transform', translate(2, 220))} 583 | function offsetLegends_specialEnglish(selection) {selection.entered.attr('transform', translate(2, aggregateGroupY - 12))} 584 | function offsetSecondRow(selection) {selection.attr('transform', translateY(legendRowPitch))} 585 | function offsetSecondColumn_benchmark(selection) {selection.attr('transform', translateX(84))} 586 | 587 | function renderDistributionLegends(root, data) { 588 | 589 | bind(root, 'groupLegends') 590 | .entered 591 | .attr('opacity', 1) 592 | bind(root['groupLegends'], 'legend', 'g', constant(data)) 593 | .entered 594 | .attr('transform', translate(154, function(d, i) {return i * 20 - 30})) 595 | bind(root['groupLegends']['legend'], 'legendText', 'text') 596 | .entered 597 | .text(key) 598 | bind(root['groupLegends']['legend'], 'legendBackground') 599 | .entered 600 | .attr('transform', translate(-8, -4)) 601 | .each(function(d) {if(d.backgroundRenderer) d.backgroundRenderer(d3.select(this))}) 602 | bind(root['groupLegends']['legend'], 'legendMarker') 603 | .entered 604 | .attr('transform', translate(-8, -4)) 605 | .each(function(d) {d.renderer(d3.select(this))}) 606 | 607 | } 608 | 609 | renderDistributionLegends(distributionsGroup.legendGroup, distributionStyleSet) 610 | 611 | ;(function namesGroupLegends(group) { 612 | 613 | var root = group.legendGroup 614 | 615 | bind(root, 'groupLegends').call(offsetLegends_specialEnglish) 616 | 617 | bind(root['groupLegends'], 'firstRow') 618 | bind(root['groupLegends']['firstRow'], 'special').call(renderLegend('S = Special Ed student', true)).entered.classed('firstColumn', true) 619 | 620 | bind(root['groupLegends'], 'secondRow').entered.call(offsetSecondRow) 621 | bind(root['groupLegends']['secondRow'], 'english').call(renderLegend('E = English language deficiency', true)).entered.classed('firstColumn', true) 622 | 623 | })(namesGroup) 624 | 625 | ;(function assignmentScoreLegends(group) { 626 | 627 | var root = group.legendGroup 628 | 629 | bind(root, 'groupLegends').call(offsetLegends) 630 | 631 | // fixme merge some properties (interactive, helpText) into the variable Model 632 | renderPetiteHeader(group.group, [ 633 | {key: 'YTD', value: -90, interactive: true}, 634 | {key: 'Spread', value: -21, interactive: true}, 635 | {key: '50%', value: 33, fontSize: 10}, 636 | {key: 'Last', value: 73, interactive: true}, 637 | {key: '100%', value: 125, fontSize: 10}, 638 | {key: 'Late', value: 160, interactive: true} 639 | ]) 640 | 641 | })(assignmentScoresGroup) 642 | 643 | ;(function assessmentLegends(root) { 644 | 645 | renderPetiteHeader(root, [ 646 | {key: 'Last 5', value: 12, interactive: true}, 647 | {key: 'Last ', value: 73, interactive: true} 648 | ]) 649 | 650 | })(assessmentScoresGroup.group) 651 | 652 | ;(function behaviorLegends(root) { 653 | 654 | renderPetiteHeader(root, [ 655 | {key: 'Ref', value: 0, interactive: true}, 656 | {key: 'Det', value: 49, interactive: true} 657 | ]) 658 | 659 | })(behaviorGroup.group) 660 | 661 | ;(function benchmarkLegends(group) { 662 | 663 | var root = group.legendGroup 664 | 665 | bind(root, 'groupLegends').call(offsetLegends_benchmark) 666 | 667 | bind(root['groupLegends'], 'firstRow') 668 | bind(root['groupLegends']['firstRow'], 'current', 'g', [{key: 'This Class'}]).entered.classed('firstColumn', true).call(renderLegend('This Class')).selectAll('line').call(benchmarkStyler()) 669 | bind(root['groupLegends']['firstRow'], 'school', 'g', [{key: 'School'}]).entered.classed('secondColumn', true).call(offsetSecondColumn_benchmark).call(renderLegend('School')).selectAll('line').call(benchmarkStyler()) 670 | 671 | bind(root['groupLegends'], 'secondRow').entered.call(offsetSecondRow) 672 | bind(root['groupLegends']['secondRow'], 'other', 'g', [{key: 'Other Classes'}]).entered.classed('firstColumn', true).call(renderLegend('Other Classes')).selectAll('line').call(benchmarkStyler()) 673 | bind(root['groupLegends']['secondRow'], 'district', 'g', [{key: 'District'}]).entered.classed('secondColumn', true).call(offsetSecondColumn_benchmark).call(renderLegend('District')).selectAll('line').call(benchmarkStyler()) 674 | 675 | })(benchmarkGroup) 676 | 677 | 678 | /** 679 | * Rows 680 | */ 681 | 682 | var rowsRoot = namesGroup.group 683 | var rowSelection = bind(rowsRoot, 'row', 'g', makeRowData) 684 | var row = rowSelection.entered 685 | function rowTransform(d, i) {return translateY(i * s.rowPitch)()} 686 | 687 | row 688 | .attr({'transform': rowTransform}) 689 | rowSelection 690 | .transition().duration(duration * 4) 691 | .attr({'transform': rowTransform}) 692 | 693 | bind(rowSelection, 'rowBackground', 'rect') 694 | .attr('fill-opacity', function(d) {return dashboardSettings.table.studentSelection.selectedStudents[d.key] ? 0.05 : 0}) 695 | .entered 696 | .attr({ 697 | width: 1328 - 48, 698 | height: s.rowPitch, 699 | x: -46, 700 | y: - s.rowPitch / 2 + 0.5 701 | }) 702 | 703 | function renderBehaviorCounts(root) { 704 | bind(root, 'refLastFlag') 705 | .entered 706 | .attr('transform', translateX(behaviorOffsetX)) 707 | bind(root['refLastFlag'], 'text', 'text') 708 | .entered 709 | .text(function (d) {return d.pastReferralCount || ''}) 710 | .attr({ 711 | y: '0.5em', 712 | 'text-anchor': 'end', 713 | fill: palette.lineGray 714 | }) 715 | 716 | bind(root, 'refCurrentFlag') 717 | .entered 718 | .attr('transform', translateX(behaviorOffsetX + 20)) 719 | bind(root['refCurrentFlag'], 'text', 'text') 720 | .entered 721 | .text(function (d) {return d.currentReferralCount || ''}) 722 | .attr({ 723 | y: '0.5em', 724 | 'text-anchor': 'end', 725 | fill: palette.magenta 726 | }) 727 | 728 | bind(root, 'detLastFlag') 729 | .entered 730 | .attr('transform', translateX(behaviorOffsetX + 50)) 731 | bind(root['detLastFlag'], 'text', 'text') 732 | .entered 733 | .text(function (d) {return d.pastDetentionCount || ''}) 734 | .attr({ 735 | y: '0.5em', 736 | 'text-anchor': 'end', 737 | fill: palette.lineGray 738 | }) 739 | 740 | bind(root, 'detCurrentFlag') 741 | .entered 742 | .attr('transform', translateX(behaviorOffsetX + 70)) 743 | bind(root['detCurrentFlag'], 'text', 'text') 744 | .entered 745 | .text(function (d) {return d.currentDetentionCount || ''}) 746 | .attr({ 747 | y: '0.5em', 748 | 'text-anchor': 'end', 749 | fill: palette.magenta 750 | }) 751 | } 752 | 753 | ;(function renderAlphanumericsAndFlag(root) { 754 | 755 | bind(root, 'flag') 756 | .entered 757 | .classed('flagGroup', true) 758 | 759 | bind(root, 'problemFlag') 760 | .entered 761 | .classed('problemFlagGroup', true) 762 | bind(root['problemFlag'], 'flagAesthetic', 'circle') 763 | .entered 764 | .filter(function(d) {return d.problematic}) 765 | .attr({ 766 | fill: 'rgb(29, 174, 236)', 767 | cy: 1, 768 | r: 8.5 769 | }).attr({ 770 | transform: translateX(-20) 771 | }) 772 | 773 | bind(root, 'nameCell') 774 | .entered 775 | .classed('namesGroup', true) 776 | bind(root['nameCell'], 'nameCellText', 'text') 777 | .entered 778 | .text(key) 779 | .attr({ 780 | y: '0.5em' 781 | }) 782 | .attr({ 783 | 'transform': translateX(0) 784 | }) 785 | 786 | bind(root, 'currentCourseGrade') 787 | .entered 788 | .classed('currentCourseGradeGroup', true) 789 | .attr({ 790 | transform: translateX(210.5), 791 | fill: palette.magenta 792 | }) 793 | bind(root['currentCourseGrade'], 'currentCourseGradeText', 'text') 794 | .entered 795 | .attr({y: '0.5em'}) 796 | .text(function (d) {return d.grades.current}) 797 | 798 | bind(root, 'assignmentAverage') 799 | .entered 800 | .attr({ 801 | transform: translateX(358.5), 802 | fill: palette.lineGray 803 | }) 804 | bind(root['assignmentAverage'], 'assignmentAverageText', 'text') 805 | .entered 806 | .attr({y: '0.5em'}) 807 | .text(function(d) {return Math.round(100 * dashboardVariables.meanAssignmentScore.plucker(d))}) 808 | 809 | bind(root, 'lateAssignment') 810 | .entered 811 | .classed('lateAssignmentGroup', true) 812 | .attr('transform', translateX(700)) 813 | bind(root['lateAssignment'], 'lateAssignmentText', 'text') 814 | .entered 815 | .text(function (d) {return d.assignmentsLateCount || ""}) 816 | .attr({ 817 | y: '0.5em' 818 | }) 819 | .attr({ 820 | fill: palette.lineGray 821 | }) 822 | 823 | bind(root, 'lastAssignmentScore') 824 | .entered 825 | .classed('lastAssignmentScoresGroup', true) 826 | .attr('transform', translateX(650)) 827 | 828 | bind(root, 'lastAssessmentScore') 829 | .entered 830 | .attr('transform', translateX(834)) 831 | bind(root['lastAssessmentScore'], 'lastAssessmentScoreText', 'text') 832 | .entered 833 | .text(function (d) {return Math.round(100 * last(d.standardScores))}) 834 | .attr({ 835 | y: '0.5em' 836 | }) 837 | .attr({ 838 | fill: palette.lineGray 839 | }) 840 | 841 | bind(root, 'englishFlag') 842 | .entered 843 | .attr('transform', translateX(158)) 844 | bind(root['englishFlag'], 'englishFlagText', 'text') 845 | .entered 846 | .text(function (d) {return !d.english ? 'E' : ''}) 847 | .attr({ 848 | y: '0.5em' 849 | }) 850 | .attr({ 851 | fill: palette.lineGray 852 | }) 853 | 854 | bind(root, 'specialFlag') 855 | .entered 856 | .attr('transform', translateX(170)) 857 | bind(root['specialFlag'], 'specialFlagText', 'text') 858 | .entered 859 | .text(function (d) {return d.special ? 'S' : ''}) 860 | .attr({ 861 | y: '0.5em' 862 | }) 863 | .attr({ 864 | fill: palette.lineGray 865 | }) 866 | 867 | renderBehaviorCounts(root) 868 | 869 | })(row) 870 | 871 | 872 | /** 873 | * Row cells - composite 874 | */ 875 | 876 | /* Grades */ 877 | 878 | ;(function renderGrades(root) { 879 | bind(root, 'courseGradeBullet') 880 | .classed('courseGradesGroup', true) 881 | .entered 882 | .attr('transform', translateX(courseGradesGroupX + courseGradeBulletOffsetX)) 883 | bind(root['courseGradeBullet'], 'courseGradeBackground1', 'rect', s.gradesDomain.map(function(g) {return {key: g}})) 884 | .entered 885 | .attr({ 886 | x: function(d) {return s.gradeScale(d.key) - 10}, 887 | width: s.gradeScale.range()[1] - s.gradeScale.range()[0], 888 | height: s.rowBandRange, 889 | y: -s.rowBandRange / 2, 890 | opacity: function(d, i) {return palette.gradePalette[i]} 891 | }) 892 | bind(root['courseGradeBullet'], 'courseGradeTargetLine') 893 | .entered 894 | .call(renderMarkerBar) 895 | .attr('transform', translateX(function (d) {return s.gradeScale(d.grades.goal)})) 896 | 897 | bind(root['courseGradeBullet'], 'priorGradeGroup') 898 | .entered 899 | .call(renderCross) 900 | .attr({ 901 | transform: translateX(function(d) {return s.gradeScale(d.grades.previous)}) 902 | }) 903 | 904 | bind(root['courseGradeBullet'], 'courseGradeCurrentPoint') 905 | .entered 906 | .call(renderImpactfulPoint) 907 | .attr('transform', translateX(function (d) {return s.gradeOverlayScale(d.meanAssignmentScore)})) 908 | 909 | })(row) 910 | 911 | var assignmentBandLine = bandLine() 912 | .bands(s.assignmentBands) 913 | .valueAccessor(property('assignmentScores')) 914 | .pointStyleAccessor(s.assignmentOutlierScale) 915 | .xScaleOfBandLine(s.assignmentScoreTemporalScale) 916 | .xScaleOfSparkStrip(s.assignmentScoreTemporalScale2) 917 | .rScaleOfBandLine(s.bandLinePointRScale) 918 | .rScaleOfSparkStrip(s.sparkStripPointRScale) 919 | .yRange(s.assignmentScoreVerticalScale.range()) 920 | .yAxis(false) 921 | 922 | bind(row, 'assignmentScoresCell') 923 | .entered 924 | .attr('transform', translateX(assignmentScoresGroupX)) 925 | row['assignmentScoresCell'].entered.call(assignmentBandLine.renderBandLine) 926 | 927 | bind(row, 'assignmentScoresVerticalCell') 928 | .entered 929 | .attr('transform', translateX(assignmentScoresGroupX + 86)) 930 | ;(function renderAssignmentScoresVertical(root) { 931 | root.call(assignmentBandLine.renderSparkStrip) 932 | 933 | })(row['assignmentScoresVerticalCell'].entered) 934 | 935 | bind(row, 'assessmentScoresCell') 936 | .entered 937 | .attr('transform', translateX(classAssessmentGroupX)) 938 | var assessmentBandLine = bandLine() 939 | .bands(s.assessmentBands) 940 | .valueAccessor(property('standardScores')) 941 | .pointStyleAccessor(s.assessmentOutlierScale) 942 | .xScaleOfBandLine(s.assessmentScoreTemporalScale) 943 | .rScaleOfBandLine(s.bandLinePointRScale) 944 | .yRange(s.assessmentScoreScale.range()) 945 | .yAxis(false) 946 | ;(function renderAssessmentScores(root) { 947 | root.call(assessmentBandLine.renderBandLine) 948 | 949 | })(row['assessmentScoresCell'].entered) 950 | 951 | ;(function renderMeanAndLastAssignments(root) { 952 | 953 | root 954 | .entered 955 | .attr({transform: translateX(560)}) 956 | 957 | bind(root, 'meanMarker') 958 | .entered 959 | .call(renderMeanLine) 960 | .attr('transform', translateX(function (d) {return s.assignmentScoreHorizontalScale(d.meanAssignmentScore)})) 961 | 962 | bind(root, 'last', 'circle') 963 | .entered 964 | .attr({ 965 | r: 3.75, 966 | fill: palette.magenta 967 | }) 968 | .attr({ 969 | cx: function(d) {return s.assignmentScoreHorizontalScale(last(d.assignmentScores))} 970 | }) 971 | })(bind(row, 'meanAndLastAssignments')) 972 | 973 | ;(function renderAttendanceTser(root) { 974 | 975 | var xScale = s.temporalScale 976 | var yScale1 = s.absentScale 977 | var yScale2 = s.tardyScale 978 | 979 | root 980 | .entered 981 | .attr('transform', translateX(tserOffsetX + 33)) 982 | 983 | bind(root, 'chartLine', 'g', function(d) { 984 | return [d.absences, d.tardies] 985 | .map(function(eventSet, i) { 986 | return { 987 | key: ['Absent', 'Tardy'][i], 988 | value: eventSet } 989 | })}) 990 | bind(root['chartLine'], 'axis', 'line') 991 | .entered 992 | .attr({ 993 | stroke: 'lightgrey' 994 | }) 995 | .attr({ 996 | x1: xScale.range()[0], 997 | x2: xScale.range()[1] 998 | }) 999 | 1000 | bind(root['chartLine'], 'bar', 'line', function(d) {return d.value.map(function(e) {return {key: d.key + '|' + e, type: d.key, value: e}})}) 1001 | .entered 1002 | .attr({ 1003 | stroke: function(d) {return d.type === 'Absent' ? palette.magenta : 'grey'} 1004 | }) 1005 | .attr({ 1006 | x1: compose(xScale, property('value')), 1007 | x2: compose(xScale, property('value')), 1008 | y2: function(d) {return d.type === 'Absent' ? yScale1(1) : yScale2(1)} 1009 | }) 1010 | })(bind(row, 'attendanceTser')) 1011 | 1012 | ;(function renderDistributions(root) { 1013 | 1014 | renderGradeHistogram(root, compose(property('current'), property('grades'))) 1015 | 1016 | ;(function renderAssignmentDistributions() { 1017 | 1018 | bind(root, 'lastAssignmentDistributions') 1019 | .entered 1020 | .attr({ 1021 | transform: translateX(323) 1022 | }) 1023 | 1024 | renderGradeHistogram(root['lastAssignmentDistributions'], function(d) {return s.scoreToGrade(last(d.assignmentScores));}) 1025 | 1026 | renderDistributionLegends(root['lastAssignmentDistributions'], lastAssignmentDistributionStyleSet) 1027 | 1028 | })() 1029 | 1030 | })(distributionsGroup.group) 1031 | 1032 | ;(function renderTsers(root) { 1033 | 1034 | var xScale = s.temporalScale 1035 | var yScale1 = s.absentScale 1036 | var yScale2 = s.absentScale 1037 | 1038 | bind(root, 'chart') 1039 | bind(root['chart'], 'chartLine', 'g', function(d) { 1040 | return [d.Absences, d.Tardies] 1041 | .map(function(eventSet, i) { 1042 | var keptEventSet = eventSet.filter(function(event) {return keptStudentFilterFunction(event[0])}) 1043 | var sparseValues = pairs(countBy(keptEventSet, property(1))).map(function(tuple) {return [new Date(tuple[0]), tuple[1]]}) 1044 | .sort(tupleSorter) 1045 | var denseValues = [] 1046 | var baseDateValue = xScale.domain()[0].valueOf() 1047 | var now = (new Date('2012-05-01')).valueOf() 1048 | for(var index = 0, day = baseDateValue; day <= xScale.domain()[1].valueOf(); index++, day += 24 * 60 * 60 * 1000) { 1049 | denseValues[index] = [new Date(day), day <= now ? 0 : null] 1050 | } 1051 | sparseValues.forEach(function(tuple) { 1052 | denseValues[Math.round((tuple[0].valueOf() - baseDateValue) / (24 * 60 * 60 * 1000))][1] = tuple[1] 1053 | }) 1054 | 1055 | return { 1056 | key: ['Absent', 'Tardy'][i], 1057 | value: sparseValues, 1058 | denseValue: denseValues 1059 | } 1060 | })}) 1061 | .entered 1062 | .attr('transform', translate(33, function(d) {return d.key === 'Absent' ? 0 : 40.5})) 1063 | 1064 | var xAxis1 = d3.svg.axis() 1065 | .scale(xScale) 1066 | .orient('bottom') 1067 | .ticks(0) 1068 | var xAxis2 = d3.svg.axis() 1069 | .scale(xScale) 1070 | .orient('top') 1071 | .ticks(0) 1072 | .tickFormat(function() {return ""}) 1073 | bind(root['chart']['chartLine'], 'xAxis1') 1074 | root['chart']['chartLine']['xAxis1'] 1075 | .entered 1076 | .each(function(d) {if(d.key === 'Absent') xAxis1(d3.select(this)); else xAxis2(d3.select(this))}) 1077 | 1078 | var xAxisTop = d3.svg.axis() 1079 | .scale(xScale) 1080 | .orient('top') 1081 | .ticks(4) 1082 | .tickPadding(1) 1083 | //.tickFormat(function() {return ""}) 1084 | bind(root['chart']['chartLine'], 'xAxisTop') 1085 | .entered 1086 | .each(function(d) {if(d.key !== 'Absent') xAxisTop(d3.select(this))}) 1087 | .attr('transform', translateY(-930)) 1088 | .attr('opacity', 100) 1089 | 1090 | var yAxis1 = d3.svg.axis() 1091 | .scale(yScale1) 1092 | .orient('left') 1093 | .ticks(1) 1094 | var yAxis2 = d3.svg.axis() 1095 | .scale(yScale2) 1096 | .orient('left') 1097 | .ticks(1) 1098 | bind(root['chart']['chartLine'], 'yAxis') 1099 | root['chart']['chartLine']['yAxis'] 1100 | .entered 1101 | .each(function(d) {if(d.key === 'Absent') yAxis1(d3.select(this)); else yAxis2(d3.select(this))}) 1102 | 1103 | bind(root['chart']['chartLine']['yAxis'], 'axisLegend', 'text') 1104 | .entered 1105 | .text(key) 1106 | .attr({ 1107 | x: 4, 1108 | y: 37, 1109 | transform: function(d) {var scale = d.key === 'Absent' ? yScale1 : yScale2; return translateY(last(scale.range()))(d)} 1110 | }) 1111 | 1112 | bind(root['chart']['chartLine'], 'tserLine', 'path') 1113 | .entered 1114 | .attr({ 1115 | stroke: function(d) {return d.key === 'Absent' ? palette.magenta : 'grey'}, 1116 | 'stroke-width': 1.75 1117 | }) 1118 | 1119 | bind(root['chart']['chartLine'], 'tserLine', 'path') 1120 | .transition().duration(duration) 1121 | .attr({ 1122 | d: function(d) { 1123 | return d3.svg.line() 1124 | .x(compose(xScale, property(0))) 1125 | .y(compose(d.key === 'Absent' ? yScale1 : yScale2, property(1))) 1126 | .defined(function(n) {return n[1] !== null})(d.denseValue) 1127 | } 1128 | }) 1129 | 1130 | })(tserGroup.group) 1131 | 1132 | 1133 | ;(function renderBenchmarks(root) { 1134 | 1135 | var xScale = s.scoreBandScale 1136 | var yScale = s.studentRatioScale 1137 | 1138 | bind(root, 'chart') 1139 | .attr('transform', translateX(33)) 1140 | bind(root['chart'], 'chartLine', 'g', property('contextScores')) 1141 | 1142 | var xAxis = d3.svg.axis() 1143 | .scale(xScale) 1144 | .orient('bottom') 1145 | .tickFormat(function(d) {return d ? (10 * (d + 4) + 1) + '-' + 10 * (d + 5) + '%' : "<= 50%"}) 1146 | bind(root['chart'], 'xAxis') 1147 | 1148 | var yAxis = d3.svg.axis() 1149 | .scale(yScale) 1150 | .orient('left') 1151 | .ticks(3) 1152 | .tickFormat(function(d) {return Math.round(d * 100) + '%'}) 1153 | bind(root['chart'], 'yAxis') 1154 | root['chart']['yAxis'] 1155 | .call(yAxis) 1156 | . selectAll('.tick') 1157 | .attr('font-size', 10) 1158 | 1159 | bind(root['chart']['chartLine'], 'markedLine') 1160 | .entered 1161 | .call(benchmarkStyler()) 1162 | 1163 | bind(root['chart']['chartLine']['markedLine'], 'line', 'path') 1164 | .attr({d: function(d) { 1165 | var drawer = d3.svg.line() 1166 | .x(compose(xScale, property(0))) 1167 | .y(compose(yScale, property(1))) 1168 | .defined(always) 1169 | 1170 | return drawer(d.distribution.map(function(s, i) {return [i, s]})) 1171 | }}) 1172 | 1173 | })(benchmarkGroup.group.entered) 1174 | 1175 | ;(function renderMedians(root) { 1176 | 1177 | bind(root, 'tableColumn', 'g', property('contextScores')) 1178 | .attr({ 1179 | transform: translateX(function (d, i) { 1180 | return (i + 1) * 110 1181 | }) 1182 | }) 1183 | 1184 | bind(root['tableColumn'], 'header', 'text') 1185 | .attr({ 1186 | y: '0.5em', 1187 | transform: translateY(-18) 1188 | }) 1189 | .text(function (d) { 1190 | return d.key 1191 | }) 1192 | 1193 | bind(root['tableColumn'], 'value', 'text') 1194 | .attr({ 1195 | y: '0.5em' 1196 | }) 1197 | .text(function (d) { 1198 | return Math.round(100 * d.median) + '%' 1199 | }) 1200 | 1201 | })(medianGroup.group.entered) 1202 | 1203 | ;(function renderBehaviorCountTotals(root) { 1204 | bind(root, 'behaviorCountMetrics', 'g', function(d) { 1205 | var aggregate = function(key) { 1206 | return d['Student Data'].map(property(key)).reduce(add) 1207 | } 1208 | return [{ 1209 | key: 0, 1210 | pastReferralCount: aggregate('pastReferralCount'), 1211 | currentReferralCount: aggregate('currentReferralCount'), 1212 | pastDetentionCount: aggregate('pastDetentionCount'), 1213 | currentDetentionCount: aggregate('currentDetentionCount') 1214 | }] 1215 | }) 1216 | renderBehaviorCounts(root['behaviorCountMetrics']) 1217 | 1218 | bind(root['behaviorCountMetrics'], 'groupLegends') 1219 | bind(root['behaviorCountMetrics']['groupLegends'], 'legend1', 'text') 1220 | .entered 1221 | .attr('transform', translate(behaviorOffsetX + 10, 35)) 1222 | .text('Last item') 1223 | bind(root['behaviorCountMetrics']['groupLegends'], 'legend2', 'text') 1224 | .entered 1225 | .attr('transform', translate(behaviorOffsetX + 10, 50)) 1226 | .text('This item') 1227 | 1228 | })(behaviorAggregateGroup.group.entered) 1229 | 1230 | ;(function renderAssignmentScoresAggregates(root) { 1231 | bind(root, 'assignmentAggregateMetrics', 'g', function(d) { 1232 | var students = keptStudentData(d) 1233 | var scores = pluck('assignmentScores')(students) 1234 | var totalsRow = { 1235 | key: 'totalsRow', 1236 | assignmentScores: [ 1237 | d3.mean(pluck(0)(scores).filter(identity)), 1238 | d3.mean(pluck(1)(scores).filter(identity)), 1239 | d3.mean(pluck(2)(scores).filter(identity)), 1240 | d3.mean(pluck(3)(scores).filter(identity)), 1241 | d3.mean(pluck(4)(scores).filter(identity)) 1242 | ] 1243 | } 1244 | return [totalsRow] 1245 | }) 1246 | .entered 1247 | .attr('transform', translateX(408.5)) 1248 | .attr('opacity', 1) 1249 | 1250 | var aggregateAssignmentBandLine = bandLine() 1251 | .bands(s.assignmentBands) 1252 | .valueAccessor(property('assignmentScores')) 1253 | .pointStyleAccessor(s.assignmentOutlierScale) 1254 | .xScaleOfBandLine(s.assignmentScoreTemporalScale) 1255 | .rScaleOfBandLine(s.bandLinePointRScale) 1256 | .yRange(s.assignmentScoreVerticalScaleLarge.range()) 1257 | .yAxis(d3.svg.axis().orient('right').ticks(4).tickFormat(d3.format('%'))) 1258 | root['assignmentAggregateMetrics'].call(aggregateAssignmentBandLine.renderBandLine) 1259 | 1260 | bind(root['assignmentAggregateMetrics'], 'legendText', 'text') 1261 | .entered 1262 | .text('Average Scores') 1263 | .attr({dx: 2, dy: 36}) 1264 | 1265 | bind(row, 'rowCaptureZone', 'rect') 1266 | .on(rowInteractions) 1267 | .attr({ 1268 | width: 1328 - 48, 1269 | height: s.rowPitch, 1270 | x: -46, 1271 | y: - s.rowPitch / 2 + 0.5 1272 | }) 1273 | 1274 | })(assignmentScoresAggregateGroup.group) 1275 | } 1276 | --------------------------------------------------------------------------------