Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(FieldSelector): add whisker/box plot option #324

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 165 additions & 63 deletions src/InfoViz/Native/FieldSelector/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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)
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -198,58 +204,152 @@ 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);
}
}
}
}

// 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) {
Expand Down Expand Up @@ -305,6 +405,8 @@ const DEFAULT_VALUES = {
fieldHistWidth: 120,
fieldHistHeight: 15,
numberOfBins: 32,
showHist: false,
lastShowHist: false,
};

// ----------------------------------------------------------------------------
Expand Down
6 changes: 0 additions & 6 deletions src/InfoViz/Native/FieldSelector/template.html

This file was deleted.

2 changes: 1 addition & 1 deletion src/InfoViz/Native/HistogramSelector/example/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading