2 "#1f77b4","#bf7f0e","#4cb00c","#b62728","#9467bd","#bc664b","#b377c2","#0fbf6f","#bcbd22","#17beef",
3 "#1f6784","#8f7f0e","#4c800c","#862728","#54678d","#8c564b","#8377c2","#0f8f6f","#8c8d22","#178eef",
4 "#1f97d4","#ff7f0e","#4cf00c","#f62728","#c467fd","#fc764b","#f377c2","#0fff6f","#fcfd22","#17feef",
9 'push_rank': 'GH Pushes',
10 'star_rank': 'GH Stars',
16 'files': 'File count',
19 'syntax': 'Syntax Style',
20 'type_check': 'Type Discipline',
21 'author_name': 'Author',
23 const axisKeySet
= new Set(Object
.keys(axisMap
))
24 const colorKeySet
= new Set(['type_check', 'syntax', 'author_name'])
26 const perfSet
= new Set(['perf1', 'perf2', 'perf3'])
27 const invertSet
= new Set(['pull_rank', 'push_rank', 'star_rank', 'so_rank', 'perf1', 'perf2'])
28 const perfLogSet
= new Set(['perf1', 'perf2', 'sloc', 'files'])
48 function malExtent(data
, key
) {
49 let extent
= d3
.extent(Object
.values(data
), d
=> d
[key
])
50 // pad the bottom rank so it's not on the opposite axis line
51 if (key
.endsWith('_rank')) {
52 extent
[0] = 0.99 // Setting this to 1 breaks log scale render
53 extent
[extent
.length
-1] += 1
55 // Replace 0's with 0.01 to prevent divide by zero errors
56 if (extent
[0] === 0) { extent
[0] = 0.0001 }
57 if (extent
[extent
.length
-1] === 0) { extent
[extent
.length
-1] = 0.0001 }
58 // For rankings, perf1, and perf2 reverse the Axis range
59 if (invertSet
.has(key
)) {
65 function malScale(log
) {
66 return log
? d3
.scale
.log() : d3
.scale
.linear()
69 function malTickValues(key
, log
) {
70 if (log
&& perfSet
.has(key
)) {
71 return [1, 10, 100, 1000, 10000, 100000]
77 function malCircleSize(key
, min
, max
, val
) {
78 let size
= (val
|| 0.01) - (min
- 0.01)
79 if (invertSet
.has(key
)) {
80 size
= (max
+ 0.01) - size
82 // if (perfLogSet.has(key)) {
83 // size = Math.log(size)
85 // console.log(key, max, val, size)
91 // UI / Axis Data / query parameters
94 // Parser query string and update cfg map with valid config options
95 (function parseQuery(q
) {
96 const pairs
= (q
[0] === '?' ? q
.substr(1) : q
).split('&')
97 for (const [p1
, p2
] of pairs
.map(p
=> p
.split('='))) {
98 let k
= decodeURIComponent(p1
).toLowerCase()
99 let v
= p2
? decodeURIComponent(p2
) : true
100 if (v
in {"true":1,"1":1,"yes":1}) { v
= true }
101 if (v
in {"false":1,"0":1,"no":1}) { v
= false }
102 if (k
in cfg
&& (axisKeySet
.has(v
) || colorKeySet
.has(v
))) {
105 if ((new Set(['xlog', 'ylog'])).has(k
) && typeof v
=== 'boolean') {
111 // Generate the control buttons and set the checked elements based on
113 function ctlChange(evt
) {
114 if (new Set(['xlog', 'ylog']).has(evt
.target
.name
)) {
115 cfg
[evt
.target
.name
] = evt
.target
.checked
117 cfg
[evt
.target
.name
] = evt
.target
.value
119 const query
= Object
.entries(cfg
).map(([k
,v
]) => k
+ "=" + v
).join('&')
120 history
.pushState(null, '', '?' + query
)
123 for (let key
of ['ckey', 'xkey', 'ykey', 'skey']) {
124 const parent
= document
.getElementById(key
+ '-controls')
127 'xkey': Object
.assign({}, axisMap
, {'xlog': 'Log Scale'}),
128 'ykey': Object
.assign({}, axisMap
, {'ylog': 'Log Scale'}),
131 for (let [val
, name
] of Object
.entries(ctlMap
)) {
132 const log
= (new Set(['xlog', 'ylog']).has(val
)) ? val
: false
133 const ctl
= document
.createElement('input')
134 ctl
.class = 'selects'
135 ctl
.type
= log
? 'checkbox' : 'radio'
136 ctl
.name
= log
? log
: key
137 ctl
.value
= log
? true : val
138 if ((log
&& cfg
[val
] === true) || cfg
[key
] === val
) {
141 ctl
.addEventListener('change', ctlChange
)
142 parent
.appendChild(ctl
)
143 parent
.appendChild(document
.createTextNode(name
))
148 // Graph rendering / updating
151 function updateGraphData() {
156 const colorSet
= new Set(Object
.values(allData
).map(d
=> d
[cfg
.ckey
]))
157 const colorList
= Array
.from(colorSet
.values())
158 // empty the graphData without recreating it
159 while (graphData
.length
> 0) { graphData
.pop() }
160 graphData
.push(...colorList
.map(t
=> ({key
: t
, values
: []})))
161 for (let dir
of Object
.keys(allData
)) {
162 const impl
= allData
[dir
]
163 if (impl
[cfg
.xkey
] > xMax
) { xMax
= impl
[cfg
.xkey
] }
164 if (impl
[cfg
.ykey
] > yMax
) { yMax
= impl
[cfg
.ykey
] }
165 if (sMin
=== null) { sMin
= impl
[cfg
.skey
] }
166 if (impl
[cfg
.skey
] < sMin
) { sMin
= impl
[cfg
.skey
] }
167 if (impl
[cfg
.skey
] > sMax
) { sMax
= impl
[cfg
.skey
] }
169 for (let dir
of Object
.keys(allData
)) {
170 const impl
= allData
[dir
]
171 // Invert size for inverted data
172 graphData
[colorList
.indexOf(impl
[cfg
.ckey
])].values
.push({
173 x
: impl
[cfg
.xkey
] || 0,
174 y
: impl
[cfg
.ykey
] || 0,
175 size
: malCircleSize(cfg
.skey
, sMin
, sMax
, impl
[cfg
.skey
]),
182 // Update the axes domain, scale and tick values
183 chart
.xDomain(malExtent(allData
, cfg
.xkey
))
184 chart
.yDomain(malExtent(allData
, cfg
.ykey
))
185 chart
.xScale(malScale(cfg
.xlog
))
186 chart
.yScale(malScale(cfg
.ylog
))
187 chart
.xAxis
.tickValues(malTickValues(cfg
.xkey
, cfg
.xlog
))
188 chart
.yAxis
.tickValues(malTickValues(cfg
.ykey
, cfg
.ylog
))
189 chart
.xAxis
.axisLabel(axisMap
[cfg
.xkey
])
190 chart
.yAxis
.axisLabel(axisMap
[cfg
.ykey
])
193 d3
.select('#mal svg')
195 .transition().duration(350).ease('linear')
200 nv
.utils
.windowResize(chart
.update
)
203 nv
.addGraph(function() {
204 chart
= nv
.models
.scatterChart()
210 chart
.dispatch
.on('renderEnd', function() {
211 //console.log('render complete')
213 chart
.dispatch
.on('stateChange', function(e
) {
214 nv
.log('New State:', JSON
.stringify(e
))
216 chart
.tooltip
.contentGenerator(function(obj
) {
217 const i
= obj
.point
.impl
218 return '<h3>' + i
.name
+ '</h3>' +
219 '<ul class="impl-data">' +
220 '<li><b>Syntax Style</b>: ' + i
.syntax
+
221 '<li><b>Type Discipline</b>: ' + i
.type_check
+
222 '<li><b>GitHub</b>:' +
224 ' <li><b>PR Count</b>: ' + (i
.pull_count
|| 'unknown') +
225 ' <li><b>PR Rank</b>: ' + i
.pull_rank
+
226 ' <li><b>Push Count</b>: ' + (i
.push_count
|| 'unknown') +
227 ' <li><b>Push Rank</b>: ' + i
.push_rank
+
228 ' <li><b>Star Count</b>: ' + (i
.star_count
|| 'unknown') +
229 ' <li><b>Star Rank</b>: ' + i
.star_rank
+
231 '<li><b>StackOverflow</b>:' +
233 ' <li><b>Tag Count</b>: ' + (i
.so_count
|| 'unknown') +
234 ' <li><b>Tag Rank</b>: ' + i
.so_rank
+
237 '<li><b>Perf 1</b>: ' + i
.perf1
+ ' ms<br>' +
238 '<li><b>Perf 2</b>: ' + i
.perf2
+ ' ms<br>' +
239 '<li><b>Perf 3</b>: ' + i
.perf3
+ ' iters / 10 sec<br>' +
240 '<li><b>SLOC</b>: ' + i
.sloc
+ ' lines<br>' +
241 '<li><b>Author</b>: ' + i
.author_name
+ '<br>' +
242 ' ' + i
.author_url
.replace(/https?:\/\//, '') +
246 // Load and mangle the data
247 d3
.json("all_data.json", function (error
, data
) {
250 console
.log(`Filling in missing data attributes`)
251 const dataList
= Object
.values(allData
)
252 // leave a gap between ranked impls and those with no rank
254 const maxPullRank
= Math
.max(...dataList
.map(d
=> d
.pull_rank
))
255 const maxPushRank
= Math
.max(...dataList
.map(d
=> d
.push_rank
))
256 const maxStarRank
= Math
.max(...dataList
.map(d
=> d
.star_rank
))
257 const maxSORank
= Math
.max(...dataList
.map(d
=> d
.so_rank
))
258 const maxPerf1
= dataList
.reduce((a
, d
) => d
.perf1
> a
? d
.perf1
: a
, 0)
259 const maxPerf2
= dataList
.reduce((a
, d
) => d
.perf2
> a
? d
.perf1
: a
, 0)
260 for (let d
of dataList
) {
261 if (d
.pull_rank
=== null) {
262 d
.pull_rank
= maxPullRank
+ rankGap
263 console
.log(` set pull_rank to ${d.pull_rank} for ${d.dir}`)
265 if (d
.push_rank
=== null) {
266 d
.push_rank
= maxPushRank
+ rankGap
267 console
.log(` set push_rank to ${d.push_rank} for ${d.dir}`)
269 if (d
.star_rank
=== null) {
270 d
.star_rank
= maxStarRank
+ rankGap
271 console
.log(` set star_rank to ${d.star_rank} for ${d.dir}`)
273 if (d
.so_count
=== 0) {
274 d
.so_rank
= maxSORank
+ rankGap
275 console
.log(` set so_rank to ${d.so_rank} for ${d.dir}`)
277 if (d
.perf1
=== null) {
279 console
.log(` set perf1 to ${maxPerf1} for ${d.dir}`)
281 if (d
.perf2
=== null) {
283 console
.log(` set perf2 to ${maxPerf2} for ${d.dir}`)
287 console
.log(`Adjusting perf numbers to avoid 0`)
288 for (let d
of dataList
) {
289 if (d
.perf1
=== 0) { d
.perf1
= 0.9 }
290 if (d
.perf2
=== 0) { d
.perf2
= 0.9 }
291 if (d
.perf3
=== 0) { d
.perf3
= 0.01 }
294 // NOTE: TODO: major hack to workaround bug with switching
295 // to/from logarithmic mode. Seems to require at least one
296 // value to be less than 1 for it to work
297 allData
.rpython
.perf2
= 0.9