├── 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 |
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 |
--------------------------------------------------------------------------------