Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
sgratzl committed Mar 15, 2019
1 parent 6099830 commit 3a5a4d4
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 157 deletions.
3 changes: 3 additions & 0 deletions src/model/IDateColumn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export interface IDateColumn extends Column {
getDate(row: IDataRow): Date | null;

iterDate(row: IDataRow): IForEachAble<Date | null>;

getFilter(): IDateFilter;
setFilter(value: IDateFilter | null): void;
}

export interface IDatesColumn extends IDateColumn, IArrayColumn<Date | null> {
Expand Down
85 changes: 77 additions & 8 deletions src/renderer/DateHistogramCellRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {dateStatsBuilder, getNumberOfBins, IDateStatistics} from '../internal';
import {Column, IDataRow, IDateColumn, IDatesColumn, IOrderedGroup, isDateColumn, isDatesColumn} from '../model';
import {Column, IDataRow, IDateColumn, IDatesColumn, IOrderedGroup, isDateColumn, isDatesColumn, DateColumn} from '../model';
import {cssClass} from '../styles';
import {ERenderMode, ICellRendererFactory, IRenderContext} from './interfaces';
import {renderMissingDOM} from './missing';
import {colorOf} from './utils';
import {histogramRender, histogramTemplate} from './histogram';
import {histogramUpdate, histogramTemplate, mappingHintTemplate, mappingHintUpdate, IFilterInfo, IFilterContext} from './histogram';
import InputDateDialog from '../ui/dialogs/InputDateDialog';

/** @internal */
export default class DateHistogramCellRenderer implements ICellRendererFactory {
Expand Down Expand Up @@ -57,7 +58,7 @@ export default class DateHistogramCellRenderer implements ICellRendererFactory {


function staticSummary(col: IDateColumn, context: IRenderContext, interactive: boolean, template: string, render: (n: HTMLElement, stats: IDateStatistics, unfiltered?: IDateStatistics) => void) {
template += `<span class="${cssClass('mapping-hint')}"></span><span class="${cssClass('mapping-hint')}"></span>`;
template += mappingHintTemplate(['', '']);

return {
template: `${template}</div>`,
Expand All @@ -72,8 +73,8 @@ function staticSummary(col: IDateColumn, context: IRenderContext, interactive: b
if (!summary) {
return;
}
const range = [summary.min, summary.max].map((d) => col.getFormatter()(d));
Array.from(node.getElementsByTagName('span')).forEach((d: HTMLElement, i) => d.textContent = range[i]);
const formatter = col.getFormatter();
mappingHintUpdate(node, [formatter(summary.min), formatter(summary.max)]);

render(node, summary, interactive ? data: undefined);
});
Expand All @@ -82,19 +83,87 @@ function staticSummary(col: IDateColumn, context: IRenderContext, interactive: b
}


function interactiveSummary(col: IDateColumn, context: IRenderContext, template: string, render: (n: HTMLElement, stats: IDateStatistics, unfiltered?: IDateStatistics) => void) {
const fContext = createFilterContext(col, context);
template += filteredHistTemplate(fContext, createFilterInfo(col));

let updateFilter: (missing: number, f: IFilterInfo<number>) => void;

return {
template: `${template}</div>`,
update: (node: HTMLElement) => {
if (!updateFilter) {
updateFilter = initFilter(node, fContext);
}
return context.tasks.summaryNumberStats(col).then((r) => {
if (typeof r === 'symbol') {
return;
}
const {summary, data} = r;

updateFilter(data ? data.missing : (summary ? summary.missing : 0), createFilterInfo(col));

node.classList.add(cssClass('histogram-i'));
node.classList.toggle(cssClass('missing'), !summary);
if (!summary) {
return;
}
render(node, summary, data);
});
}
};
}

function getHistDOMRenderer(col: IDateColumn) {
const ranking = col.findMyRanker();
const guessedBins = ranking ? getNumberOfBins(ranking.getOrderLength()) : 10;
const guessedBins = 10;

const formatter = col.getFormatter();
const color = colorOf(col)!;

const render = (n: HTMLElement, stats: IDateStatistics, unfiltered?: IDateStatistics) => {
return histogramRender(n, stats, unfiltered || null, formatter, () => color);
return histogramUpdate(n, stats, unfiltered || null, formatter, () => color);
};
return {
template: histogramTemplate(guessedBins),
render,
guessedBins
};
}

function createFilterInfo(col: IDateColumn, domain: [Date, Date]): IFilterInfo<number> {
const filter = col.getFilter();
const filterMin = isFinite(filter.min) ? filter.min : domain[0];
const filterMax = isFinite(filter.max) ? filter.max : domain[1];
return {
filterMissing: filter.filterMissing,
filterMin,
filterMax
};
}

function createFilterContext(col: IDateColumn, domain: [Date, Date], context: IRenderContext): IFilterContext<number> {
const percent = (v: Date | null) => Math.round(100 * (v - domain[0]) / (domain[1] - domain[0]));
const unpercent = (v: Date | null) => ((v / 100) * (domain[1] - domain[0]) + domain[0]);
return {
percent,
unpercent,
format: col.getFormatter.bind(col),
setFilter: (filterMissing, minValue, maxValue) => col.setFilter({
filterMissing,
min: Math.abs(minValue - domain[0]) < 0.001 ? NaN : minValue,
max: Math.abs(maxValue - domain[1]) < 0.001 ? NaN : maxValue
}),
edit: (value, attachment) => {
return new Promise((resolve) => {
const dialogCtx = {
attachment,
manager: context.dialogManager,
level: 1,
idPrefix: context.idPrefix
};
const dialog = new InputDateDialog(dialogCtx, resolve, {value});
dialog.open();
});
}
};
}
188 changes: 41 additions & 147 deletions src/renderer/HistogramCellRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import {dragHandle, IDragHandleOptions, normalizedStatsBuilder, IStatistics, round, getNumberOfBins} from '../internal';
import {normalizedStatsBuilder, IStatistics, round, getNumberOfBins} from '../internal';
import {Column, IDataRow, IOrderedGroup, INumberColumn, INumbersColumn, isNumberColumn, isNumbersColumn, IMapAbleColumn, isMapAbleColumn} from '../model';
import InputNumberDialog from '../ui/dialogs/InputNumberDialog';
import {filterMissingNumberMarkup, updateFilterMissingNumberMarkup} from '../ui/missing';
import {colorOf} from './impose';
import {IRenderContext, ERenderMode, ICellRendererFactory, IImposer} from './interfaces';
import {renderMissingDOM} from './missing';
import {cssClass} from '../styles';
import {histogramRender, histogramTemplate, IHistogramLike} from './histogram';
import {histogramUpdate, histogramTemplate, IHistogramLike, mappingHintTemplate, mappingHintUpdate, IFilterInfo, filteredHistTemplate, IFilterContext, initFilter} from './histogram';

/** @internal */
export default class HistogramCellRenderer implements ICellRendererFactory {
Expand Down Expand Up @@ -62,15 +61,13 @@ export default class HistogramCellRenderer implements ICellRendererFactory {

function staticSummary(col: INumberColumn, context: IRenderContext, template: string, render: (n: HTMLElement, stats: IStatistics, unfiltered?: IStatistics) => void) {
if (isMapAbleColumn(col)) {
const range = col.getRange();
template += `<span class="${cssClass('mapping-hint')}">${range[0]}</span><span class="${cssClass('mapping-hint')}">${range[1]}</span>`;
template += mappingHintTemplate(col.getRange());
}
return {
template: `${template}</div>`,
update: (node: HTMLElement) => {
if (isMapAbleColumn(col)) {
const range = col.getRange();
Array.from(node.getElementsByTagName('span')).forEach((d: HTMLElement, i) => d.textContent = range[i]);
mappingHintUpdate(node, col.getRange());
}

return context.tasks.summaryNumberStats(col).then((r) => {
Expand All @@ -90,30 +87,24 @@ function staticSummary(col: INumberColumn, context: IRenderContext, template: st
}

function interactiveSummary(col: IMapAbleColumn, context: IRenderContext, template: string, render: (n: HTMLElement, stats: IStatistics, unfiltered?: IStatistics) => void) {
const f = filter(col);
template += `
<div class="${cssClass('histogram-min-hint')}" style="width: ${f.percent(f.filterMin)}%"></div>
<div class="${cssClass('histogram-max-hint')}" style="width: ${100 - f.percent(f.filterMax)}%"></div>
<div class="${cssClass('histogram-min')}" data-value="${round(f.filterMin, 2)}" style="left: ${f.percent(f.filterMin)}%" title="min filter, drag or shift click to change"></div>
<div class="${cssClass('histogram-max')}" data-value="${round(f.filterMax, 2)}" style="right: ${100 - f.percent(f.filterMax)}%" title="max filter, drag or shift click to change"></div>
${filterMissingNumberMarkup(f.filterMissing, 0)}
`;
const fContext = createFilterContext(col, context);
template += filteredHistTemplate(fContext, createFilterInfo(col));

let updateFilter: (missing: number, col: IMapAbleColumn) => void;
let updateFilter: (missing: number, f: IFilterInfo<number>) => void;

return {
template: `${template}</div>`,
update: (node: HTMLElement) => {
if (!updateFilter) {
updateFilter = initFilter(node, col, context);
updateFilter = initFilter(node, fContext);
}
return context.tasks.summaryNumberStats(col).then((r) => {
if (typeof r === 'symbol') {
return;
}
const {summary, data} = r;

updateFilter(data ? data.missing : (summary ? summary.missing : 0), col);
updateFilter(data ? data.missing : (summary ? summary.missing : 0), createFilterInfo(col));

node.classList.add(cssClass('histogram-i'));
node.classList.toggle(cssClass('missing'), !summary);
Expand All @@ -126,135 +117,14 @@ function interactiveSummary(col: IMapAbleColumn, context: IRenderContext, templa
};
}

function initFilter(node: HTMLElement, col: IMapAbleColumn, context: IRenderContext) {
const min = <HTMLElement>node.getElementsByClassName(cssClass('histogram-min'))[0];
const max = <HTMLElement>node.getElementsByClassName(cssClass('histogram-max'))[0];
const minHint = <HTMLElement>node.getElementsByClassName(cssClass('histogram-min-hint'))[0];
const maxHint = <HTMLElement>node.getElementsByClassName(cssClass('histogram-max-hint'))[0];
const filterMissing = <HTMLInputElement>node.getElementsByTagName('input')[0];

const setFilter = () => {
const f = filter(col);
const minValue = f.unpercent(parseFloat(min.style.left!));
const maxValue = f.unpercent(100 - parseFloat(max.style.right!));
col.setFilter({
filterMissing: filterMissing.checked,
min: Math.abs(minValue - f.domain[0]) < 0.001 ? NaN : minValue,
max: Math.abs(maxValue - f.domain[1]) < 0.001 ? NaN : maxValue
});
};

min.onclick = (evt) => {
if (!evt.shiftKey && !evt.ctrlKey) {
return;
}
evt.preventDefault();
evt.stopPropagation();

const f = filter(col);
const value = f.unpercent(parseFloat(min.style.left!));

const dialogCtx = {
attachment: min,
manager: context.dialogManager,
level: 1,
idPrefix: context.idPrefix
};

const dialog = new InputNumberDialog(dialogCtx, (newValue) => {
minHint.style.width = `${f.percent(newValue)}%`;
min.dataset.value = round(newValue, 2).toString();
min.style.left = `${f.percent(newValue)}%`;
min.classList.toggle(cssClass('swap-hint'), f.percent(newValue) > 15);
setFilter();
}, {
value, min: f.domain[0], max: f.domain[1]
});
dialog.open();
};

max.onclick = (evt) => {
if (!evt.shiftKey && !evt.ctrlKey) {
return;
}
evt.preventDefault();
evt.stopPropagation();

const f = filter(col);
const value = f.unpercent(100 - parseFloat(max.style.right!));

const dialogCtx = {
attachment: max,
manager: context.dialogManager,
level: 1,
idPrefix: context.idPrefix
};

const dialog = new InputNumberDialog(dialogCtx, (newValue) => {
maxHint.style.width = `${100 - f.percent(newValue)}%`;
max.dataset.value = round(newValue, 2).toString();
max.style.right = `${100 - f.percent(newValue)}%`;
min.classList.toggle(cssClass('swap-hint'), f.percent(newValue) < 85);
setFilter();
}, {
value, min: f.domain[0], max: f.domain[1]
});
dialog.open();
};

filterMissing.onchange = () => setFilter();

const options: Partial<IDragHandleOptions> = {
minDelta: 0,
filter: (evt) => evt.button === 0 && !evt.shiftKey && !evt.ctrlKey,
onStart: (handle) => handle.classList.add(cssClass('hist-dragging')),
onDrag: (handle, x) => {
const total = node.clientWidth;
const px = Math.max(0, Math.min(x, total));
const percent = Math.round(100 * px / total);
const domain = col.getMapping().domain;
(<HTMLElement>handle).dataset.value = round(((percent / 100) * (domain[1] - domain[0]) + domain[0]), 2).toString();

if ((<HTMLElement>handle).classList.contains(cssClass('histogram-min'))) {
handle.style.left = `${percent}%`;
handle.classList.toggle(cssClass('swap-hint'), percent > 15);
minHint.style.width = `${percent}%`;
return;
}
handle.style.right = `${100 - percent}%`;
handle.classList.toggle(cssClass('swap-hint'), percent < 85);
maxHint.style.width = `${100 - percent}%`;
},
onEnd: (handle) => {
handle.classList.remove(cssClass('hist-dragging'));
setFilter();
}
};
dragHandle(min, options);
dragHandle(max, options);

return (missing: number, actCol: IMapAbleColumn) => {
col = actCol;
const f = filter(col);
minHint.style.width = `${f.percent(f.filterMin)}%`;
maxHint.style.width = `${100 - f.percent(f.filterMax)}%`;
min.dataset.value = round(f.filterMin, 2).toString();
max.dataset.value = round(f.filterMax, 2).toString();
min.style.left = `${f.percent(f.filterMin)}%`;
max.style.right = `${100 - f.percent(f.filterMax)}%`;
filterMissing.checked = f.filterMissing;
updateFilterMissingNumberMarkup(<HTMLElement>filterMissing.parentElement, missing);
};
}

/** @internal */
export function getHistDOMRenderer(col: INumberColumn, imposer?: IImposer) {
const ranking = col.findMyRanker();
const guessedBins = ranking ? getNumberOfBins(ranking.getOrderLength()) : 10;
const formatter = col.getNumberFormat();

const render = (n: HTMLElement, stats: IHistogramLike<number>, unfiltered?: IHistogramLike<number>) => {
return histogramRender(n, stats, unfiltered || null, formatter, (bin) => colorOf(col, null, imposer, (bin.x1 + bin.x0) / 2)!);
return histogramUpdate(n, stats, unfiltered || null, formatter, (bin) => colorOf(col, null, imposer, (bin.x1 + bin.x0) / 2)!);
};
return {
template: histogramTemplate(guessedBins),
Expand All @@ -263,20 +133,44 @@ export function getHistDOMRenderer(col: INumberColumn, imposer?: IImposer) {
};
}


function filter(col: IMapAbleColumn) {
function createFilterInfo(col: IMapAbleColumn): IFilterInfo<number> {
const filter = col.getFilter();
const domain = col.getMapping().domain;
const percent = (v: number) => Math.round(100 * (v - domain[0]) / (domain[1] - domain[0]));
const unpercent = (v: number) => ((v / 100) * (domain[1] - domain[0]) + domain[0]);
const filterMin = isFinite(filter.min) ? filter.min : domain[0];
const filterMax = isFinite(filter.max) ? filter.max : domain[1];
return {
filterMissing: filter.filterMissing,
domain,
percent,
unpercent,
filterMin,
filterMax
};
}

function createFilterContext(col: IMapAbleColumn, context: IRenderContext): IFilterContext<number> {
const domain = col.getMapping().domain;
const percent = (v: number) => Math.round(100 * (v - domain[0]) / (domain[1] - domain[0]));
const unpercent = (v: number) => ((v / 100) * (domain[1] - domain[0]) + domain[0]);
return {
percent,
unpercent,
format: (v) => round(v, 2).toString(),
setFilter: (filterMissing, minValue, maxValue) => col.setFilter({
filterMissing,
min: Math.abs(minValue - domain[0]) < 0.001 ? NaN : minValue,
max: Math.abs(maxValue - domain[1]) < 0.001 ? NaN : maxValue
}),
edit: (value, attachment) => {
return new Promise((resolve) => {
const dialogCtx = {
attachment,
manager: context.dialogManager,
level: 1,
idPrefix: context.idPrefix
};
const dialog = new InputNumberDialog(dialogCtx, resolve, {
value, min: domain[0], max: domain[1]
});
dialog.open();
});
}
};
}
Loading

0 comments on commit 3a5a4d4

Please sign in to comment.