print functions as #<fn name>
[jackhill/mal.git] / docs / graph / graph_languages.js
1 const malColors = [
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",
5 ]
6
7 const axisMap = {
8 'pull_rank': 'GH PRs',
9 'push_rank': 'GH Pushes',
10 'star_rank': 'GH Stars',
11 'so_rank': 'SO Tags',
12 'perf1': 'Perf 1',
13 'perf2': 'Perf 2',
14 'perf3': 'Perf 3',
15 'sloc': 'SLOC size',
16 'files': 'File count',
17 }
18 const colorMap = {
19 'syntax': 'Syntax Style',
20 'type_check': 'Type Discipline',
21 'author_name': 'Author',
22 }
23 const axisKeySet = new Set(Object.keys(axisMap))
24 const colorKeySet = new Set(['type_check', 'syntax', 'author_name'])
25
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'])
29
30 let cfg = {
31 ckey: 'syntax',
32 xkey: 'push_rank',
33 ykey: 'perf3',
34 skey: 'sloc',
35
36 xlog: false,
37 ylog: true,
38 }
39
40 let allData
41 let graphData = []
42 let chart
43
44 //
45 // Util functions
46 //
47
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
54 }
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)) {
60 extent.reverse()
61 }
62 return extent
63 }
64
65 function malScale(log) {
66 return log ? d3.scale.log() : d3.scale.linear()
67 }
68
69 function malTickValues(key, log) {
70 if (log && perfSet.has(key)) {
71 return [1, 10, 100, 1000, 10000, 100000]
72 } else {
73 return null
74 }
75 }
76
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
81 }
82 // if (perfLogSet.has(key)) {
83 // size = Math.log(size)
84 // }
85 // console.log(key, max, val, size)
86 return size
87 }
88
89
90 //
91 // UI / Axis Data / query parameters
92 //
93
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))) {
103 cfg[k] = v
104 }
105 if ((new Set(['xlog', 'ylog'])).has(k) && typeof v === 'boolean') {
106 cfg[k] = v
107 }
108 }
109 })(location.search)
110
111 // Generate the control buttons and set the checked elements based on
112 // the cfg
113 function ctlChange(evt) {
114 if (new Set(['xlog', 'ylog']).has(evt.target.name)) {
115 cfg[evt.target.name] = evt.target.checked
116 } else {
117 cfg[evt.target.name] = evt.target.value
118 }
119 const query = Object.entries(cfg).map(([k,v]) => k + "=" + v).join('&')
120 history.pushState(null, '', '?' + query)
121 updateGraphData()
122 }
123 for (let key of ['ckey', 'xkey', 'ykey', 'skey']) {
124 const parent = document.getElementById(key + '-controls')
125 const ctlMap = ({
126 'ckey': colorMap,
127 'xkey': Object.assign({}, axisMap, {'xlog': 'Log Scale'}),
128 'ykey': Object.assign({}, axisMap, {'ylog': 'Log Scale'}),
129 'skey': axisMap,
130 })[key]
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) {
139 ctl.checked = true
140 }
141 ctl.addEventListener('change', ctlChange)
142 parent.appendChild(ctl)
143 parent.appendChild(document.createTextNode(name))
144 }
145 }
146
147 //
148 // Graph rendering / updating
149 //
150
151 function updateGraphData() {
152 let xMax = 0
153 let yMax = 0
154 let sMin = null
155 let sMax = null
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] }
168 }
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]),
176 shape: 'circle',
177 label: impl.name,
178 impl: impl,
179 })
180 }
181
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])
191
192 // Update the graph
193 d3.select('#mal svg')
194 .data([graphData])
195 .transition().duration(350).ease('linear')
196 .call(chart)
197
198 chart.update()
199
200 nv.utils.windowResize(chart.update)
201 }
202
203 nv.addGraph(function() {
204 chart = nv.models.scatterChart()
205 .showDistX(true)
206 .showDistY(true)
207 .showLabels(true)
208 .duration(300)
209 .color(malColors)
210 chart.dispatch.on('renderEnd', function() {
211 //console.log('render complete')
212 })
213 chart.dispatch.on('stateChange', function(e) {
214 nv.log('New State:', JSON.stringify(e))
215 })
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>:' +
223 ' <ul>' +
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 +
230 ' </ul>' +
231 '<li><b>StackOverflow</b>:' +
232 ' <ul>' +
233 ' <li><b>Tag Count</b>: ' + (i.so_count || 'unknown') +
234 ' <li><b>Tag Rank</b>: ' + i.so_rank +
235 ' </ul>' +
236 '<li><br>' +
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 '&nbsp; &nbsp; ' + i.author_url.replace(/https?:\/\//, '') +
243 '</ul>'
244 })
245
246 // Load and mangle the data
247 d3.json("all_data.json", function (error, data) {
248 allData = data
249
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
253 const rankGap = 10
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}`)
264 }
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}`)
268 }
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}`)
272 }
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}`)
276 }
277 if (d.perf1 === null) {
278 d.perf1 = maxPerf1
279 console.log(` set perf1 to ${maxPerf1} for ${d.dir}`)
280 }
281 if (d.perf2 === null) {
282 d.perf2 = maxPerf2
283 console.log(` set perf2 to ${maxPerf2} for ${d.dir}`)
284 }
285 }
286
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 }
292 }
293
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
298
299 updateGraphData()
300 })
301
302 return chart
303 })
304