From 739c62774aa458e1af1397ca5cf6f123f9d99b7d Mon Sep 17 00:00:00 2001 From: Aron Helser Date: Thu, 5 Jan 2017 14:39:21 -0500 Subject: [PATCH] fix(FieldSelector): add whisker/box plot option Show a whisker plot of the data by default, and let the user toggle to a histogram. Uses fewer svg/dom elements, allowing for larger lists of fields. Modify HistogramSelector example to show whiskers. Include script that generated whisker data from nba.csv. --- src/InfoViz/Native/FieldSelector/index.js | 228 +++- .../Native/FieldSelector/template.html | 6 - .../Native/HistogramSelector/example/index.js | 2 +- .../HistogramSelector/example/state2.json | 1185 +++++++++++++++++ style/InfoVizNative/FieldSelector.mcss | 45 +- tools/scripts/genWhiskers.py | 60 + 6 files changed, 1455 insertions(+), 71 deletions(-) delete mode 100644 src/InfoViz/Native/FieldSelector/template.html create mode 100644 src/InfoViz/Native/HistogramSelector/example/state2.json create mode 100644 tools/scripts/genWhiskers.py diff --git a/src/InfoViz/Native/FieldSelector/index.js b/src/InfoViz/Native/FieldSelector/index.js index 97141e241c..81f0a0d46a 100644 --- a/src/InfoViz/Native/FieldSelector/index.js +++ b/src/InfoViz/Native/FieldSelector/index.js @@ -2,7 +2,6 @@ import d3 from 'd3'; import style from 'PVWStyle/InfoVizNative/FieldSelector.mcss'; import CompositeClosureHelper from '../../../Common/Core/CompositeClosureHelper'; -import template from './template.html'; // ---------------------------------------------------------------------------- // Global @@ -42,21 +41,34 @@ function fieldSelector(publicAPI, model) { model.container = el; if (el) { - d3.select(model.container).html(template); - d3.select(model.container).select('.fieldSelector').classed(style.fieldSelector, true); + const table = d3.select(model.container).append('table').classed(style.fieldSelector, true); + const theadRow = table.append('thead').classed(style.thead, true).append('tr'); + theadRow.append('th').classed(style.jsFieldSelectorMode, true).append('i'); + theadRow.append('th').classed(style.jsFieldSelectorLabel, true); + table.append('tbody').classed(style.tbody, true); model.fieldShowHistogram = model.fieldShowHistogram && (model.provider.isA('Histogram1DProvider')); // append headers for histogram columns if (model.fieldShowHistogram) { - const header = d3.select(model.container).select('thead').select('tr'); - header.append('th').text('Min').classed(style.jsHistMin, true); - header.append('th').text('Histogram').classed(style.jsSparkline, true); - header.append('th').text('Max').classed(style.jsHistMax, true); + theadRow.append('th').text('Min').classed(style.jsHistMin, true); + const chartHeader = theadRow.append('th').classed(style.jsSparkline, true); + chartHeader.append('span').text('Histogram').classed(style.chartHeader, true); + chartHeader.append('i'); + theadRow.append('th').text('Max').classed(style.jsHistMax, true); } publicAPI.render(); } }; + const clickSelected = (d) => { + model.displayUnselected = !model.displayUnselected; + publicAPI.render(); + }; + const clickChart = (d) => { + model.showHist = !model.showHist; + publicAPI.render(); + }; + publicAPI.render = () => { if (!model.container) { return; @@ -65,14 +77,8 @@ function fieldSelector(publicAPI, model) { const legendSize = 15; // Apply style - d3.select(model.container).select('thead').classed(style.thead, true); - d3.select(model.container).select('tbody').classed(style.tbody, true); - d3.select(model.container) - .select('th.field-selector-mode') - .on('click', (d) => { - model.displayUnselected = !model.displayUnselected; - publicAPI.render(); - }) + d3.select(`.${style.jsFieldSelectorMode}`) + .on('click', clickSelected) .select('i') // apply class - 'false' should come first to not remove common base class. .classed(!model.displayUnselected ? style.allFieldsIcon : style.selectedFieldsIcon, false) @@ -83,14 +89,10 @@ function fieldSelector(publicAPI, model) { const totalNum = model.displayUnselected ? data.length : model.provider.getFieldNames().length; // Update header label - d3.select(model.container) - .select('th.field-selector-label') + d3.select(`.${style.jsFieldSelectorLabel}`) .style('text-align', 'left') .text(model.displayUnselected ? `Only Selected (${data.length} total)` : `Only Selected (${data.length} / ${totalNum} total)`) - .on('click', (d) => { - model.displayUnselected = !model.displayUnselected; - publicAPI.render(); - }); + .on('click', clickSelected); // test for too-long rows const hideMore = model.container.scrollWidth > model.container.clientWidth; @@ -120,30 +122,34 @@ function fieldSelector(publicAPI, model) { } } } - const header = d3.select(model.container).select('thead').select('tr'); + const header = d3.select(`.${style.jsTHead}`).select('tr'); header.selectAll(`.${style.jsHistMin}`) .style('display', hideField.minMax ? 'none' : null); - header.selectAll(`.${style.jsSparkline}`) + const chartHeader = header.selectAll(`.${style.jsSparkline}`) .style('display', hideField.hist ? 'none' : null); header.selectAll(`.${style.jsHistMax}`) .style('display', hideField.minMax ? 'none' : null); // Handle variables const variablesContainer = d3 - .select(model.container) - .select('tbody.fields') + .select(`.${style.jsTBody}`) .selectAll('tr') .data(data); variablesContainer.enter().append('tr'); variablesContainer.exit().remove(); + // track if any quantiles are available at all. + let foundQuantile = false; + // Apply on each data item function renderField(fieldName, index) { const field = model.provider.getField(fieldName); const fieldContainer = d3.select(this); let legendCell = fieldContainer.select(`.${style.jsLegend}`); let fieldCell = fieldContainer.select(`.${style.jsFieldName}`); + if (field.quantiles) foundQuantile = true; + // Apply style to row (selected/unselected) fieldContainer @@ -198,51 +204,132 @@ function fieldSelector(publicAPI, model) { // make sure our data is ready. If not, render will be called when loaded. const hobj = model.histograms ? model.histograms[fieldName] : null; - if (hobj) { + if (hobj || field.quantiles) { histCell .style('display', hideField.hist ? 'none' : null); // only do work if histogram is displayed. if (!hideField.hist) { - const cmax = 1.0 * d3.max(hobj.counts); - const hsize = hobj.counts.length; - const hdata = histCell.select('svg') - .selectAll(`.${style.jsHistRect}`).data(hobj.counts); - - hdata.enter().append('rect'); - // changes apply to both enter and update data join: - hdata - .attr('class', (d, i) => (i % 2 === 0 ? style.histRectEven : style.histRectOdd)) - .attr('pname', fieldName) - .attr('y', d => model.fieldHistHeight * (1.0 - (d / cmax))) - .attr('x', (d, i) => (model.fieldHistWidth / hsize) * i) - .attr('height', d => model.fieldHistHeight * (d / cmax)) - .attr('width', model.fieldHistWidth / hsize); - - hdata.exit().remove(); - - if (model.provider.isA('HistogramBinHoverProvider')) { - histCell.select('svg') - .on('mousemove', function inner(d, i) { - const mCoords = d3.mouse(this); - const binNum = Math.floor((mCoords[0] / model.fieldHistWidth) * hsize); - const state = {}; - state[fieldName] = [binNum]; - model.provider.setHoverState({ state }); - }) - .on('mouseout', (d, i) => { - const state = {}; - state[fieldName] = [-1]; - model.provider.setHoverState({ state }); - }); + // ignore whisker/histogram toggle if whisker data is unavailable + if (model.showHist || !field.quantiles) { + if (!model.lastShowHist) { + histCell.select('svg').selectAll('*').remove(); + } + const cmax = 1.0 * d3.max(hobj.counts); + const hsize = hobj.counts.length; + const hdata = histCell.select('svg') + .selectAll(`.${style.jsHistRect}`).data(hobj.counts); + + hdata.enter().append('rect'); + // changes apply to both enter and update data join: + hdata + .attr('class', (d, i) => (i % 2 === 0 ? style.histRectEven : style.histRectOdd)) + .attr('pname', fieldName) + .attr('y', d => model.fieldHistHeight * (1.0 - (d / cmax))) + .attr('x', (d, i) => (model.fieldHistWidth / hsize) * i) + .attr('height', d => model.fieldHistHeight * (d / cmax)) + .attr('width', model.fieldHistWidth / hsize); + + hdata.exit().remove(); + + if (model.provider.isA('HistogramBinHoverProvider')) { + histCell.select('svg') + .on('mousemove', function inner(d, i) { + const mCoords = d3.mouse(this); + const binNum = Math.floor((mCoords[0] / model.fieldHistWidth) * hsize); + const state = {}; + state[fieldName] = [binNum]; + model.provider.setHoverState({ state }); + }) + .on('mouseout', (d, i) => { + const state = {}; + state[fieldName] = [-1]; + model.provider.setHoverState({ state }); + }); + } + } else { + // whisker/box plot + // draw verical line for lowerWhisker + const svg = histCell.select('svg'); + svg.selectAll('line').remove(); + svg.selectAll('rect').remove(); + const xScale = d3.scale.linear() + // allows 1 pixel whiskers at the extremes to not be cut off. + .range([0.5, model.fieldHistWidth - 0.5]) + .domain(field.range); + const midline = model.fieldHistHeight * 0.5; + let lowerWhisker = 0; + let q1Val = 0; + let medianVal = 0; + let meanVal = 0; + let iqr = 0; + let upperWhisker = 0; + const bh = Math.floor(0.5 * (model.fieldHistHeight - 4)); + if (field.quantiles) { + lowerWhisker = field.quantiles[0]; + q1Val = field.quantiles[1]; + medianVal = field.quantiles[2]; + meanVal = field.mean; + iqr = field.quantiles[3] - q1Val; + upperWhisker = field.quantiles[4]; + } + + svg.append('line') + .attr('class', style.whisker) + .attr('x1', xScale(lowerWhisker)) + .attr('x2', xScale(lowerWhisker)) + .attr('y1', midline - bh) + .attr('y2', midline + bh); + + // draw vertical line for upperWhisker + svg.append('line') + .attr('class', style.whisker) + .attr('x1', xScale(upperWhisker)) + .attr('x2', xScale(upperWhisker)) + .attr('y1', midline - bh) + .attr('y2', midline + bh); + + // draw horizontal line from lowerWhisker to upperWhisker + svg.append('line') + .attr('class', style.whisker) + .attr('x1', xScale(lowerWhisker)) + .attr('x2', xScale(upperWhisker)) + .attr('y1', midline) + .attr('y2', midline); + + // mean, behind the rect + svg.append('line') + .attr('class', style.mean) + .attr('x1', xScale(meanVal)) + .attr('x2', xScale(meanVal)) + .attr('y1', midline - bh - 3) + .attr('y2', midline + bh + 3); + + // draw rect for iqr + svg.append('rect') + .attr('class', style.iqr) + .attr('x', xScale(q1Val)) + .attr('y', midline - bh) + .attr('width', xScale(iqr + field.range[0])) + .attr('height', 2 * bh); + + // draw vertical line at median + if (field.mean) { + svg.append('line') + .attr('class', style.median) + .attr('x1', xScale(medianVal)) + .attr('x2', xScale(medianVal)) + .attr('y1', midline - bh) + .attr('y2', midline + bh); + } } - } - const formatter = d3.format('.3s'); - minCell.text(formatter(hobj.min)) - .style('display', hideField.minMax ? 'none' : null); - maxCell.text(formatter(hobj.max)) - .style('display', hideField.minMax ? 'none' : null); + const formatter = d3.format('.3s'); + minCell.text(formatter(hobj ? hobj.min : field.range[0])) + .style('display', hideField.minMax ? 'none' : null); + maxCell.text(formatter(hobj ? hobj.max : field.range[1])) + .style('display', hideField.minMax ? 'none' : null); + } } } } @@ -250,6 +337,19 @@ function fieldSelector(publicAPI, model) { // Render all fields variablesContainer .each(renderField); + + // hide the hist/whisker toggle if no whisker data found. + if (!foundQuantile) model.showHist = true; + + chartHeader.select('span').text(model.showHist ? 'Histogram' : 'Whisker'); + chartHeader.select('i') + .style('display', foundQuantile ? null : 'none') + .on('click', clickChart) + .classed(model.showHist ? style.showHistIcon : style.showBoxIcon, false) + .classed(!model.showHist ? style.showHistIcon : style.showBoxIcon, true); + + + model.lastShowHist = model.showHist; }; function handleHoverUpdate(data) { @@ -305,6 +405,8 @@ const DEFAULT_VALUES = { fieldHistWidth: 120, fieldHistHeight: 15, numberOfBins: 32, + showHist: false, + lastShowHist: false, }; // ---------------------------------------------------------------------------- diff --git a/src/InfoViz/Native/FieldSelector/template.html b/src/InfoViz/Native/FieldSelector/template.html deleted file mode 100644 index 8e46962257..0000000000 --- a/src/InfoViz/Native/FieldSelector/template.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - -
diff --git a/src/InfoViz/Native/HistogramSelector/example/index.js b/src/InfoViz/Native/HistogramSelector/example/index.js index 5203f9741e..9598bf6612 100644 --- a/src/InfoViz/Native/HistogramSelector/example/index.js +++ b/src/InfoViz/Native/HistogramSelector/example/index.js @@ -15,7 +15,7 @@ import SelectionProvider from '../../../../../src/InfoViz/Core/SelectionProvider import HistogramSelector from '../../../Native/HistogramSelector'; import FieldSelector from '../../../Native/FieldSelector'; -import dataModel from './state.json'; +import dataModel from './state2.json'; const bodyElt = document.querySelector('body'); // '100vh' is 100% of the current screen height diff --git a/src/InfoViz/Native/HistogramSelector/example/state2.json b/src/InfoViz/Native/HistogramSelector/example/state2.json new file mode 100644 index 0000000000..3d44bbf05a --- /dev/null +++ b/src/InfoViz/Native/HistogramSelector/example/state2.json @@ -0,0 +1,1185 @@ +{ + "histogram1D_storage": { + "32": { + "true shooting percentage": { + "max": 0.8200000000000001, + "counts": [ + 3, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 2, + 2, + 2, + 3, + 5, + 2, + 7, + 8, + 12, + 36, + 42, + 81, + 94, + 82, + 65, + 32, + 9, + 5, + 3, + 1, + 1, + 1, + 0, + 1 + ], + "name": "true shooting percentage", + "min": 0.0 + }, + "rebounds pergame": { + "max": 13.600000000000001, + "counts": [ + 3, + 25, + 39, + 38, + 59, + 43, + 44, + 35, + 44, + 22, + 17, + 21, + 22, + 7, + 12, + 9, + 14, + 5, + 5, + 4, + 8, + 5, + 6, + 4, + 0, + 2, + 2, + 1, + 1, + 1, + 0, + 2 + ], + "name": "rebounds pergame", + "min": 0.0 + }, + "blocks per game": { + "max": 2.82, + "counts": [ + 106, + 86, + 65, + 56, + 38, + 25, + 21, + 18, + 15, + 11, + 9, + 9, + 5, + 6, + 7, + 3, + 3, + 3, + 3, + 3, + 2, + 1, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 1, + 1 + ], + "name": "blocks per game", + "min": 0.0 + }, + "points per game": { + "max": 32.0, + "counts": [ + 9, + 29, + 53, + 54, + 45, + 29, + 38, + 23, + 28, + 29, + 20, + 24, + 13, + 23, + 15, + 12, + 9, + 12, + 6, + 8, + 7, + 4, + 2, + 1, + 2, + 1, + 1, + 2, + 0, + 0, + 0, + 1 + ], + "name": "points per game", + "min": 0.0 + }, + "3 point shots percentage": { + "max": 1.0, + "counts": [ + 127, + 1, + 3, + 3, + 6, + 9, + 16, + 11, + 31, + 34, + 63, + 75, + 66, + 26, + 10, + 4, + 7, + 0, + 1, + 2, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 4 + ], + "name": "3 point shots percentage", + "min": 0.0 + }, + "turnover per pocession": { + "max": 1.0, + "counts": [ + 7, + 9, + 53, + 143, + 127, + 74, + 46, + 21, + 8, + 5, + 0, + 3, + 2, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1 + ], + "name": "turnover per pocession", + "min": 0.0 + }, + "age": { + "max": 39.0, + "counts": [ + 2, + 17, + 0, + 19, + 39, + 0, + 51, + 0, + 50, + 53, + 0, + 46, + 42, + 0, + 38, + 0, + 31, + 19, + 0, + 21, + 14, + 0, + 23, + 0, + 8, + 10, + 0, + 6, + 7, + 0, + 2, + 2 + ], + "name": "age", + "min": 19.0 + }, + "free throw attempts": { + "max": 805.0, + "counts": [ + 152, + 62, + 63, + 33, + 29, + 31, + 11, + 23, + 11, + 15, + 10, + 11, + 10, + 11, + 8, + 4, + 4, + 2, + 1, + 0, + 0, + 1, + 0, + 2, + 0, + 3, + 2, + 0, + 0, + 0, + 0, + 1 + ], + "name": "free throw attempts", + "min": 0.0 + }, + "versatility index": { + "max": 12.600000000000001, + "counts": [ + 17, + 0, + 0, + 0, + 0, + 0, + 1, + 4, + 5, + 12, + 11, + 33, + 45, + 35, + 49, + 65, + 28, + 45, + 31, + 29, + 26, + 22, + 13, + 7, + 7, + 2, + 4, + 4, + 3, + 0, + 0, + 2 + ], + "name": "versatility index", + "min": 0.0 + }, + "2 point shots attempts": { + "max": 1408.0, + "counts": [ + 94, + 63, + 47, + 31, + 28, + 30, + 20, + 21, + 28, + 16, + 13, + 12, + 10, + 8, + 8, + 9, + 14, + 9, + 10, + 6, + 5, + 2, + 4, + 3, + 1, + 0, + 2, + 3, + 0, + 1, + 1, + 1 + ], + "name": "2 point shots attempts", + "min": 0.0 + }, + "3 point shots attempts": { + "max": 615.0, + "counts": [ + 201, + 33, + 33, + 26, + 22, + 17, + 22, + 12, + 11, + 15, + 14, + 12, + 15, + 8, + 3, + 11, + 6, + 6, + 6, + 2, + 4, + 4, + 3, + 2, + 1, + 3, + 5, + 1, + 1, + 0, + 0, + 1 + ], + "name": "3 point shots attempts", + "min": 0.0 + }, + "percentage of team minutes": { + "max": 79.5, + "counts": [ + 4, + 11, + 18, + 24, + 21, + 23, + 17, + 16, + 19, + 17, + 22, + 14, + 15, + 20, + 23, + 18, + 9, + 13, + 12, + 14, + 12, + 15, + 20, + 16, + 15, + 20, + 20, + 13, + 16, + 13, + 4, + 6 + ], + "name": "percentage of team minutes", + "min": 7.2 + }, + "games": { + "max": 82.0, + "counts": [ + 13, + 14, + 9, + 18, + 4, + 13, + 10, + 18, + 20, + 16, + 11, + 17, + 4, + 8, + 7, + 15, + 13, + 12, + 9, + 6, + 20, + 14, + 5, + 12, + 18, + 9, + 19, + 16, + 33, + 13, + 35, + 69 + ], + "name": "games", + "min": 1.0 + }, + "true rebound percentage": { + "max": 35.2, + "counts": [ + 1, + 0, + 5, + 15, + 68, + 74, + 50, + 35, + 23, + 41, + 24, + 30, + 20, + 29, + 22, + 21, + 14, + 13, + 4, + 3, + 4, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 1 + ], + "name": "true rebound percentage", + "min": 0.0 + }, + "assists per game": { + "max": 10.700000000000001, + "counts": [ + 67, + 72, + 89, + 40, + 48, + 36, + 18, + 19, + 16, + 13, + 13, + 12, + 8, + 3, + 8, + 4, + 4, + 8, + 8, + 0, + 3, + 0, + 3, + 1, + 0, + 2, + 3, + 0, + 0, + 1, + 0, + 1 + ], + "name": "assists per game", + "min": 0.0 + }, + "steals per game": { + "max": 2.48, + "counts": [ + 26, + 26, + 33, + 27, + 51, + 32, + 51, + 30, + 34, + 31, + 25, + 22, + 24, + 16, + 9, + 13, + 10, + 7, + 4, + 6, + 6, + 5, + 3, + 3, + 3, + 1, + 0, + 0, + 0, + 1, + 0, + 1 + ], + "name": "steals per game", + "min": 0.0 + }, + "usage percent": { + "max": 37.6, + "counts": [ + 1, + 1, + 2, + 1, + 1, + 4, + 7, + 12, + 19, + 20, + 32, + 47, + 36, + 41, + 40, + 40, + 42, + 25, + 26, + 35, + 18, + 16, + 12, + 6, + 6, + 2, + 2, + 2, + 1, + 2, + 0, + 1 + ], + "name": "usage percent", + "min": 2.0 + }, + "minutes": { + "max": 38.5, + "counts": [ + 14, + 22, + 20, + 26, + 20, + 15, + 14, + 15, + 19, + 21, + 19, + 12, + 15, + 27, + 19, + 11, + 9, + 8, + 18, + 12, + 16, + 16, + 13, + 15, + 15, + 23, + 16, + 12, + 16, + 13, + 3, + 6 + ], + "name": "minutes", + "min": 5.1000000000000005 + }, + "2 point shots percentage": { + "max": 1.0, + "counts": [ + 6, + 0, + 0, + 0, + 0, + 1, + 1, + 2, + 6, + 7, + 14, + 12, + 32, + 55, + 103, + 89, + 90, + 42, + 19, + 7, + 4, + 7, + 0, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 1 + ], + "name": "2 point shots percentage", + "min": 0.0 + }, + "percentage of team assists": { + "max": 49.2, + "counts": [ + 18, + 12, + 37, + 55, + 47, + 61, + 34, + 41, + 26, + 20, + 18, + 21, + 12, + 8, + 17, + 6, + 9, + 12, + 5, + 9, + 7, + 6, + 8, + 0, + 2, + 1, + 4, + 1, + 0, + 1, + 0, + 2 + ], + "name": "percentage of team assists", + "min": 0.0 + }, + "free throw percent": { + "max": 1.0, + "counts": [ + 18, + 0, + 0, + 0, + 0, + 1, + 1, + 0, + 2, + 0, + 3, + 1, + 3, + 4, + 2, + 3, + 21, + 11, + 20, + 13, + 32, + 31, + 32, + 37, + 61, + 58, + 62, + 37, + 17, + 7, + 7, + 16 + ], + "name": "free throw percent", + "min": 0.0 + } + } + }, + "fields": { + "true shooting percentage": { + "name": "true shooting percentage", + "range": [ + 0.0, + 0.8200000000000001 + ], + "quantiles": [ + 0.379, + 0.48674999999999996, + 0.5265, + 0.559, + 0.666 + ], + "active": true, + "id": 1, + "mean": 0.51505800000000002 + }, + "rebounds pergame": { + "name": "rebounds pergame", + "range": [ + 0.0, + 13.600000000000001 + ], + "quantiles": [ + 0.0, + 1.8, + 2.9, + 4.7, + 9.0 + ], + "active": false, + "id": 2, + "mean": 3.5950000000000002 + }, + "blocks per game": { + "name": "blocks per game", + "range": [ + 0.0, + 2.82 + ], + "quantiles": [ + 0.0, + 0.1, + 0.25, + 0.52, + 1.15 + ], + "active": true, + "id": 3, + "mean": 0.39505999999999997 + }, + "points per game": { + "name": "points per game", + "range": [ + 0.0, + 32.0 + ], + "quantiles": [ + 0.0, + 3.575, + 6.8, + 11.7, + 23.2 + ], + "active": false, + "id": 4, + "mean": 8.2064000000000021 + }, + "3 point shots percentage": { + "name": "3 point shots percentage", + "range": [ + 0.0, + 1.0 + ], + "quantiles": [ + 0.0, + 0.0, + 0.318, + 0.3715, + 0.667 + ], + "active": false, + "id": 5, + "mean": 0.25501600000000002 + }, + "turnover per pocession": { + "name": "turnover per pocession", + "range": [ + 0.0, + 1.0 + ], + "quantiles": [ + 0.033, + 0.107, + 0.132, + 0.167, + 0.257 + ], + "active": false, + "id": 6, + "mean": 0.14385800000000001 + }, + "age": { + "name": "age", + "range": [ + 19.0, + 39.0 + ], + "quantiles": [ + 19.0, + 23.0, + 26.0, + 29.0, + 38.0 + ], + "active": false, + "id": 7, + "mean": 26.609999999999999 + }, + "free throw attempts": { + "name": "free throw attempts", + "range": [ + 0.0, + 805.0 + ], + "quantiles": [ + 0.0, + 19.0, + 63.0, + 163.25, + 376.0 + ], + "active": false, + "id": 8, + "mean": 112.61199999999999 + }, + "versatility index": { + "name": "versatility index", + "range": [ + 0.0, + 12.600000000000001 + ], + "quantiles": [ + 2.5, + 5.1, + 6.2, + 7.324999999999999, + 10.6 + ], + "active": true, + "id": 0, + "mean": 6.2140000000000004 + }, + "2 point shots attempts": { + "name": "2 point shots attempts", + "range": [ + 0.0, + 1408.0 + ], + "quantiles": [ + 0.0, + 59.0, + 200.0, + 424.25, + 968.0 + ], + "active": true, + "id": 10, + "mean": 293.31400000000002 + }, + "3 point shots attempts": { + "name": "3 point shots attempts", + "range": [ + 0.0, + 615.0 + ], + "quantiles": [ + 0.0, + 3.75, + 47.0, + 165.5, + 408.0 + ], + "active": true, + "id": 11, + "mean": 102.806 + }, + "percentage of team minutes": { + "name": "percentage of team minutes", + "range": [ + 7.2, + 79.5 + ], + "quantiles": [ + 7.2, + 23.7, + 39.7, + 58.800000000000004, + 79.5 + ], + "active": false, + "id": 12, + "mean": 41.359399999999994 + }, + "games": { + "name": "games", + "range": [ + 1.0, + 82.0 + ], + "quantiles": [ + 1.0, + 24.75, + 53.0, + 73.0, + 82.0 + ], + "active": true, + "id": 13, + "mean": 49.223999999999997 + }, + "true rebound percentage": { + "name": "true rebound percentage", + "range": [ + 0.0, + 35.2 + ], + "quantiles": [ + 0.0, + 6.1, + 9.0, + 13.6, + 23.7 + ], + "active": false, + "id": 9, + "mean": 10.143799999999999 + }, + "assists per game": { + "name": "assists per game", + "range": [ + 0.0, + 10.700000000000001 + ], + "quantiles": [ + 0.0, + 0.6, + 1.15, + 2.425, + 5.1 + ], + "active": false, + "id": 15, + "mean": 1.8260000000000001 + }, + "steals per game": { + "name": "steals per game", + "range": [ + 0.0, + 2.48 + ], + "quantiles": [ + 0.0, + 0.33, + 0.555, + 0.88, + 1.65 + ], + "active": false, + "id": 16, + "mean": 0.63891999999999993 + }, + "usage percent": { + "name": "usage percent", + "range": [ + 2.0, + 37.6 + ], + "quantiles": [ + 4.0, + 14.9, + 18.2, + 22.3, + 33.2 + ], + "active": false, + "id": 17, + "mean": 18.568800000000003 + }, + "minutes": { + "name": "minutes", + "range": [ + 5.1000000000000005, + 38.5 + ], + "quantiles": [ + 5.1, + 12.1, + 19.4, + 28.625000000000004, + 38.5 + ], + "active": true, + "id": 18, + "mean": 20.248799999999999 + }, + "2 point shots percentage": { + "name": "2 point shots percentage", + "range": [ + 0.0, + 1.0 + ], + "quantiles": [ + 0.323, + 0.433, + 0.474, + 0.51, + 0.625 + ], + "active": true, + "id": 19, + "mean": 0.46557199999999999 + }, + "percentage of team assists": { + "name": "percentage of team assists", + "range": [ + 0.0, + 49.2 + ], + "quantiles": [ + 0.0, + 6.2, + 10.149999999999999, + 17.525, + 34.5 + ], + "active": true, + "id": 14, + "mean": 12.9314 + }, + "free throw percent": { + "name": "free throw percent", + "range": [ + 0.0, + 1.0 + ], + "quantiles": [ + 0.4, + 0.645, + 0.755, + 0.821, + 1.0 + ], + "active": false, + "id": 20, + "mean": 0.70936199999999994 + } + }, + "dirty": true +} \ No newline at end of file diff --git a/style/InfoVizNative/FieldSelector.mcss b/style/InfoVizNative/FieldSelector.mcss index bc7af0bcfc..45423e89ff 100644 --- a/style/InfoVizNative/FieldSelector.mcss +++ b/style/InfoVizNative/FieldSelector.mcss @@ -1,10 +1,15 @@ /*empty styles allow for d3 selection in javascript*/ .jsFieldName, +.jsFieldSelector, +.jsFieldSelectorMode, +.jsFieldSelectorLabel, .jsHistMax, .jsHistMin, .jsHistRect, .jsLegend, -.jsSparkline { +.jsSparkline, +.jsTHead, +.jsTBody { } @@ -29,6 +34,18 @@ composes: fa-square-o from 'font-awesome/css/font-awesome.css'; } +.showHistIcon { + composes: icon; + composes: fa-bar-chart from 'font-awesome/css/font-awesome.css'; + float: right; + margin-left: 3px; +} + +.showBoxIcon { + composes: showHistIcon; + opacity: 0.5; +} + .legend { composes: jsLegend; text-align: center; @@ -46,6 +63,11 @@ text-overflow: ellipsis; } +.chartHeader { + width: calc(100% - 16px); + text-align: center; +} + .row { user-select: none; cursor: pointer; @@ -66,11 +88,13 @@ } .thead { + composes: jsTHead; user-select: none; cursor: pointer; } .tbody { + composes: jsTBody; } .sparkline { @@ -80,6 +104,7 @@ .sparklineSvg { vertical-align: middle; + shape-rendering: crispEdges; } .histRect { @@ -103,3 +128,21 @@ .binHilite { fill: #001EB8; } + +.whisker { + stroke: black; +} + +.iqr { + stroke: black; + fill: white; +} + +.median { + stroke: black; +} + +.mean { + stroke: black; + stroke-width: 2; +} diff --git a/tools/scripts/genWhiskers.py b/tools/scripts/genWhiskers.py new file mode 100644 index 0000000000..303b7d696d --- /dev/null +++ b/tools/scripts/genWhiskers.py @@ -0,0 +1,60 @@ +import vtk, sys, os, json +from vtk import vtkDoubleArray, vtkIntArray, vtkVariantArray, vtkTable, vtkIdList +import argparse +from vtk.util.numpy_support import vtk_to_numpy + +import numpy as np + +parser = argparse.ArgumentParser(description="Sample data generator") +parser.add_argument("--input", default=None, help="input csv") +parser.add_argument("--output", default=None, help="output json") +parser.add_argument("--base", default=None, help="base json") +args = parser.parse_args() +print(args.input, args.output) + +csvReader = vtk.vtkDelimitedTextReader() +csvReader.SetFileName(args.input or 'nba.csv') +csvReader.SetHaveHeaders(True) +csvReader.SetDetectNumericColumns(True) +csvReader.SetForceDouble(True) + +csvReader.Update() +inputTable = csvReader.GetOutput() +print ("Num cols", inputTable.GetNumberOfColumns()) +nameList = [] +for i in range(inputTable.GetNumberOfColumns()): + arr = inputTable.GetColumn(i) + if (arr.IsNumeric()): + nameList.append(inputTable.GetColumnName(i)) + else: + print("rejected", inputTable.GetColumnName(i)) + +print ("Numeric", nameList) + +filepath = './state.json' +with open(args.base or filepath, 'r') as fd: + data = json.load(fd) + +for key in data['fields']: + arr = inputTable.GetColumnByName(key) + narr = vtk_to_numpy(arr) + # narr.sort() + data['fields'][key]['mean'] = np.mean(narr) + quantiles = np.percentile(narr, [2, 25, 50, 75, 98]).tolist() + # print(key, quantiles) + # we also might want to search for IQR whisker values + # last data inside 1.5 * IQR + iqr = quantiles[3] - quantiles[1] + multIqr = 1.5 * iqr + quantiles[0] = quantiles[1] + quantiles[4] = quantiles[3] + for a in narr.tolist(): + if (a < quantiles[0] and a >= (quantiles[1] - multIqr)): + quantiles[0] = a + elif (a > quantiles[4] and a <= (quantiles[3] + multIqr)): + quantiles[4] = a + data['fields'][key]['quantiles'] = quantiles + print(key, quantiles) + +with open(args.output or './state2.json', 'w') as fd: + json.dump(data, fd, indent=2) \ No newline at end of file