Skip to content

Instantly share code, notes, and snippets.

@kanaka
Created March 27, 2019 18:16
Show Gist options
  • Save kanaka/4dc0ddb0312f9f329065c5f8b54367ed to your computer and use it in GitHub Desktop.
Save kanaka/4dc0ddb0312f9f329065c5f8b54367ed to your computer and use it in GitHub Desktop.
Test case for nvd3 labels not updating correctly
<html>
<head>
<meta charset="utf-8">
<link href="https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.8.6/nv.d3.min.css" rel="stylesheet" type="text/css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.2/d3.min.js" charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.8.6/nv.d3.min.js"></script>
<style>
text {
font: 12px sans-serif;
}
svg {
display: block;
}
#notes {
border: 2px solid #202020;
padding: 4px;
margin: 2px 20px 2px 10px;
width: 70%;
position: absolute;
background: #ffffff;
}
.controls {
}
#toggle {
padding: 2px;
margin-right: 10px;
}
#toggle button {
width: 7em;
color: #d04444;
}
#show {
display: none;
}
.impl-data, .impl-data ul {
text-align: left !important;
list-style: none;
padding: 0;
margin: 5;
}
.impl-data ul {
margin: 0 0 0 15;
}
#title {
text-align: center;
padding: 5px;
}
#mal {
height: 80%;
width: 100%;
}
html, body, #mal, svg {
margin: 0px;
padding: 0px;
}
</style>
</head>
<body class='with-3d-shadow with-transitions'>
<div>
<h2 id="title">Mal Implementation Stats</h2>
</div>
<div class="controls">
<table border=0>
<tr>
<th align=right>Color data:</th>
<td id='ckey-controls'></td>
</tr>
<tr>
<th align=right>X-Axis data:</th>
<td id='xkey-controls'></td>
</tr>
<tr>
<th align=right>Y-Axis data:</th>
<td id='ykey-controls'></td>
</tr>
<tr>
<th align=right>Circle size:</th>
<td id='skey-controls'></td>
</tr>
</table>
</div>
<div id="mal" class="chartWrap">
<svg></svg>
</div>
<script>
let allData = {
"coffee": {
"dir": "coffee",
"name": "CoffeeScript",
"syntax": "OTHER",
"type_check": "Dynamic",
"modes": [],
"perf1": 6,
"perf2": 27,
"perf3": 17703,
"pull_count": 179032,
"pull_rank": 19,
"push_count": 686417,
"push_rank": 20,
"star_count": 512339,
"star_rank": 17,
"sloc": 1073,
"files": 21,
"author_name": "Joel Martin",
"author_url": "https://github.com/kanaka",
"so_count": 9675,
"so_rank": 35,
"lloc": 0
},
"nasm": {
"dir": "nasm",
"name": "NASM",
"syntax": "OTHER",
"type_check": "OTHER",
"modes": [],
"perf1": 1,
"perf2": 2,
"perf3": 23900,
"pull_count": 9679,
"pull_rank": 43,
"push_count": 91017,
"push_rank": 39,
"star_count": 31339,
"star_rank": 38,
"sloc": 14483,
"files": 19,
"author_name": "Ben Dudson",
"author_url": "https://github.com/bendudson",
"so_count": 3420,
"so_rank": 47,
"lloc": 0
},
"rpython": {
"dir": "rpython",
"name": "RPython",
"syntax": "Python",
"type_check": "Static",
"modes": [],
"perf1": 0,
"perf2": 1,
"perf3": 69099,
"pull_count": null,
"pull_rank": null,
"push_count": null,
"push_rank": null,
"star_count": null,
"star_rank": null,
"sloc": 2102,
"files": 19,
"author_name": "Joel Martin",
"author_url": "https://github.com/kanaka",
"so_count": 64,
"so_rank": 66,
"lloc": 2096
}
}
const malColors = [
"#1f77b4","#bf7f0e","#4cb00c","#b62728","#9467bd","#bc664b","#b377c2","#0fbf6f","#bcbd22","#17beef",
"#1f6784","#8f7f0e","#4c800c","#862728","#54678d","#8c564b","#8377c2","#0f8f6f","#8c8d22","#178eef",
"#1f97d4","#ff7f0e","#4cf00c","#f62728","#c467fd","#fc764b","#f377c2","#0fff6f","#fcfd22","#17feef",
]
const axisMap = {
'pull_rank': 'GH PRs',
'push_rank': 'GH Pushes',
'star_rank': 'GH Stars',
'so_rank': 'SO Tags',
'perf1': 'Perf 1',
'perf2': 'Perf 2',
'perf3': 'Perf 3',
'sloc': 'SLOC size',
'files': 'File count',
}
const colorMap = {
'syntax': 'Syntax Style',
'type_check': 'Type Discipline',
'author_name': 'Author',
}
const axisKeySet = new Set(Object.keys(axisMap))
const colorKeySet = new Set(['type_check', 'syntax', 'author_name'])
const perfSet = new Set(['perf1', 'perf2', 'perf3'])
const invertSet = new Set(['pull_rank', 'push_rank', 'star_rank', 'so_rank', 'perf1', 'perf2'])
const perfLogSet = new Set(['perf1', 'perf2', 'sloc', 'files'])
let cfg = {
ckey: 'syntax',
xkey: 'push_rank',
ykey: 'perf3',
skey: 'sloc',
xlog: false,
ylog: true,
}
let graphData = []
let chart
//
// Util functions
//
function malExtent(data, key) {
let extent = d3.extent(Object.values(data), d => d[key])
// pad the bottom rank so it's not on the opposite axis line
if (key.endsWith('_rank')) {
extent[0] = 0.99 // Setting this to 1 breaks log scale render
extent[extent.length-1] += 1
}
// Replace 0's with 0.01 to prevent divide by zero errors
if (extent[0] === 0) { extent[0] = 0.0001 }
if (extent[extent.length-1] === 0) { extent[extent.length-1] = 0.0001 }
// For rankings, perf1, and perf2 reverse the Axis range
if (invertSet.has(key)) {
extent.reverse()
}
return extent
}
function malScale(log) {
return log ? d3.scale.log() : d3.scale.linear()
}
function malTickValues(key, log) {
if (log && perfSet.has(key)) {
return [1, 10, 100, 1000, 10000, 100000]
} else {
return null
}
}
function malCircleSize(key, min, max, val) {
let size = (val || 0.01) - (min - 0.01)
if (invertSet.has(key)) {
size = (max + 0.01) - size
}
return size
}
//
// UI / Axis Data / query parameters
//
// Parser query string and update cfg map with valid config options
(function parseQuery(q) {
const pairs = (q[0] === '?' ? q.substr(1) : q).split('&')
for (const [p1, p2] of pairs.map(p => p.split('='))) {
let k = decodeURIComponent(p1).toLowerCase()
let v = p2 ? decodeURIComponent(p2) : true
if (v in {"true":1,"1":1,"yes":1}) { v = true }
if (v in {"false":1,"0":1,"no":1}) { v = false }
if (k in cfg && (axisKeySet.has(v) || colorKeySet.has(v))) {
cfg[k] = v
}
if ((new Set(['xlog', 'ylog'])).has(k) && typeof v === 'boolean') {
cfg[k] = v
}
}
})(location.search)
// Generate the control buttons and set the checked elements based on
// the cfg
function ctlChange(evt) {
if (new Set(['xlog', 'ylog']).has(evt.target.name)) {
cfg[evt.target.name] = evt.target.checked
} else {
cfg[evt.target.name] = evt.target.value
}
const query = Object.entries(cfg).map(([k,v]) => k + "=" + v).join('&')
history.pushState(null, '', '?' + query)
updateGraphData()
}
for (let key of ['ckey', 'xkey', 'ykey', 'skey']) {
const parent = document.getElementById(key + '-controls')
const ctlMap = ({
'ckey': colorMap,
'xkey': Object.assign({}, axisMap, {'xlog': 'Log Scale'}),
'ykey': Object.assign({}, axisMap, {'ylog': 'Log Scale'}),
'skey': axisMap,
})[key]
for (let [val, name] of Object.entries(ctlMap)) {
const log = (new Set(['xlog', 'ylog']).has(val)) ? val : false
const ctl = document.createElement('input')
ctl.class = 'selects'
ctl.type = log ? 'checkbox' : 'radio'
ctl.name = log ? log : key
ctl.value = log ? true : val
if ((log && cfg[val] === true) || cfg[key] === val) {
ctl.checked = true
}
ctl.addEventListener('change', ctlChange)
parent.appendChild(ctl)
parent.appendChild(document.createTextNode(name))
}
}
//
// Graph rendering / updating
//
function updateGraphData() {
let xMax = 0
let yMax = 0
let sMin = null
let sMax = null
const colorSet = new Set(Object.values(allData).map(d => d[cfg.ckey]))
const colorList = Array.from(colorSet.values())
// empty the graphData without recreating it
while (graphData.length > 0) { graphData.pop() }
graphData.push(...colorList.map(t => ({key: t, values: []})))
for (let dir of Object.keys(allData)) {
const impl = allData[dir]
if (impl[cfg.xkey] > xMax) { xMax = impl[cfg.xkey] }
if (impl[cfg.ykey] > yMax) { yMax = impl[cfg.ykey] }
if (sMin === null) { sMin = impl[cfg.skey] }
if (impl[cfg.skey] < sMin) { sMin = impl[cfg.skey] }
if (impl[cfg.skey] > sMax) { sMax = impl[cfg.skey] }
}
for (let dir of Object.keys(allData)) {
const impl = allData[dir]
// Invert size for inverted data
graphData[colorList.indexOf(impl[cfg.ckey])].values.push({
x: impl[cfg.xkey] || 0,
y: impl[cfg.ykey] || 0,
size: malCircleSize(cfg.skey, sMin, sMax, impl[cfg.skey]),
shape: 'circle',
label: impl.name,
impl: impl,
})
}
// Update the axes domain, scale and tick values
chart.xDomain(malExtent(allData, cfg.xkey))
chart.yDomain(malExtent(allData, cfg.ykey))
chart.xScale(malScale(cfg.xlog))
chart.yScale(malScale(cfg.ylog))
chart.xAxis.tickValues(malTickValues(cfg.xkey, cfg.xlog))
chart.yAxis.tickValues(malTickValues(cfg.ykey, cfg.ylog))
chart.xAxis.axisLabel(axisMap[cfg.xkey])
chart.yAxis.axisLabel(axisMap[cfg.ykey])
// Update the graph
d3.select('#mal svg')
.data([graphData])
.transition().duration(350).ease('linear')
.call(chart)
chart.update()
nv.utils.windowResize(chart.update)
}
nv.addGraph(function() {
chart = nv.models.scatterChart()
.showDistX(true)
.showDistY(true)
.showLabels(true)
.duration(300)
.color(malColors)
chart.dispatch.on('renderEnd', function() {
//console.log('render complete')
})
chart.dispatch.on('stateChange', function(e) {
nv.log('New State:', JSON.stringify(e))
})
chart.tooltip.contentGenerator(function(obj) {
const i = obj.point.impl
return '<h3>' + i.name + '</h3>' +
'<ul class="impl-data">' +
'<li><b>Syntax Style</b>: ' + i.syntax + '<br>' +
'<li><b>Type Discipline</b>: ' + i.type_check + '<br>' +
'<li><b>GitHub</b>:' +
' <ul>' +
' <li><b>PR Count</b>: ' + (i.pull_count || 'unknown') + '<br>' +
' <li><b>PR Rank</b>: ' + i.pull_rank + '<br>' +
' <li><b>Push Count</b>: ' + (i.push_count || 'unknown') + '<br>' +
' <li><b>Push Rank</b>: ' + i.push_rank + '<br>' +
' <li><b>Star Count</b>: ' + (i.star_count || 'unknown') + '<br>' +
' <li><b>Star Rank</b>: ' + i.star_rank + '<br>' +
' </ul>' +
'<li><b>StackOverflow</b>:' +
' <ul>' +
' <li><b>Tag Count</b>: ' + (i.so_count || 'unknown') + '<br>' +
' <li><b>Tag Rank</b>: ' + i.so_rank + '<br>' +
' </ul>' +
'<li><br>' +
'<li><b>Perf 1</b>: ' + i.perf1 + ' ms<br>' +
'<li><b>Perf 2</b>: ' + i.perf2 + ' ms<br>' +
'<li><b>Perf 3</b>: ' + i.perf3 + ' iters / 10 sec<br>' +
'<li><b>SLOC</b>: ' + i.sloc + ' lines<br>' +
'<li><b>Author</b>: ' + i.author_name + '<br>' +
'&nbsp; &nbsp; ' + i.author_url.replace(/https?:\/\//, '') + '<br>' +
'</ul>'
})
// Load and mangle the data
console.log(`Filling in missing data attributes`)
const dataList = Object.values(allData)
// leave a gap between ranked impls and those with no rank
const rankGap = 10
const maxPullRank = Math.max(...dataList.map(d => d.pull_rank))
const maxPushRank = Math.max(...dataList.map(d => d.push_rank))
const maxStarRank = Math.max(...dataList.map(d => d.star_rank))
const maxSORank = Math.max(...dataList.map(d => d.so_rank))
const maxPerf1 = dataList.reduce((a, d) => d.perf1 > a ? d.perf1 : a, 0)
const maxPerf2 = dataList.reduce((a, d) => d.perf2 > a ? d.perf1 : a, 0)
for (let d of dataList) {
if (d.pull_rank === null) {
d.pull_rank = maxPullRank + rankGap
console.log(` set pull_rank to ${d.pull_rank} for ${d.dir}`)
}
if (d.push_rank === null) {
d.push_rank = maxPushRank + rankGap
console.log(` set push_rank to ${d.push_rank} for ${d.dir}`)
}
if (d.star_rank === null) {
d.star_rank = maxStarRank + rankGap
console.log(` set star_rank to ${d.star_rank} for ${d.dir}`)
}
if (d.so_count === 0) {
d.so_rank = maxSORank + rankGap
console.log(` set so_rank to ${d.so_rank} for ${d.dir}`)
}
if (d.perf1 === null) {
d.perf1 = maxPerf1
console.log(` set perf1 to ${maxPerf1} for ${d.dir}`)
}
if (d.perf2 === null) {
d.perf2 = maxPerf2
console.log(` set perf2 to ${maxPerf2} for ${d.dir}`)
}
}
console.log(`Adjusting perf numbers to avoid 0`)
for (let d of dataList) {
if (d.perf1 === 0) { d.perf1 = 0.9 }
if (d.perf2 === 0) { d.perf2 = 0.9 }
if (d.perf3 === 0) { d.perf3 = 0.01 }
}
// NOTE: TODO: major hack to workaround bug with switching
// to/from logarithmic mode. Seems to require at least one
// value to be less than 1 for it to work
allData.rpython.perf2 = 0.9
updateGraphData()
return chart
})
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment