From 581e36b43baddda7480a26b99ae0aae5307e09f9 Mon Sep 17 00:00:00 2001 From: Dominik Dirmeier Date: Sun, 24 Jun 2018 22:47:34 +0200 Subject: [PATCH 01/16] implementation of a canvas texture renderer, for Caleydo/lineup_app#3 and Caleydo/lineup_app#4 --- src/styles/engine/_index.scss | 1 + src/styles/engine/_texture_renderer.scss | 15 ++ src/ui/CanvasTextureRenderer.ts | 170 +++++++++++++++++++++++ src/ui/EngineRenderer.ts | 91 +++++++----- src/ui/taggle/Taggle.ts | 3 +- src/ui/taggle/TaggleRenderer.ts | 5 + 6 files changed, 251 insertions(+), 34 deletions(-) create mode 100644 src/styles/engine/_texture_renderer.scss create mode 100644 src/ui/CanvasTextureRenderer.ts diff --git a/src/styles/engine/_index.scss b/src/styles/engine/_index.scss index 9fa845400..5b736e0f1 100644 --- a/src/styles/engine/_index.scss +++ b/src/styles/engine/_index.scss @@ -13,3 +13,4 @@ $engine_assets: '~lineupengine/src/assets'; @import './header'; @import './selection'; @import './row'; +@import './texture_renderer'; diff --git a/src/styles/engine/_texture_renderer.scss b/src/styles/engine/_texture_renderer.scss new file mode 100644 index 000000000..50122a6f8 --- /dev/null +++ b/src/styles/engine/_texture_renderer.scss @@ -0,0 +1,15 @@ +@import '../vars'; + +##{$lu-css_prefix}-texture-container { + display: flex; + overflow-y: hidden; + overflow-x: auto; + + img { + margin-right: 5px; + + &.partOfComposite { + margin-right: 6px; + } + } +} diff --git a/src/ui/CanvasTextureRenderer.ts b/src/ui/CanvasTextureRenderer.ts new file mode 100644 index 000000000..74b89f321 --- /dev/null +++ b/src/ui/CanvasTextureRenderer.ts @@ -0,0 +1,170 @@ +import EngineRanking from './EngineRanking'; +import {IDataRow} from '../model'; +import {scaleLinear, scaleOrdinal} from 'd3-scale'; +import Column from '../model/Column'; +import NumberColumn from '../model/NumberColumn'; +import NumbersColumn from '../model/NumbersColumn'; +import CategoricalColumn from '../model/CategoricalColumn'; +import CategoricalsColumn from '../model/CategoricalsColumn'; +import CompositeColumn from '../model/CompositeColumn'; +import * as d3 from 'd3-selection'; + +export interface ITextureRenderer { + update(rankings: EngineRanking[], localData: IDataRow[][]): void; + destroy(): void; + show(): void; + hide(): void; +} + +export default class CanvasTextureRenderer implements ITextureRenderer { + + readonly node: HTMLElement; + readonly canvas: any; + readonly headerNode: HTMLElement; + private renderedColumns: any[]; + + constructor(parent: Element) { + this.node = parent.ownerDocument.createElement('main'); + this.node.id = 'lu-texture-container'; + parent.appendChild(this.node); + this.canvas = parent.ownerDocument.createElement('canvas'); + this.headerNode = d3.select(parent).select('header').node(); + this.renderedColumns = []; + + this.node.addEventListener('scroll', () => { + { + //scroll header with main panel + this.headerNode.scrollLeft = this.node.scrollLeft; + } + }); + } + + update(rankings: EngineRanking[], localData: IDataRow[][]) { + this.node.innerHTML = ''; //remove all children + this.renderedColumns = []; + + rankings.forEach((r, i) => { + const grouped = r.groupData(localData[i]); + + r.ranking.flatColumns.forEach((column) => this.createColumn(column, grouped, false)); + }); + } + + private createColumn(column: Column, grouped: any[], partOfComposite: boolean) { + if (this.renderedColumns.includes(column.id)) { + if (partOfComposite) { + const $node = d3.select(this.node); + const $img = $node.select(`img[data-columnid="${column.id}"]`).node(); + if ($img !== null) { + $node.append(() => $img); //reorder the column + return; + } + } else { + return; //column already rendered + } + } + let newElement = null; + if (column instanceof NumbersColumn) { + const col = column; + newElement = this.generateImage(grouped.map((value) => { + return (value).v[(col.desc).column]; + }), CanvasTextureRenderer.getColorScale(col)); + } else if (column instanceof NumberColumn) { + const col = column; + newElement = this.generateImage(grouped.map((value) => { + return [(value).v[(col.desc).column]]; + }), CanvasTextureRenderer.getColorScale(col)); + } else if (column instanceof CategoricalsColumn) { + const col = column; + newElement = this.generateImage(grouped.map((value) => { + return (value).v[(col.desc).column]; + }), CanvasTextureRenderer.getColorScale(col)); + } else if (column instanceof CategoricalColumn) { + const col = column; + newElement = this.generateImage(grouped.map((value) => { + return [(value).v[(col.desc).column]]; + }), CanvasTextureRenderer.getColorScale(col)); + } else if ('children' in column) { + //handle composite columns + (column).children.forEach((c) => this.createColumn(c, grouped, true)); + return; + } else { + newElement = this.node.ownerDocument.createElement('img'); + } + + newElement.style.width = `${column.getWidth()}px`; + newElement.setAttribute('data-columnid', column.id); + if (partOfComposite) { + newElement.classList.add('partOfComposite'); + } + this.node.appendChild(newElement); + this.renderedColumns.push(column.id); + } + + private static getColorScale(column: Column) { + let domain = [0, 0]; + + if (column instanceof NumberColumn || column instanceof NumbersColumn) { + const colorScale = scaleLinear(); + domain = column.getMapping().domain; + if (domain[0] < 0 && domain[1] > 0) { // diverging + colorScale + .domain([domain[0], 0, domain[1]]); + } else { + colorScale + .domain([domain[0], domain[1]]); + } + colorScale.range(['white', column.color ? column.color : 'black']); + return colorScale; + } + if (column instanceof CategoricalColumn) { + const colorScale = scaleOrdinal(); + const categories = column.categories; + colorScale + .domain(categories.map((v) => v.value)) + .range(categories.map((v) => v.color)); + return colorScale; + } + return null; + } + + private generateImage(data: any[][], colorScale: any) { + const height = data.length; + let width = 0; + if(height > 0) { + width = data[0].length; + } + + this.canvas.setAttribute('height', `${height}`); + this.canvas.setAttribute('width', `${width}`); + + if (colorScale !== null) { + const ctx = this.canvas.getContext('2d'); + data.forEach((row, y) => { + row.forEach((value, x) => { + ctx.fillStyle = colorScale(value); + ctx.fillRect(x, y, 1, 1); + }); + }); + ctx.save(); + } + + const image = this.node.ownerDocument.createElement('img'); + image.src = this.canvas.toDataURL(); + + return image; + } + + destroy() { + this.node.remove(); + } + + show() { + this.node.style.display = null; + } + + hide() { + this.node.style.display = 'none'; + } +} + diff --git a/src/ui/EngineRenderer.ts b/src/ui/EngineRenderer.ts index a86112b5e..6f76e3ec3 100644 --- a/src/ui/EngineRenderer.ts +++ b/src/ui/EngineRenderer.ts @@ -16,6 +16,8 @@ import EngineRanking, {IEngineRankingContext} from './EngineRanking'; import {IRankingHeaderContext, IRankingHeaderContextContainer} from './interfaces'; import SlopeGraph, {EMode} from './SlopeGraph'; import DialogManager from './dialogs/DialogManager'; +import {default as CanvasTextureRenderer, ITextureRenderer} from './CanvasTextureRenderer'; +import * as d3 from 'd3-selection'; export default class EngineRenderer extends AEventDispatcher { @@ -37,6 +39,8 @@ export default class EngineRenderer extends AEventDispatcher { readonly idPrefix = `lu${Math.random().toString(36).slice(-8).substr(0, 3)}`; //generate a random string with length3; private enabledHighlightListening: boolean = false; + public useTextureRenderer: boolean = false; + private textureRenderer: ITextureRenderer; constructor(protected data: ADataProvider, parent: HTMLElement, options: Readonly) { super(); @@ -133,6 +137,8 @@ export default class EngineRenderer extends AEventDispatcher { }`); } + this.textureRenderer = new CanvasTextureRenderer(this.node); + this.initProvider(data); } @@ -316,45 +322,55 @@ export default class EngineRenderer extends AEventDispatcher { this.updateHist(); } - const round2 = (v: number) => round(v, 2); - const rowPadding = round2(this.zoomFactor * this.options.rowPadding!); - const groupPadding = round2(this.zoomFactor * this.options.groupPadding!); - - const heightsFor = (ranking: Ranking, data: (IGroupItem | IGroupData)[]) => { - if (this.options.dynamicHeight) { - const impl = this.options.dynamicHeight(data, ranking); - const f = (v: number | any, d: any) => typeof v === 'number' ? v : v(d); - if (impl) { - return { - defaultHeight: round2(this.zoomFactor * impl.defaultHeight), - height: (d: IGroupItem | IGroupData) => round2(this.zoomFactor * f(impl.height, d)), - padding: (d: IGroupItem | IGroupData) => round2(this.zoomFactor * f(impl.padding, d)), - }; + if (this.useTextureRenderer) { + this.hide(); + this.textureRenderer.show(); + this.textureRenderer.update(rankings, localData); + + } else { + this.textureRenderer.hide(); + this.show(); + + const round2 = (v: number) => round(v, 2); + const rowPadding = round2(this.zoomFactor * this.options.rowPadding!); + const groupPadding = round2(this.zoomFactor * this.options.groupPadding!); + + const heightsFor = (ranking: Ranking, data: (IGroupItem | IGroupData)[]) => { + if (this.options.dynamicHeight) { + const impl = this.options.dynamicHeight(data, ranking); + const f = (v: number | any, d: any) => typeof v === 'number' ? v : v(d); + if (impl) { + return { + defaultHeight: round2(this.zoomFactor * impl.defaultHeight), + height: (d: IGroupItem | IGroupData) => round2(this.zoomFactor * f(impl.height, d)), + padding: (d: IGroupItem | IGroupData) => round2(this.zoomFactor * f(impl.padding, d)), + }; + } } - } - const item = round2(this.zoomFactor * this.options.rowHeight!); - const group = round2(this.zoomFactor * this.options.groupHeight!); - return { - defaultHeight: item, - height: (d: IGroupItem | IGroupData) => isGroup(d) ? group : item, - padding: rowPadding + const item = round2(this.zoomFactor * this.options.rowHeight!); + const group = round2(this.zoomFactor * this.options.groupHeight!); + return { + defaultHeight: item, + height: (d: IGroupItem | IGroupData) => isGroup(d) ? group : item, + padding: rowPadding + }; }; - }; - rankings.forEach((r, i) => { - const grouped = r.groupData(localData[i]); + rankings.forEach((r, i) => { + const grouped = r.groupData(localData[i]); - const {height, defaultHeight, padding} = heightsFor(r.ranking, grouped); + const {height, defaultHeight, padding} = heightsFor(r.ranking, grouped); - const rowContext = nonUniformContext(grouped.map(height), defaultHeight, (index) => { - const pad = (typeof padding === 'number' ? padding : padding(grouped[index] || null)); - if (index >= 0 && grouped[index] && (isGroup(grouped[index]) || (grouped[index]).meta === 'last' || (grouped[index]).meta === 'first last')) { - return groupPadding + pad; - } - return pad; + const rowContext = nonUniformContext(grouped.map(height), defaultHeight, (index) => { + const pad = (typeof padding === 'number' ? padding : padding(grouped[index] || null)); + if (index >= 0 && grouped[index] && (isGroup(grouped[index]) || (grouped[index]).meta === 'last' || (grouped[index]).meta === 'first last')) { + return groupPadding + pad; + } + return pad; + }); + r.render(grouped, rowContext); }); - r.render(grouped, rowContext); - }); + } this.updateSlopeGraphs(rankings); @@ -422,7 +438,16 @@ export default class EngineRenderer extends AEventDispatcher { destroy() { this.takeDownProvider(); this.table.destroy(); + this.textureRenderer.destroy(); this.node.remove(); } + + show() { + d3.select(this.node).select('main').style('display', null); + } + + hide() { + d3.select(this.node).select('main').style('display', 'none'); + } } diff --git a/src/ui/taggle/Taggle.ts b/src/ui/taggle/Taggle.ts index 3af0a8589..222093b95 100644 --- a/src/ui/taggle/Taggle.ts +++ b/src/ui/taggle/Taggle.ts @@ -42,7 +42,8 @@ export default class Taggle extends ALineUp { const input = this.spaceFilling.querySelector('input'); input.onchange = () => { const selected = this.spaceFilling.classList.toggle('chosen'); - self.setTimeout(() => this.renderer.switchRule(selected ? spaceFilling : null)); + //self.setTimeout(() => this.renderer.switchRule(selected ? spaceFilling : null)); + this.renderer.useTextureRenderer(selected); }; if (this.options.overviewMode) { input.checked = true; diff --git a/src/ui/taggle/TaggleRenderer.ts b/src/ui/taggle/TaggleRenderer.ts index 8d1747685..251333ac2 100644 --- a/src/ui/taggle/TaggleRenderer.ts +++ b/src/ui/taggle/TaggleRenderer.ts @@ -135,6 +135,11 @@ export default class TaggleRenderer extends AEventDispatcher { this.update(); } + useTextureRenderer(use: boolean) { + this.renderer.useTextureRenderer = use; + this.update(); + } + destroy() { this.renderer.destroy(); window.removeEventListener('resize', this.resizeListener); From ca0dcaa70730cda5a0129e72f1600de3e7741c0a Mon Sep 17 00:00:00 2001 From: Dominik Dirmeier Date: Mon, 13 Aug 2018 07:29:49 +0200 Subject: [PATCH 02/16] implementation of the canvas texture renderer, for Caleydo/lineup_app#3 Caleydo/lineup_app#4 Caleydo/lineup_app#5 Caleydo/lineup_app#6 --- src/styles/engine/_texture_renderer.scss | 68 ++++- src/ui/CanvasTextureRenderer.ts | 348 ++++++++++++++++++++--- src/ui/EngineRanking.ts | 2 +- src/ui/EngineRenderer.ts | 97 ++++--- src/ui/SelectionManager.ts | 2 +- src/ui/taggle/Taggle.ts | 25 +- src/ui/taggle/TaggleRenderer.ts | 12 + 7 files changed, 470 insertions(+), 84 deletions(-) diff --git a/src/styles/engine/_texture_renderer.scss b/src/styles/engine/_texture_renderer.scss index 50122a6f8..c4c0c3c20 100644 --- a/src/styles/engine/_texture_renderer.scss +++ b/src/styles/engine/_texture_renderer.scss @@ -1,15 +1,71 @@ @import '../vars'; ##{$lu-css_prefix}-texture-container { - display: flex; - overflow-y: hidden; + flex-direction: row; + overflow-y: auto; overflow-x: auto; - img { - margin-right: 5px; + .rankingContainer { + display: flex; + flex-direction: column; - &.partOfComposite { - margin-right: 6px; + .row { + position: relative; + + .textureContainer { + display: flex; + + .columnContainer { + flex: 0 0 auto; + margin-right: 5px; + + &.partOfComposite { + margin-right: 6px; + } + + canvas { + width: 100%; + height: 100%; + } + } + } + + .engineRendererContainer { + > header { + display: none; + } + + > main { + overflow: hidden; + width: 100%; + } + } + } + } + + &:not(.expand) { + .engineRendererContainer:not(.always) { + display: none; } } + + .row.expanded { + .engineRendererContainer:not(.always) { + display: block; + } + } + + &.expand, + .row.expanded { + .textureContainer:not(.always) { + display: none; + } + } + + #lu-drag-overlay { + position: absolute; + background-color: #c1c1c1; + opacity: 0.4; + left: 0; + } } diff --git a/src/ui/CanvasTextureRenderer.ts b/src/ui/CanvasTextureRenderer.ts index 74b89f321..ff0b5554a 100644 --- a/src/ui/CanvasTextureRenderer.ts +++ b/src/ui/CanvasTextureRenderer.ts @@ -7,13 +7,22 @@ import NumbersColumn from '../model/NumbersColumn'; import CategoricalColumn from '../model/CategoricalColumn'; import CategoricalsColumn from '../model/CategoricalsColumn'; import CompositeColumn from '../model/CompositeColumn'; +import AggregateGroupColumn from '../model/AggregateGroupColumn'; import * as d3 from 'd3-selection'; +import * as drag from 'd3-drag'; +import {ILineUpOptions} from '../interfaces'; +import EngineRenderer from './EngineRenderer'; +import {MultiTableRowRenderer} from 'lineupengine'; export interface ITextureRenderer { update(rankings: EngineRanking[], localData: IDataRow[][]): void; + expandTextureRenderer(use: boolean): void; destroy(): void; show(): void; hide(): void; + updateSelection(dataIndices: number[]): void; + s2d(): void; + d2s(): void; } export default class CanvasTextureRenderer implements ITextureRenderer { @@ -22,14 +31,32 @@ export default class CanvasTextureRenderer implements ITextureRenderer { readonly canvas: any; readonly headerNode: HTMLElement; private renderedColumns: any[]; + private dragStartPosition: [number, number] = [0,0]; + private dragOverlay: HTMLElement; + private detailParts: any[]; + private currentRankings: EngineRanking[] = []; + private currentLocalData: IDataRow[][] = []; + private currentNodeHeight: number = 0; + private currentRankingWidths: number[] = []; + private engineRenderer: EngineRenderer; + private engineRankings: EngineRanking[] = []; + private skipUpdateEvents: number = 0; + private alreadyExpanded: boolean = false; + private expandLaterRows: any[] = []; + private readonly options: Readonly; + private readonly idPrefix = 'testprefix'; - constructor(parent: Element) { + constructor(parent: Element, engineRenderer: EngineRenderer, options: Readonly) { this.node = parent.ownerDocument.createElement('main'); this.node.id = 'lu-texture-container'; parent.appendChild(this.node); this.canvas = parent.ownerDocument.createElement('canvas'); this.headerNode = d3.select(parent).select('header').node(); + this.engineRenderer = engineRenderer; + this.options = options; this.renderedColumns = []; + this.detailParts = []; + this.dragOverlay = this.node; this.node.addEventListener('scroll', () => { { @@ -39,31 +66,148 @@ export default class CanvasTextureRenderer implements ITextureRenderer { }); } + updateSelection(dataIndices: number[]) { + const s = new Set(dataIndices); + this.engineRankings.forEach((r) => r.updateSelection(s)); + this.drawSelection(); + } + update(rankings: EngineRanking[], localData: IDataRow[][]) { + this.detailParts = []; + this.currentRankings = rankings; + this.currentLocalData = localData; + this.currentNodeHeight = this.node.offsetHeight; + let totalWidth = 0; + rankings.forEach((r, i) => { + let rankingWidth = 0; + r.ranking.flatColumns.forEach((c) => 'children' in c ? rankingWidth += (c).children.length : rankingWidth += c.getWidth() + 5); + this.currentRankingWidths[i] = rankingWidth; + totalWidth += rankingWidth; + }); + if (totalWidth > this.node.clientWidth) { + this.currentNodeHeight -= 20; + } + this.alreadyExpanded = this.node.classList.contains('expand'); + + this.renderColumns(rankings, localData); + } + + private renderColumns (rankings: EngineRanking[], localData: IDataRow[][]) { this.node.innerHTML = ''; //remove all children - this.renderedColumns = []; + this.engineRankings = []; rankings.forEach((r, i) => { - const grouped = r.groupData(localData[i]); + const dataParts = []; + const expandableParts = []; + if(this.detailParts.length === 0) { + dataParts.push(localData[i].length); + } else { + let next = 0; + this.detailParts.forEach((v) => { + const curFrom = v[0]; + const curTo = v[1]; + if (curFrom > next) { + dataParts.push(curFrom); + } + expandableParts.push(dataParts.length); + dataParts.push(curTo + 1); + next = curTo + 1; + }); + if (next < localData[i].length) { + dataParts.push(localData[i].length); + } + } + let curIndex = 0; + const rankingDiv = this.node.ownerDocument.createElement('div'); + rankingDiv.classList.add('rankingContainer'); + this.node.appendChild(rankingDiv); + dataParts.forEach((v: number, di: number) => { + const expandable = expandableParts.includes(di); + const data = localData[i].slice(curIndex, v); + const grouped = r.groupData(data); + const rowDiv = this.node.ownerDocument.createElement('div'); + rowDiv.setAttribute('data-from', `${curIndex}`); + rowDiv.setAttribute('data-to', `${v-1}`); + rowDiv.classList.add('row'); + + const textureDiv = this.node.ownerDocument.createElement('div'); + textureDiv.style.height = `${data.length / localData[i].length * this.currentNodeHeight}px`; + textureDiv.classList.add('textureContainer'); + if (!expandable) { + textureDiv.classList.add('always'); + } + this.renderedColumns = []; + r.ranking.flatColumns.forEach((column) => this.createColumn(column, grouped, textureDiv, false, expandable)); + curIndex = v; + rowDiv.appendChild(textureDiv); + rankingDiv.appendChild(rowDiv); + if (expandable) { + const expandLater = () => { + const engineRendererDiv = this.node.ownerDocument.createElement('article'); + //const id = `renderRow_${di}`; + engineRendererDiv.classList.add('engineRendererContainer'); + engineRendererDiv.style.height = `${(this.options.rowHeight + this.options.rowPadding) * data.length + 10}px`; + engineRendererDiv.style.width = `${this.currentRankingWidths[i]}px`; + + rowDiv.appendChild(engineRendererDiv); - r.ranking.flatColumns.forEach((column) => this.createColumn(column, grouped, false)); + const table = new MultiTableRowRenderer(engineRendererDiv, `#${this.idPrefix}`); + const engineRanking = table.pushTable((header, body, tableId, style) => new EngineRanking(r.ranking, header, body, tableId, style, this.engineRenderer.ctx, { + animation: this.options.animated, + customRowUpdate: this.options.customRowUpdate || (() => undefined), + levelOfDetail: this.options.levelOfDetail || (() => 'high'), + flags: this.options.flags + })); + + this.engineRenderer.render(engineRanking, data); + this.engineRankings.push(engineRanking); + engineRanking.on(EngineRanking.EVENT_UPDATE_DATA, () => this.handleUpdateEvent(r)); + this.skipUpdateEvents++; + }; + if (this.alreadyExpanded) { + expandLater(); + } else { + this.expandLaterRows.push(expandLater); + } + } + + if (expandable) { + return; + } + const that = this; + d3.select(rowDiv) + .call(drag.drag() + .on('start', (_, __, element) => { that.dragStarted(element[0]); }) + .on('drag', (_ , __, element) => { that.dragged(element[0]); }) + .on('end', (_, __, element) => { that.dragEnd(element[0]); })); + }); }); + this.drawSelection(); } - private createColumn(column: Column, grouped: any[], partOfComposite: boolean) { + private createColumn(column: Column, grouped: any[], container: HTMLElement, partOfComposite: boolean, expandable: boolean) { if (this.renderedColumns.includes(column.id)) { if (partOfComposite) { - const $node = d3.select(this.node); - const $img = $node.select(`img[data-columnid="${column.id}"]`).node(); - if ($img !== null) { - $node.append(() => $img); //reorder the column + const $container = d3.select(container); + const $col = $container.select(`.columnContainer[data-columnid="${column.id}"]`).node(); + if ($col !== null) { + $container.append(() => $col); //reorder the column return; } } else { return; //column already rendered } } - let newElement = null; + + const columnContainer = this.node.ownerDocument.createElement('div'); + columnContainer.style.width = `${column.getWidth()}px`; + columnContainer.setAttribute('data-columnid', column.id); + columnContainer.classList.add('columnContainer'); + if (partOfComposite) { + columnContainer.classList.add('partOfComposite'); + } + + let newElement = null; if (column instanceof NumbersColumn) { const col = column; newElement = this.generateImage(grouped.map((value) => { @@ -86,18 +230,22 @@ export default class CanvasTextureRenderer implements ITextureRenderer { }), CanvasTextureRenderer.getColorScale(col)); } else if ('children' in column) { //handle composite columns - (column).children.forEach((c) => this.createColumn(c, grouped, true)); + (column).children.forEach((c) => this.createColumn(c, grouped, container, true, expandable)); return; + } else if (column instanceof AggregateGroupColumn) { + newElement = this.createAggregateColumn(column.getWidth(), grouped.length, expandable); + if (expandable) { + newElement.onclick = () => { + newElement.parentNode.parentNode.parentNode.classList.add('expanded'); + }; + } } else { - newElement = this.node.ownerDocument.createElement('img'); + newElement = this.node.ownerDocument.createElement('canvas'); } - newElement.style.width = `${column.getWidth()}px`; - newElement.setAttribute('data-columnid', column.id); - if (partOfComposite) { - newElement.classList.add('partOfComposite'); - } - this.node.appendChild(newElement); + columnContainer.appendChild(newElement); + + container.appendChild(columnContainer); this.renderedColumns.push(column.id); } @@ -134,25 +282,100 @@ export default class CanvasTextureRenderer implements ITextureRenderer { if(height > 0) { width = data[0].length; } + const canvas = this.node.ownerDocument.createElement('canvas'); + canvas.setAttribute('height', `${height}`); + canvas.setAttribute('width', `${width}`); + this.drawOntoCanvas(data, colorScale, canvas); + return canvas; + } - this.canvas.setAttribute('height', `${height}`); - this.canvas.setAttribute('width', `${width}`); - - if (colorScale !== null) { - const ctx = this.canvas.getContext('2d'); - data.forEach((row, y) => { - row.forEach((value, x) => { - ctx.fillStyle = colorScale(value); - ctx.fillRect(x, y, 1, 1); - }); + private drawOntoCanvas(data: any[][], colorScale: any, canvas: any) { + if (colorScale === null) { + return; + } + const ctx = canvas.getContext('2d'); + data.forEach((row, y) => { + row.forEach((value, x) => { + ctx.fillStyle = colorScale(value); + ctx.fillRect(x, y, 1, 1); }); - ctx.save(); + }); + ctx.save(); + } + + private createAggregateColumn(width: number, height: number, expandable: boolean) { + const canvas = this.node.ownerDocument.createElement('canvas'); + canvas.setAttribute('height', `${height}`); + canvas.setAttribute('width', `${width}`); + canvas.classList.add('aggregateColumn'); + const ctx = canvas.getContext('2d'); + + if (expandable) { + ctx.fillStyle = '#499cff'; + } else { + ctx.fillStyle = '#000000'; } + ctx.fillRect(width-2, 0, 2, height); + ctx.save(); + return canvas; + } - const image = this.node.ownerDocument.createElement('img'); - image.src = this.canvas.toDataURL(); + expandTextureRenderer(use: boolean) { + d3.select(this.node).classed('expand', use); + if (!this.alreadyExpanded) { + this.expandLaterRows.forEach((r) => r()); + this.alreadyExpanded = true; + } + } - return image; + private dragStarted(element: any) { + this.dragStartPosition = d3.mouse(element); + this.dragOverlay = element.ownerDocument.createElement('div'); + this.dragOverlay.id = 'lu-drag-overlay'; + this.dragOverlay.style.width = `${element.scrollWidth}px`; + element.appendChild(this.dragOverlay); + } + + private dragged(element: any) { + const currentPosition = d3.mouse(element); + if (this.dragStartPosition[1] < currentPosition[1]) { + this.dragOverlay.style.top = `${this.dragStartPosition[1]}px`; + this.dragOverlay.style.height = `${currentPosition[1]-this.dragStartPosition[1]}px`; + } else { + this.dragOverlay.style.top = `${currentPosition[1]}px`; + this.dragOverlay.style.height = `${this.dragStartPosition[1]-currentPosition[1]}px`; + } + } + + private dragEnd(element: any) { + this.dragOverlay.remove(); + const currentPosition = d3.mouse(element); + if(currentPosition[1] === this.dragStartPosition[1]) { + if (!d3.event.sourceEvent.ctrlKey) { + this.detailParts = []; + this.renderColumns(this.currentRankings, this.currentLocalData); + } + return; + } + const from = Math.min(currentPosition[1], this.dragStartPosition[1]); + const to = Math.max(currentPosition[1], this.dragStartPosition[1]); + const fromData = parseInt(element.getAttribute('data-from'), 10); + const toData = parseInt(element.getAttribute('data-to'), 10); + const fromIndex = Math.max(Math.floor(from / element.offsetHeight * (toData - fromData) + fromData), fromData); + const toIndex = Math.min(Math.ceil(to / element.offsetHeight * (toData - fromData) + fromData), toData); + if (fromIndex > toIndex) { + return; + } + if (!d3.event.sourceEvent.ctrlKey) { + this.detailParts = []; + } + const i = this.detailParts.findIndex((v) => v[0] >= toIndex); + if(i === -1) { + this.detailParts.push([fromIndex, toIndex]); + } else { + this.detailParts.splice(i, 0, [fromIndex, toIndex]); + } + this.renderColumns(this.currentRankings, this.currentLocalData); } destroy() { @@ -166,5 +389,64 @@ export default class CanvasTextureRenderer implements ITextureRenderer { hide() { this.node.style.display = 'none'; } -} + s2d() { + this.detailParts = []; + let startIndex = -1; + for (let i = 0; i < this.currentLocalData[0].length; i++) { + if (this.engineRenderer.ctx.provider.isSelected(this.currentLocalData[0][i].i)) { + if (startIndex === -1) { + startIndex = i; + } + } else if (startIndex !== -1) { + this.detailParts.push([startIndex, i-1]); + startIndex = -1; + } + } + if (startIndex !== -1) { + this.detailParts.push([startIndex, this.currentLocalData[0].length-1]); + } + this.renderColumns(this.currentRankings, this.currentLocalData); + } + + d2s() { + d3.select(this.node).selectAll('.engineRendererContainer').nodes().forEach((v: any, i: number) => { + const first = d3.select(v).select('.lu-row:first-child').node(); + const last = d3.select(v).select('.lu-row:last-child').node(); + if (typeof first !== 'undefined' && typeof last !== 'undefined') { + this.engineRankings[i].selection.select(i !== 0, first, last); + } + }); + } + + drawSelection() { + d3.select(this.node).selectAll('.aggregateColumn').nodes().forEach((v: any) => { + let parent = v.parentElement; + while (!parent.classList.contains('row')) { + parent = parent.parentElement; + } + const fromIndex = parseInt(parent.getAttribute('data-from'), 10); + const toIndex = parseInt(parent.getAttribute('data-to'), 10); + + const ctx = v.getContext('2d'); + + for (let i = fromIndex; i <= toIndex; i++) { + if (this.engineRenderer.ctx.provider.isSelected(this.currentLocalData[0][i].i)) { + ctx.fillStyle = '#ffa809'; + } else { + ctx.fillStyle = '#ffffff'; + } + ctx.fillRect(2, i, 2, 1); + } + ctx.save(); + }); + } + + private handleUpdateEvent (r: EngineRanking) { + if (this.skipUpdateEvents > 0) { + this.skipUpdateEvents--; + } else { + this.engineRenderer.update([r]); + } + } +} diff --git a/src/ui/EngineRanking.ts b/src/ui/EngineRanking.ts index 4454e4afe..619f33f75 100644 --- a/src/ui/EngineRanking.ts +++ b/src/ui/EngineRanking.ts @@ -55,7 +55,7 @@ export default class EngineRanking extends ACellTableSection imple private readonly renderCtx: IRankingBodyContext; private data: (IGroupItem | IGroupData)[] = []; - private readonly selection: SelectionManager; + readonly selection: SelectionManager; private highlight: number = -1; private readonly canvasPool: HTMLCanvasElement[] = []; diff --git a/src/ui/EngineRenderer.ts b/src/ui/EngineRenderer.ts index 6f76e3ec3..0ecde5f96 100644 --- a/src/ui/EngineRenderer.ts +++ b/src/ui/EngineRenderer.ts @@ -41,6 +41,8 @@ export default class EngineRenderer extends AEventDispatcher { private enabledHighlightListening: boolean = false; public useTextureRenderer: boolean = false; private textureRenderer: ITextureRenderer; + private groupPadding: any = null; + private heightsFor: any = null; constructor(protected data: ADataProvider, parent: HTMLElement, options: Readonly) { super(); @@ -137,7 +139,7 @@ export default class EngineRenderer extends AEventDispatcher { }`); } - this.textureRenderer = new CanvasTextureRenderer(this.node); + this.textureRenderer = new CanvasTextureRenderer(this.node, this, this.options); this.initProvider(data); } @@ -214,7 +216,7 @@ export default class EngineRenderer extends AEventDispatcher { private updateSelection(dataIndices: number[]) { const s = new Set(dataIndices); this.rankings.forEach((r) => r.updateSelection(s)); - + this.textureRenderer.updateSelection(dataIndices); this.slopeGraphs.forEach((r) => r.updateSelection(s)); } @@ -277,6 +279,10 @@ export default class EngineRenderer extends AEventDispatcher { this.update([r]); } + expandTextureRenderer(use: boolean) { + this.textureRenderer.expandTextureRenderer(use); + } + private updateRotatedHeaderState() { if (this.options.labelRotation === 0) { return; @@ -322,6 +328,31 @@ export default class EngineRenderer extends AEventDispatcher { this.updateHist(); } + const round2 = (v: number) => round(v, 2); + const rowPadding = round2(this.zoomFactor * this.options.rowPadding!); + this.groupPadding = round2(this.zoomFactor * this.options.groupPadding!); + + this.heightsFor = (ranking: Ranking, data: (IGroupItem | IGroupData)[]) => { + if (this.options.dynamicHeight) { + const impl = this.options.dynamicHeight(data, ranking); + const f = (v: number | any, d: any) => typeof v === 'number' ? v : v(d); + if (impl) { + return { + defaultHeight: round2(this.zoomFactor * impl.defaultHeight), + height: (d: IGroupItem | IGroupData) => round2(this.zoomFactor * f(impl.height, d)), + padding: (d: IGroupItem | IGroupData) => round2(this.zoomFactor * f(impl.padding, d)), + }; + } + } + const item = round2(this.zoomFactor * this.options.rowHeight!); + const group = round2(this.zoomFactor * this.options.groupHeight!); + return { + defaultHeight: item, + height: (d: IGroupItem | IGroupData) => isGroup(d) ? group : item, + padding: rowPadding + }; + }; + if (this.useTextureRenderer) { this.hide(); this.textureRenderer.show(); @@ -331,44 +362,8 @@ export default class EngineRenderer extends AEventDispatcher { this.textureRenderer.hide(); this.show(); - const round2 = (v: number) => round(v, 2); - const rowPadding = round2(this.zoomFactor * this.options.rowPadding!); - const groupPadding = round2(this.zoomFactor * this.options.groupPadding!); - - const heightsFor = (ranking: Ranking, data: (IGroupItem | IGroupData)[]) => { - if (this.options.dynamicHeight) { - const impl = this.options.dynamicHeight(data, ranking); - const f = (v: number | any, d: any) => typeof v === 'number' ? v : v(d); - if (impl) { - return { - defaultHeight: round2(this.zoomFactor * impl.defaultHeight), - height: (d: IGroupItem | IGroupData) => round2(this.zoomFactor * f(impl.height, d)), - padding: (d: IGroupItem | IGroupData) => round2(this.zoomFactor * f(impl.padding, d)), - }; - } - } - const item = round2(this.zoomFactor * this.options.rowHeight!); - const group = round2(this.zoomFactor * this.options.groupHeight!); - return { - defaultHeight: item, - height: (d: IGroupItem | IGroupData) => isGroup(d) ? group : item, - padding: rowPadding - }; - }; - rankings.forEach((r, i) => { - const grouped = r.groupData(localData[i]); - - const {height, defaultHeight, padding} = heightsFor(r.ranking, grouped); - - const rowContext = nonUniformContext(grouped.map(height), defaultHeight, (index) => { - const pad = (typeof padding === 'number' ? padding : padding(grouped[index] || null)); - if (index >= 0 && grouped[index] && (isGroup(grouped[index]) || (grouped[index]).meta === 'last' || (grouped[index]).meta === 'first last')) { - return groupPadding + pad; - } - return pad; - }); - r.render(grouped, rowContext); + this.render(r, localData[i]); }); } @@ -378,6 +373,22 @@ export default class EngineRenderer extends AEventDispatcher { this.table.widthChanged(); } + render(r: EngineRanking, localData: IDataRow[]) { + const grouped = r.groupData(localData); + + const {height, defaultHeight, padding} = this.heightsFor(r.ranking, grouped); + + const that = this; + const rowContext = nonUniformContext(grouped.map(height), defaultHeight, (index) => { + const pad = (typeof padding === 'number' ? padding : padding(grouped[index] || null)); + if (index >= 0 && grouped[index] && (isGroup(grouped[index]) || (grouped[index]).meta === 'last' || (grouped[index]).meta === 'first last')) { + return that.groupPadding + pad; + } + return pad; + }); + r.render(grouped, rowContext); + } + private updateSlopeGraphs(rankings: EngineRanking[] = this.rankings) { const indices = new Set(rankings.map((d) => this.rankings.indexOf(d))); this.slopeGraphs.forEach((s, i) => { @@ -449,5 +460,13 @@ export default class EngineRenderer extends AEventDispatcher { hide() { d3.select(this.node).select('main').style('display', 'none'); } + + s2d() { + this.textureRenderer.s2d(); + } + + d2s() { + this.textureRenderer.d2s(); + } } diff --git a/src/ui/SelectionManager.ts b/src/ui/SelectionManager.ts index c1f727414..9085dc5a2 100644 --- a/src/ui/SelectionManager.ts +++ b/src/ui/SelectionManager.ts @@ -79,7 +79,7 @@ export default class SelectionManager extends AEventDispatcher { return super.createEventList().concat([SelectionManager.EVENT_SELECT_RANGE]); } - private select(additional: boolean, startNode?: HTMLElement, endNode?: HTMLElement) { + select(additional: boolean, startNode?: HTMLElement, endNode?: HTMLElement) { if (!startNode || !endNode || startNode === endNode) { return; // no single } diff --git a/src/ui/taggle/Taggle.ts b/src/ui/taggle/Taggle.ts index 222093b95..d6308d91e 100644 --- a/src/ui/taggle/Taggle.ts +++ b/src/ui/taggle/Taggle.ts @@ -33,23 +33,40 @@ export default class Taggle extends ALineUp { this.node.insertBefore(this.panel.node, this.node.firstChild); { this.panel.node.insertAdjacentHTML('afterbegin', `
`); const spaceFilling = spaceFillingRule(this.options); this.spaceFilling = this.node.querySelector('.lu-rule-button-chooser')!; - const input = this.spaceFilling.querySelector('input'); - input.onchange = () => { + const ruleInput = this.spaceFilling.querySelector('input.spaceFilling'); + ruleInput.onchange = () => { const selected = this.spaceFilling.classList.toggle('chosen'); //self.setTimeout(() => this.renderer.switchRule(selected ? spaceFilling : null)); this.renderer.useTextureRenderer(selected); }; if (this.options.overviewMode) { - input.checked = true; + ruleInput.checked = true; this.spaceFilling.classList.toggle('chosen'); this.renderer.switchRule(spaceFilling); } + const expandInput = this.spaceFilling.querySelector('input.expand'); + expandInput.onchange = () => { + const selected = expandInput.checked; + this.renderer.expandTextureRenderer(selected); + }; + const s2dInput = this.spaceFilling.querySelector('input.s2d'); + s2dInput.onclick = () => { + this.renderer.s2d(); + }; + const d2sInput = this.spaceFilling.querySelector('input.d2s'); + d2sInput.onclick = () => { + this.renderer.d2s(); + }; } this.forward(this.renderer, `${ALineUp.EVENT_HIGHLIGHT_CHANGED}.main`); } diff --git a/src/ui/taggle/TaggleRenderer.ts b/src/ui/taggle/TaggleRenderer.ts index 251333ac2..d888b5f93 100644 --- a/src/ui/taggle/TaggleRenderer.ts +++ b/src/ui/taggle/TaggleRenderer.ts @@ -140,6 +140,10 @@ export default class TaggleRenderer extends AEventDispatcher { this.update(); } + expandTextureRenderer(use: boolean) { + this.renderer.expandTextureRenderer(use); + } + destroy() { this.renderer.destroy(); window.removeEventListener('resize', this.resizeListener); @@ -174,4 +178,12 @@ export default class TaggleRenderer extends AEventDispatcher { enableHighlightListening(enable: boolean) { this.renderer.enableHighlightListening(enable); } + + d2s() { + this.renderer.d2s(); + } + + s2d() { + this.renderer.s2d(); + } } From 957530fdfb09bbd7c5ac23c16c3288014c6496d4 Mon Sep 17 00:00:00 2001 From: Dominik Dirmeier Date: Sun, 26 Aug 2018 23:26:51 +0200 Subject: [PATCH 03/16] added OverviewDetail column, for Caleydo/lineup_app#3 Caleydo/lineup_app#4 Caleydo/lineup_app#5 Caleydo/lineup_app#6 --- src/model/OverviewDetailColumn.ts | 98 ++++++++ src/model/index.ts | 2 + src/provider/ACommonDataProvider.ts | 2 + src/provider/ADataProvider.ts | 118 ++++++++- src/renderer/OverviewDetailRenderer.ts | 113 +++++++++ src/renderer/index.ts | 2 + src/styles/engine/_header.scss | 3 +- src/styles/engine/_texture_renderer.scss | 6 +- src/styles/renderer/_detail.scss | 25 ++ src/styles/renderer/_index.scss | 1 + src/ui/CanvasTextureRenderer.ts | 293 ++++++++++++++++------- src/ui/EngineRanking.ts | 16 ++ src/ui/EngineRenderer.ts | 16 +- 13 files changed, 597 insertions(+), 98 deletions(-) create mode 100644 src/model/OverviewDetailColumn.ts create mode 100644 src/renderer/OverviewDetailRenderer.ts create mode 100644 src/styles/renderer/_detail.scss diff --git a/src/model/OverviewDetailColumn.ts b/src/model/OverviewDetailColumn.ts new file mode 100644 index 000000000..5288e4763 --- /dev/null +++ b/src/model/OverviewDetailColumn.ts @@ -0,0 +1,98 @@ +import {Category, SupportType, toolbar} from './annotations'; +import {IDataRow, IGroup} from './interfaces'; +import ValueColumn, {IValueColumnDesc} from './ValueColumn'; + +/** + * factory for creating a description creating a rank column + * @param label + * @returns {{type: string, label: string}} + */ +export function createDetailDesc(label: string = 'Detail Checkboxes') { + return {type: 'detail', label, fixed: true}; +} + +export interface IDetailColumnDesc extends IValueColumnDesc { + /** + * setter for selecting/deselecting the given row + */ + setter(row: IDataRow, value: boolean): void; + + /** + * setter for selecting/deselecting the given row + */ + setterAll(rows: IDataRow[], value: boolean): void; +} + +/** + * a checkbox column for selections + */ +@SupportType() +@toolbar('sort', 'stratify') +@Category('support') +export default class OverviewDetailColumn extends ValueColumn { + private static DETAILED_GROUP: IGroup = { + name: 'Detailed', + color: 'blue' + }; + private static NOT_DETAILED_GROUP: IGroup = { + name: 'Undetailed', + color: 'gray' + }; + static readonly EVENT_DETAIL = 'detail'; + + constructor(id: string, desc: Readonly) { + super(id, desc); + this.setDefaultWidth(20); + } + + get frozen() { + return this.desc.frozen !== false; + } + + protected createEventList() { + return super.createEventList().concat([OverviewDetailColumn.EVENT_DETAIL]); + } + + setValue(row: IDataRow, value: boolean) { + const old = this.getValue(row); + if (old === value) { + return true; + } + return this.setImpl(row, value); + } + + setValues(rows: IDataRow[], value: boolean) { + if (rows.length === 0) { + return; + } + if ((this.desc).setterAll) { + (this.desc).setterAll(rows, value); + } + this.fire(OverviewDetailColumn.EVENT_DETAIL, rows[0], value, rows); + return true; + } + + private setImpl(row: IDataRow, value: boolean) { + if ((this.desc).setter) { + (this.desc).setter(row, value); + } + this.fire(OverviewDetailColumn.EVENT_DETAIL, row, value); + return true; + } + + toggleValue(row: IDataRow) { + const old = this.getValue(row); + this.setImpl(row, !old); + return !old; + } + + compare(a: IDataRow, b: IDataRow) { + const va = this.getValue(a) === true; + const vb = this.getValue(b) === true; + return va === vb ? 0 : (va < vb ? -1 : +1); + } + + group(row: IDataRow) { + const isSelected = this.getValue(row); + return isSelected ? OverviewDetailColumn.DETAILED_GROUP : OverviewDetailColumn.NOT_DETAILED_GROUP; } +} diff --git a/src/model/index.ts b/src/model/index.ts index 70384a676..e2407eb69 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -31,6 +31,7 @@ import StringMapColumn from './StringMapColumn'; import StringsColumn from './StringsColumn'; import ValueColumn, {IValueColumnDesc} from './ValueColumn'; import ImpositionBoxPlotColumn from './ImpositionBoxPlotColumn'; +import OverviewDetailColumn from './OverviewDetailColumn'; export {isSupportType, Category, SupportType} from './annotations'; export {isMissingValue, isUnknown, FIRST_IS_NAN, missingGroup} from './missing'; @@ -166,6 +167,7 @@ export function models() { date: DateColumn, dateMap: DatesMapColumn, dates: DatesColumn, + detail: OverviewDetailColumn, group: GroupColumn, hierarchy: HierarchyColumn, imposition: ImpositionCompositeColumn, diff --git a/src/provider/ACommonDataProvider.ts b/src/provider/ACommonDataProvider.ts index 8b98f5851..b66b8f3db 100644 --- a/src/provider/ACommonDataProvider.ts +++ b/src/provider/ACommonDataProvider.ts @@ -2,6 +2,7 @@ import {createAggregateDesc, createRankDesc, createSelectionDesc, IColumnDesc, I import {IOrderedGroup} from '../model/Group'; import Ranking from '../model/Ranking'; import ADataProvider, {IDataProviderOptions} from './ADataProvider'; +import {createDetailDesc} from '../model/OverviewDetailColumn'; function isComplexAccessor(column: any) { @@ -164,6 +165,7 @@ abstract class ACommonDataProvider extends ADataProvider { if (this.multiSelections) { r.push(this.create(createSelectionDesc())!); } + r.push(this.create(createDetailDesc())!); } this.getColumns().forEach((col) => { const c = this.create(col); diff --git a/src/provider/ADataProvider.ts b/src/provider/ADataProvider.ts index 868c23993..0360e24aa 100644 --- a/src/provider/ADataProvider.ts +++ b/src/provider/ADataProvider.ts @@ -14,6 +14,7 @@ import Ranking from '../model/Ranking'; import StackColumn from '../model/StackColumn'; import {exportRanking, IExportOptions} from './utils'; import {isSupportType} from '../model/annotations'; +import {createDetailDesc} from '../model/OverviewDetailColumn'; export {IExportOptions} from './utils'; @@ -50,6 +51,16 @@ export interface IDataProvider extends AEventDispatcher { isSelected(i: number): boolean; + detailAllOf(ranking: Ranking): void; + + getDetail(): number[]; + + setDetail(dataIndices: number[]): void; + + toggleDetail(i: number, additional?: boolean): boolean; + + isDetail(i: number): boolean; + removeRanking(ranking: Ranking): void; ensureOneRanking(): void; @@ -85,6 +96,7 @@ export interface IDataProvider extends AEventDispatcher { */ abstract class ADataProvider extends AEventDispatcher implements IDataProvider { static readonly EVENT_SELECTION_CHANGED = 'selectionChanged'; + static readonly EVENT_DETAIL_CHANGED = 'detailChanged'; static readonly EVENT_ADD_COLUMN = Ranking.EVENT_ADD_COLUMN; static readonly EVENT_REMOVE_COLUMN = Ranking.EVENT_REMOVE_COLUMN; static readonly EVENT_ADD_RANKING = 'addRanking'; @@ -110,6 +122,8 @@ abstract class ADataProvider extends AEventDispatcher implements IDataProvider { */ private readonly selection = new OrderedSet(); + private readonly detail = new OrderedSet(); + //ranking.id@group.name private aggregations = new Set(); @@ -141,7 +155,7 @@ abstract class ADataProvider extends AEventDispatcher implements IDataProvider { ADataProvider.EVENT_ADD_COLUMN, ADataProvider.EVENT_REMOVE_COLUMN, ADataProvider.EVENT_ADD_RANKING, ADataProvider.EVENT_REMOVE_RANKING, ADataProvider.EVENT_DIRTY, ADataProvider.EVENT_DIRTY_HEADER, ADataProvider.EVENT_DIRTY_VALUES, - ADataProvider.EVENT_ORDER_CHANGED, ADataProvider.EVENT_SELECTION_CHANGED, + ADataProvider.EVENT_ORDER_CHANGED, ADataProvider.EVENT_SELECTION_CHANGED, ADataProvider.EVENT_DETAIL_CHANGED, ADataProvider.EVENT_ADD_DESC, ADataProvider.EVENT_CLEAR_DESC, ADataProvider.EVENT_JUMP_TO_NEAREST, ADataProvider.EVENT_GROUP_AGGREGATION_CHANGED]); } @@ -335,6 +349,10 @@ abstract class ADataProvider extends AEventDispatcher implements IDataProvider { (desc).accessor = (row: IDataRow) => this.isSelected(row.i); (desc).setter = (row: IDataRow, value: boolean) => value ? this.select(row.i) : this.deselect(row.i); (desc).setterAll = (rows: IDataRow[], value: boolean) => value ? this.selectAll(rows.map((d) => d.i)) : this.deselectAll(rows.map((d) => d.i)); + } else if (desc.type === 'detail') { + (desc).accessor = (row: IDataRow) => this.isDetail(row.i); + (desc).setter = (row: IDataRow, value: boolean) => value ? this.addDetail(row.i) : this.removeDetail(row.i); + (desc).setterAll = (rows: IDataRow[], value: boolean) => value ? this.addDetailAll(rows.map((d) => d.i)) : this.removeDetailAll(rows.map((d) => d.i)); } else if (desc.type === 'aggregate') { (desc).isAggregated = (ranking: Ranking, group: IGroup) => this.isAggregated(ranking, group); (desc).setAggregated = (ranking: Ranking, group: IGroup, value: boolean) => this.setAggregated(ranking, group, value); @@ -411,6 +429,7 @@ abstract class ADataProvider extends AEventDispatcher implements IDataProvider { return { uid: this.uid, selection: this.getSelection(), + detail: this.getDetail(), aggregations: Array.from(this.aggregations), rankings: this.rankings.map((r) => r.dump(this.toDescRef)) }; @@ -472,6 +491,10 @@ abstract class ADataProvider extends AEventDispatcher implements IDataProvider { if (dump.selection) { dump.selection.forEach((s: number) => this.selection.add(s)); } + //restore detail + if (dump.detail) { + dump.detail.forEach((s: number) => this.detail.add(s)); + } if (dump.aggregations) { this.aggregations.clear(); dump.aggregations.forEach((a: string) => this.aggregations.add(a)); @@ -519,6 +542,8 @@ abstract class ADataProvider extends AEventDispatcher implements IDataProvider { return this.create(createRankDesc()); case 'selection': return this.create(createSelectionDesc()); + case 'detail': + return this.create(createDetailDesc()); case 'group': return this.create(createGroupDesc()); case 'aggregate': @@ -659,6 +684,10 @@ abstract class ADataProvider extends AEventDispatcher implements IDataProvider { return this.selection.has(index); } + isDetail(index: number) { + return this.detail.has(index); + } + /** * also select the given row * @param index @@ -674,6 +703,14 @@ abstract class ADataProvider extends AEventDispatcher implements IDataProvider { this.fire(ADataProvider.EVENT_SELECTION_CHANGED, this.getSelection()); } + addDetail(index: number) { + if (this.detail.has(index)) { + return; //no change + } + this.detail.add(index); + this.fire(ADataProvider.EVENT_DETAIL_CHANGED, this.getDetail()); + } + /** * hook for selecting elements matching the given arguments * @param search @@ -706,10 +743,24 @@ abstract class ADataProvider extends AEventDispatcher implements IDataProvider { this.fire(ADataProvider.EVENT_SELECTION_CHANGED, this.getSelection()); } + addDetailAll(indices: number[]) { + if (indices.every((i) => this.detail.has(i))) { + return; //no change + } + indices.forEach((index) => { + this.detail.add(index); + }); + this.fire(ADataProvider.EVENT_DETAIL_CHANGED, this.getDetail()); + } + selectAllOf(ranking: Ranking) { this.setSelection(ranking.getOrder()); } + detailAllOf(ranking: Ranking) { + this.setDetail(ranking.getOrder()); + } + /** * set the selection to the given rows * @param indices @@ -725,6 +776,17 @@ abstract class ADataProvider extends AEventDispatcher implements IDataProvider { this.selectAll(indices); } + setDetail(indices: number[]) { + if (indices.length === 0) { + return this.clearDetail(); + } + if (this.detail.size === indices.length && indices.every((i) => this.detail.has(i))) { + return; //no change + } + this.detail.clear(); + this.addDetailAll(indices); + } + /** * toggles the selection of the given data index * @param index @@ -748,6 +810,23 @@ abstract class ADataProvider extends AEventDispatcher implements IDataProvider { return true; } + toggleDetail(index: number, additional = false) { + if (this.isDetail(index)) { + if (additional) { + this.removeDetail(index); + } else { + this.clearDetail(); + } + return false; + } + if (additional) { + this.addDetail(index); + } else { + this.setDetail([index]); + } + return true; + } + /** * deselect the given row * @param index @@ -760,6 +839,14 @@ abstract class ADataProvider extends AEventDispatcher implements IDataProvider { this.fire(ADataProvider.EVENT_SELECTION_CHANGED, this.getSelection()); } + removeDetail(index: number) { + if (!this.detail.has(index)) { + return; //no change + } + this.detail.delete(index); + this.fire(ADataProvider.EVENT_DETAIL_CHANGED, this.getDetail()); + } + /** * also select all the given rows * @param indices @@ -774,6 +861,16 @@ abstract class ADataProvider extends AEventDispatcher implements IDataProvider { this.fire(ADataProvider.EVENT_SELECTION_CHANGED, this.getSelection()); } + removeDetailAll(indices: number[]) { + if (indices.every((i) => !this.detail.has(i))) { + return; //no change + } + indices.forEach((index) => { + this.detail.delete(index); + }); + this.fire(ADataProvider.EVENT_DETAIL_CHANGED, this.getDetail()); + } + /** * returns a promise containing the selected rows * @return {Promise} @@ -785,6 +882,13 @@ abstract class ADataProvider extends AEventDispatcher implements IDataProvider { return this.view(this.getSelection()); } + detailRows(): Promise | any[] { + if (this.detail.size === 0) { + return []; + } + return this.view(this.getDetail()); + } + /** * returns the currently selected indices * @returns {Array} @@ -793,6 +897,10 @@ abstract class ADataProvider extends AEventDispatcher implements IDataProvider { return Array.from(this.selection); } + getDetail() { + return Array.from(this.detail); + } + /** * clears the selection */ @@ -804,6 +912,14 @@ abstract class ADataProvider extends AEventDispatcher implements IDataProvider { this.fire(ADataProvider.EVENT_SELECTION_CHANGED, [], false); } + clearDetail() { + if (this.detail.size === 0) { + return; //no change + } + this.detail.clear(); + this.fire(ADataProvider.EVENT_DETAIL_CHANGED, [], false); + } + /** * utility to export a ranking to a table with the given separator * @param ranking diff --git a/src/renderer/OverviewDetailRenderer.ts b/src/renderer/OverviewDetailRenderer.ts new file mode 100644 index 000000000..8ee544fe6 --- /dev/null +++ b/src/renderer/OverviewDetailRenderer.ts @@ -0,0 +1,113 @@ +import {IDataRow, IGroup} from '../model'; +import Column from '../model/Column'; +import {default as IRenderContext, ICellRendererFactory} from './interfaces'; +import {noop} from './utils'; +import {IDataProvider} from '../provider/ADataProvider'; +import OverviewDetailColumn from '../model/OverviewDetailColumn'; + +/** @internal */ +export default class OverviewDetailRenderer implements ICellRendererFactory { + readonly title = 'Default'; + + canRender(col: Column) { + return col instanceof OverviewDetailColumn; + } + + create(col: OverviewDetailColumn, ctx: IRenderContext) { + return { + template: `
`, + update: (n: HTMLElement, d: IDataRow, i: number) => { + n.onclick = function (event) { + event.preventDefault(); + event.stopPropagation(); + if (event.shiftKey) { + const ranking = col.findMyRanker()!.id; + if (rangeSelection(ctx.provider, ranking, d.i, i, event.ctrlKey)) { + return; + } + } + + col.toggleValue(d); + }; + }, + render: noop + }; + } + + createGroup(col: OverviewDetailColumn) { + return { + template: `
`, + update: (n: HTMLElement, _group: IGroup, rows: IDataRow[]) => { + const selected = rows.reduce((act, r) => col.getValue(r) ? act + 1 : act, 0); + const all = selected >= rows.length / 2; + if (all) { + n.classList.add('lu-group-selected'); + } else { + n.classList.remove('lu-group-selected'); + } + n.onclick = function (event) { + event.preventDefault(); + event.stopPropagation(); + const value = n.classList.toggle('lu-group-selected'); + col.setValues(rows, value); + }; + } + }; + } + + createSummary(col: OverviewDetailColumn, context: IRenderContext) { + return { + template: `
`, + update: (node: HTMLElement) => { + node.onclick = (evt) => { + evt.stopPropagation(); + const icon = node.dataset.icon; + if (icon === 'unchecked') { + context.provider.detailAllOf(col.findMyRanker()!); + node.dataset.icon = 'checked'; + } else { + context.provider.setDetail([]); + node.dataset.icon = 'unchecked'; + } + }; + } + }; + } +} + +/** @internal */ +export function rangeSelection(provider: IDataProvider, rankingId: string, dataIndex: number, relIndex: number, ctrlKey: boolean) { + const ranking = provider.getRankings().find((d) => d.id === rankingId); + if (!ranking) { // no known reference + return false; + } + const selection = provider.getDetail(); + if (selection.length === 0 || selection.includes(dataIndex)) { + return false; // no other or deselect + } + const order = ranking.getOrder(); + const lookup = new Map(ranking.getOrder().map((d, i) => <[number, number]>[d, i])); + const distances = selection.map((d) => { + const index = (lookup.has(d) ? lookup.get(d)! : Infinity); + return {s: d, index, distance: Math.abs(relIndex - index)}; + }); + const nearest = distances.sort((a, b) => a.distance - b.distance)[0]!; + if (!isFinite(nearest.distance)) { + return false; // all outside + } + if (!ctrlKey) { + selection.splice(0, selection.length); + selection.push(nearest.s); + } + if (nearest.index < relIndex) { + for(let i = nearest.index + 1; i <= relIndex; ++i) { + selection.push(order[i]); + } + } else { + for(let i = relIndex; i <= nearest.index; ++i) { + selection.push(order[i]); + } + } + provider.setDetail(selection); + return true; +} diff --git a/src/renderer/index.ts b/src/renderer/index.ts index 71ecb3337..eeb439720 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -30,6 +30,7 @@ import StringCellRenderer from './StringCellRenderer'; import TableCellRenderer from './TableCellRenderer'; import UpSetCellRenderer from './UpSetCellRenderer'; import VerticalBarCellRenderer from './VerticalBarCellRenderer'; +import OverviewDetailRenderer from './OverviewDetailRenderer'; export { default as IRenderContext, @@ -55,6 +56,7 @@ export const renderers: { [key: string]: ICellRendererFactory } = { categorical: new CategoricalCellRenderer(), circle: new CircleCellRenderer(), default: defaultCellRenderer, + detail: new OverviewDetailRenderer(), dot: new DotCellRenderer(), group: new GroupCellRenderer(), heatmap: new HeatmapCellRenderer(), diff --git a/src/styles/engine/_header.scss b/src/styles/engine/_header.scss index e3f8bf1fb..23a8c18b9 100644 --- a/src/styles/engine/_header.scss +++ b/src/styles/engine/_header.scss @@ -68,7 +68,8 @@ section.#{$lu_css_prefix}-header { } &[data-renderer=aggregate], - &[data-renderer=selection] { + &[data-renderer=selection], + &[data-renderer=detail] { display: block; // have just a before element } } diff --git a/src/styles/engine/_texture_renderer.scss b/src/styles/engine/_texture_renderer.scss index c4c0c3c20..d311e1c9a 100644 --- a/src/styles/engine/_texture_renderer.scss +++ b/src/styles/engine/_texture_renderer.scss @@ -9,7 +9,7 @@ display: flex; flex-direction: column; - .row { + .rowContainer { position: relative; .textureContainer { @@ -49,14 +49,14 @@ } } - .row.expanded { + .rowContainer.expanded { .engineRendererContainer:not(.always) { display: block; } } &.expand, - .row.expanded { + .rowContainer.expanded { .textureContainer:not(.always) { display: none; } diff --git a/src/styles/renderer/_detail.scss b/src/styles/renderer/_detail.scss new file mode 100644 index 000000000..afcf808e5 --- /dev/null +++ b/src/styles/renderer/_detail.scss @@ -0,0 +1,25 @@ +@import '../vars'; +@import '../icons/index'; + +.#{$lu_css_prefix}-row [data-renderer=detail] { + text-align: center; + + &::before { + @include lu_icons(); + + display: inline; + content: $lu_icon_square_o; + } +} + +.#{$lu_css_prefix}-selected [data-renderer=detail]::before, +.selected [data-renderer=detail]::before, +.#{$lu_css_prefix}-group-selected[data-renderer=detail]::before { + content: $lu_icon_check_square_o; + padding-left: 2px; +} + +.#{$lu_css_prefix}-summary[data-renderer=detail] { + cursor: pointer; + font-size: medium; +} diff --git a/src/styles/renderer/_index.scss b/src/styles/renderer/_index.scss index 5ad214e06..64486156e 100644 --- a/src/styles/renderer/_index.scss +++ b/src/styles/renderer/_index.scss @@ -6,6 +6,7 @@ @import './boxplot'; @import './catdistributionbar'; @import './categorical'; + @import './detail'; @import './dot'; @import './heatmap'; @import './histogram'; diff --git a/src/ui/CanvasTextureRenderer.ts b/src/ui/CanvasTextureRenderer.ts index ff0b5554a..f20845596 100644 --- a/src/ui/CanvasTextureRenderer.ts +++ b/src/ui/CanvasTextureRenderer.ts @@ -7,20 +7,24 @@ import NumbersColumn from '../model/NumbersColumn'; import CategoricalColumn from '../model/CategoricalColumn'; import CategoricalsColumn from '../model/CategoricalsColumn'; import CompositeColumn from '../model/CompositeColumn'; -import AggregateGroupColumn from '../model/AggregateGroupColumn'; import * as d3 from 'd3-selection'; import * as drag from 'd3-drag'; import {ILineUpOptions} from '../interfaces'; import EngineRenderer from './EngineRenderer'; import {MultiTableRowRenderer} from 'lineupengine'; +import SelectionColumn from '../model/SelectionColumn'; +import OverviewDetailColumn from '../model/OverviewDetailColumn'; +import Ranking from '../model/Ranking'; export interface ITextureRenderer { - update(rankings: EngineRanking[], localData: IDataRow[][]): void; + update(rankings?: EngineRanking[], localData?: IDataRow[][]): void; expandTextureRenderer(use: boolean): void; destroy(): void; show(): void; hide(): void; updateSelection(dataIndices: number[]): void; + addRanking(ranking: EngineRanking): void; + removeRanking(ranking: Ranking | null): void; s2d(): void; d2s(): void; } @@ -39,7 +43,7 @@ export default class CanvasTextureRenderer implements ITextureRenderer { private currentNodeHeight: number = 0; private currentRankingWidths: number[] = []; private engineRenderer: EngineRenderer; - private engineRankings: EngineRanking[] = []; + private engineRankings: EngineRanking[][] = []; private skipUpdateEvents: number = 0; private alreadyExpanded: boolean = false; private expandLaterRows: any[] = []; @@ -68,13 +72,13 @@ export default class CanvasTextureRenderer implements ITextureRenderer { updateSelection(dataIndices: number[]) { const s = new Set(dataIndices); - this.engineRankings.forEach((r) => r.updateSelection(s)); + this.engineRankings.forEach((v) => v.forEach((r) => r.updateSelection(s))); this.drawSelection(); + this.update(); } - update(rankings: EngineRanking[], localData: IDataRow[][]) { + update(rankings: EngineRanking[] = this.currentRankings, localData: IDataRow[][] = this.currentLocalData) { this.detailParts = []; - this.currentRankings = rankings; this.currentLocalData = localData; this.currentNodeHeight = this.node.offsetHeight; let totalWidth = 0; @@ -93,23 +97,85 @@ export default class CanvasTextureRenderer implements ITextureRenderer { } private renderColumns (rankings: EngineRanking[], localData: IDataRow[][]) { - this.node.innerHTML = ''; //remove all children - this.engineRankings = []; - rankings.forEach((r, i) => { + let gIndex = 0; + const aggregatedParts = []; + r.ranking.getGroups().forEach((g) => { + if (this.engineRenderer.ctx.provider.isAggregated(r.ranking, g)) { + aggregatedParts.push([gIndex, gIndex + g.order.length - 1]); + } + gIndex += g.order.length; + }); + + const rankingIndex = this.currentRankings.findIndex((v) => v === r); + this.engineRankings[rankingIndex] = []; + //TODO: combine + this.detailParts = []; + let startIndex = -1; + for (let j = 0; j < localData[i].length; j++) { + if (this.engineRenderer.ctx.provider.isDetail(localData[i][j].i)) { + if (startIndex === -1) { + startIndex = j; + } + } else if (startIndex !== -1) { + this.detailParts.push([startIndex, j-1]); + startIndex = -1; + } + } + if (startIndex !== -1) { + this.detailParts.push([startIndex, localData[i].length-1]); + } + + const aggregateIndices = []; + aggregatedParts.forEach((g: any) => { + for (let j = 0; j < this.detailParts.length; j++) { + if (g[0] <= this.detailParts[j][0]) { + if (g[1] <= this.detailParts[j][0]) { + this.detailParts.splice(j, 0, g); + aggregateIndices.push(j); + return; + } else { + if (g[1] < this.detailParts[j][1]) { + this.detailParts.splice(j, 1, g, [g[1] + 1, this.detailParts[j][1]]); + aggregateIndices.push(j); + return; + } else { + this.detailParts.splice(j, 1); + } + } + } else { + if (g[0] <= this.detailParts[j][1]) { + if (g[1] < this.detailParts[j][1]) { + this.detailParts.splice(j, 1, [this.detailParts[j][0], g[0] - 1], g, [g[1] + 1, this.detailParts[j][1]]); + aggregateIndices.push(j + 1); + return; + } else { + this.detailParts.splice(j, 1, [this.detailParts[j][0], g[0] - 1]); + } + } + } + } + this.detailParts.push(g); + aggregateIndices.push(this.detailParts.length - 1); + }); + const dataParts = []; const expandableParts = []; + const aggregateParts = []; if(this.detailParts.length === 0) { dataParts.push(localData[i].length); } else { let next = 0; - this.detailParts.forEach((v) => { + this.detailParts.forEach((v, j) => { const curFrom = v[0]; const curTo = v[1]; if (curFrom > next) { dataParts.push(curFrom); } expandableParts.push(dataParts.length); + if (aggregateIndices.includes(j)) { + aggregateParts.push(dataParts.length); + } dataParts.push(curTo + 1); next = curTo + 1; }); @@ -117,36 +183,57 @@ export default class CanvasTextureRenderer implements ITextureRenderer { dataParts.push(localData[i].length); } } +// let curIndex = 0; - const rankingDiv = this.node.ownerDocument.createElement('div'); - rankingDiv.classList.add('rankingContainer'); - this.node.appendChild(rankingDiv); + const rankingDiv = d3.select(this.node).select(`[data-ranking="${rankingIndex}"]`)!.node(); + if (!rankingDiv) { + return; + } + rankingDiv.innerHTML = ''; //remove all children + const grouped = r.groupData(localData[i]); + let aggregateOffset = 0; dataParts.forEach((v: number, di: number) => { const expandable = expandableParts.includes(di); - const data = localData[i].slice(curIndex, v); - const grouped = r.groupData(data); + const aggregated = aggregateParts.includes(di); + let newOffset = 0; + if (aggregated) { + newOffset = v - curIndex - 1; + } + const data = grouped.slice(curIndex - aggregateOffset, v - aggregateOffset - newOffset); + const rowDiv = this.node.ownerDocument.createElement('div'); rowDiv.setAttribute('data-from', `${curIndex}`); rowDiv.setAttribute('data-to', `${v-1}`); - rowDiv.classList.add('row'); + rowDiv.classList.add('rowContainer'); + rankingDiv.appendChild(rowDiv); - const textureDiv = this.node.ownerDocument.createElement('div'); - textureDiv.style.height = `${data.length / localData[i].length * this.currentNodeHeight}px`; - textureDiv.classList.add('textureContainer'); - if (!expandable) { - textureDiv.classList.add('always'); - } - this.renderedColumns = []; - r.ranking.flatColumns.forEach((column) => this.createColumn(column, grouped, textureDiv, false, expandable)); curIndex = v; - rowDiv.appendChild(textureDiv); - rankingDiv.appendChild(rowDiv); + aggregateOffset += newOffset; + + if (!aggregated) { + const textureDiv = this.node.ownerDocument.createElement('div'); + textureDiv.style.height = `${data.length / localData[i].length * this.currentNodeHeight}px`; + textureDiv.classList.add('textureContainer'); + if (!expandable) { + textureDiv.classList.add('always'); + } + this.renderedColumns = []; + r.ranking.flatColumns.forEach((column) => this.createColumn(column, data, textureDiv, false, expandable)); + rowDiv.appendChild(textureDiv); + } if (expandable) { const expandLater = () => { const engineRendererDiv = this.node.ownerDocument.createElement('article'); //const id = `renderRow_${di}`; engineRendererDiv.classList.add('engineRendererContainer'); - engineRendererDiv.style.height = `${(this.options.rowHeight + this.options.rowPadding) * data.length + 10}px`; + if (aggregated) { + engineRendererDiv.classList.add('always'); + } + if (aggregated) { + engineRendererDiv.style.height = `${45}px`; + } else { + engineRendererDiv.style.height = `${(this.options.rowHeight + this.options.rowPadding) * data.length + 10}px`; + } engineRendererDiv.style.width = `${this.currentRankingWidths[i]}px`; rowDiv.appendChild(engineRendererDiv); @@ -159,12 +246,12 @@ export default class CanvasTextureRenderer implements ITextureRenderer { flags: this.options.flags })); - this.engineRenderer.render(engineRanking, data); - this.engineRankings.push(engineRanking); + this.engineRenderer.render(engineRanking, data); + this.engineRankings[rankingIndex].push(engineRanking); engineRanking.on(EngineRanking.EVENT_UPDATE_DATA, () => this.handleUpdateEvent(r)); this.skipUpdateEvents++; }; - if (this.alreadyExpanded) { + if (this.alreadyExpanded || aggregated) { expandLater(); } else { this.expandLaterRows.push(expandLater); @@ -228,17 +315,20 @@ export default class CanvasTextureRenderer implements ITextureRenderer { newElement = this.generateImage(grouped.map((value) => { return [(value).v[(col.desc).column]]; }), CanvasTextureRenderer.getColorScale(col)); + } else if (column instanceof SelectionColumn) { + const col = column; + newElement = this.generateImage(grouped.map((value) => { + return [this.engineRenderer.ctx.provider.isSelected((value).i)]; + }), CanvasTextureRenderer.getColorScale(col)); + } else if (column instanceof OverviewDetailColumn) { + const col = column; + newElement = this.generateImage(grouped.map((value) => { + return [this.engineRenderer.ctx.provider.isDetail((value).i)]; + }), CanvasTextureRenderer.getColorScale(col)); } else if ('children' in column) { //handle composite columns (column).children.forEach((c) => this.createColumn(c, grouped, container, true, expandable)); return; - } else if (column instanceof AggregateGroupColumn) { - newElement = this.createAggregateColumn(column.getWidth(), grouped.length, expandable); - if (expandable) { - newElement.onclick = () => { - newElement.parentNode.parentNode.parentNode.classList.add('expanded'); - }; - } } else { newElement = this.node.ownerDocument.createElement('canvas'); } @@ -273,6 +363,22 @@ export default class CanvasTextureRenderer implements ITextureRenderer { .range(categories.map((v) => v.color)); return colorScale; } + + if (column instanceof SelectionColumn) { + const colorScale = scaleOrdinal(); + colorScale + .domain([false, true]) + .range(['transparent', 'orange']); + return colorScale; + } + + if (column instanceof OverviewDetailColumn) { + const colorScale = scaleOrdinal(); + colorScale + .domain([false, true]) + .range(['transparent', 'blue']); + return colorScale; + } return null; } @@ -303,23 +409,6 @@ export default class CanvasTextureRenderer implements ITextureRenderer { ctx.save(); } - private createAggregateColumn(width: number, height: number, expandable: boolean) { - const canvas = this.node.ownerDocument.createElement('canvas'); - canvas.setAttribute('height', `${height}`); - canvas.setAttribute('width', `${width}`); - canvas.classList.add('aggregateColumn'); - const ctx = canvas.getContext('2d'); - - if (expandable) { - ctx.fillStyle = '#499cff'; - } else { - ctx.fillStyle = '#000000'; - } - ctx.fillRect(width-2, 0, 2, height); - ctx.save(); - return canvas; - } - expandTextureRenderer(use: boolean) { d3.select(this.node).classed('expand', use); if (!this.alreadyExpanded) { @@ -352,8 +441,11 @@ export default class CanvasTextureRenderer implements ITextureRenderer { const currentPosition = d3.mouse(element); if(currentPosition[1] === this.dragStartPosition[1]) { if (!d3.event.sourceEvent.ctrlKey) { - this.detailParts = []; - this.renderColumns(this.currentRankings, this.currentLocalData); + if (d3.event.sourceEvent.altKey) { + this.engineRenderer.ctx.provider.setDetail([]); + } else { + this.engineRenderer.ctx.provider.setSelection([]); + } } return; } @@ -366,16 +458,37 @@ export default class CanvasTextureRenderer implements ITextureRenderer { if (fromIndex > toIndex) { return; } - if (!d3.event.sourceEvent.ctrlKey) { - this.detailParts = []; - } - const i = this.detailParts.findIndex((v) => v[0] >= toIndex); - if(i === -1) { - this.detailParts.push([fromIndex, toIndex]); + const ranking = element.parentElement.getAttribute('data-ranking'); + const indices : number[] = d3.event.sourceEvent.ctrlKey ? (d3.event.sourceEvent.altKey ? this.engineRenderer.ctx.provider.getDetail() : this.engineRenderer.ctx.provider.getSelection()) :[]; + this.currentLocalData[ranking].slice(fromIndex, toIndex).forEach((d) => { + indices.push(d.i); + }); + if (d3.event.sourceEvent.altKey) { + this.engineRenderer.ctx.provider.setDetail(indices); } else { - this.detailParts.splice(i, 0, [fromIndex, toIndex]); + this.engineRenderer.ctx.provider.setSelection(indices); } - this.renderColumns(this.currentRankings, this.currentLocalData); + } + + addRanking(ranking: EngineRanking) { + this.currentRankings.push(ranking); + const rankingDiv = this.node.ownerDocument.createElement('div'); + rankingDiv.classList.add('rankingContainer'); + rankingDiv.setAttribute('data-ranking', `${this.currentRankings.length-1}`); + this.node.appendChild(rankingDiv); + } + + removeRanking(ranking: Ranking | null) { + if (!ranking) { + this.node.innerHTML = ''; + } + const index = this.currentRankings.findIndex((r) => r.ranking === ranking); + if (index < 0) { + return; // error + } + this.currentRankings.splice(index, 1); + this.engineRankings.splice(index, 1); + d3.select(this.node).select(`[data-ranking="${index}"]`).remove(); } destroy() { @@ -391,38 +504,40 @@ export default class CanvasTextureRenderer implements ITextureRenderer { } s2d() { - this.detailParts = []; - let startIndex = -1; - for (let i = 0; i < this.currentLocalData[0].length; i++) { - if (this.engineRenderer.ctx.provider.isSelected(this.currentLocalData[0][i].i)) { - if (startIndex === -1) { - startIndex = i; - } - } else if (startIndex !== -1) { - this.detailParts.push([startIndex, i-1]); - startIndex = -1; - } - } - if (startIndex !== -1) { - this.detailParts.push([startIndex, this.currentLocalData[0].length-1]); - } - this.renderColumns(this.currentRankings, this.currentLocalData); + this.engineRenderer.ctx.provider.setDetail(this.engineRenderer.ctx.provider.getSelection()); + //this.detailParts = []; + //let startIndex = -1; + //for (let i = 0; i < this.currentLocalData[0].length; i++) { + // if (this.engineRenderer.ctx.provider.isSelected(this.currentLocalData[0][i].i)) { + // if (startIndex === -1) { + // startIndex = i; + // } + // } else if (startIndex !== -1) { + // this.detailParts.push([startIndex, i-1]); + // startIndex = -1; + // } + //} + //if (startIndex !== -1) { + // this.detailParts.push([startIndex, this.currentLocalData[0].length-1]); + //} + //this.renderColumns(this.currentRankings, this.currentLocalData); } d2s() { - d3.select(this.node).selectAll('.engineRendererContainer').nodes().forEach((v: any, i: number) => { - const first = d3.select(v).select('.lu-row:first-child').node(); - const last = d3.select(v).select('.lu-row:last-child').node(); - if (typeof first !== 'undefined' && typeof last !== 'undefined') { - this.engineRankings[i].selection.select(i !== 0, first, last); - } - }); + this.engineRenderer.ctx.provider.setSelection(this.engineRenderer.ctx.provider.getDetail()); + //d3.select(this.node).selectAll('.engineRendererContainer').nodes().forEach((v: any, i: number) => { + // const first = d3.select(v).select('.lu-row:first-child').node(); + // const last = d3.select(v).select('.lu-row:last-child').node(); + // if (typeof first !== 'undefined' && typeof last !== 'undefined') { + // this.engineRankings[i].selection.select(i !== 0, first, last); + // } + //}); } drawSelection() { - d3.select(this.node).selectAll('.aggregateColumn').nodes().forEach((v: any) => { + d3.select(this.node).selectAll('.selectionColumn').nodes().forEach((v: any) => { let parent = v.parentElement; - while (!parent.classList.contains('row')) { + while (!parent.classList.contains('rowContainer')) { parent = parent.parentElement; } const fromIndex = parseInt(parent.getAttribute('data-from'), 10); diff --git a/src/ui/EngineRanking.ts b/src/ui/EngineRanking.ts index 619f33f75..d665bb2e0 100644 --- a/src/ui/EngineRanking.ts +++ b/src/ui/EngineRanking.ts @@ -532,6 +532,22 @@ export default class EngineRanking extends ACellTableSection imple }, true); } + updateDetail(detailDataIndices: { has(i: number): boolean }) { + super.forEachRow((node: HTMLElement, rowIndex: number) => { + if (this.renderCtx.isGroup(rowIndex)) { + this.updateRow(node, rowIndex); + return; + } + + const dataIndex = parseInt(node.dataset.i!, 10); + if (detailDataIndices.has(dataIndex)) { + node.classList.add('lu-detail'); + } else { + node.classList.remove('lu-detail'); + } + }, true); + } + updateColumnWidths() { // update the column context in place (this._context).column = nonUniformContext(this._context.columns.map((w) => w.width), 100, COLUMN_PADDING); diff --git a/src/ui/EngineRenderer.ts b/src/ui/EngineRenderer.ts index 0ecde5f96..c68522904 100644 --- a/src/ui/EngineRenderer.ts +++ b/src/ui/EngineRenderer.ts @@ -184,6 +184,7 @@ export default class EngineRenderer extends AEventDispatcher { private takeDownProvider() { this.data.on(`${ADataProvider.EVENT_SELECTION_CHANGED}.body`, null); + this.data.on(`${ADataProvider.EVENT_DETAIL_CHANGED}.body`, null); this.data.on(`${ADataProvider.EVENT_ADD_RANKING}.body`, null); this.data.on(`${ADataProvider.EVENT_REMOVE_RANKING}.body`, null); this.data.on(`${ADataProvider.EVENT_GROUP_AGGREGATION_CHANGED}.body`, null); @@ -197,6 +198,7 @@ export default class EngineRenderer extends AEventDispatcher { private initProvider(data: ADataProvider) { data.on(`${ADataProvider.EVENT_SELECTION_CHANGED}.body`, () => this.updateSelection(data.getSelection())); + data.on(`${ADataProvider.EVENT_DETAIL_CHANGED}.body`, () => this.updateDetail(data.getDetail())); data.on(`${ADataProvider.EVENT_ADD_RANKING}.body`, (ranking: Ranking) => { this.addRanking(ranking); }); @@ -220,6 +222,12 @@ export default class EngineRenderer extends AEventDispatcher { this.slopeGraphs.forEach((r) => r.updateSelection(s)); } + private updateDetail(dataIndices: number[]) { + const s = new Set(dataIndices); + this.rankings.forEach((r) => r.updateDetail(s)); + this.textureRenderer.update(); + } + private updateHist(ranking?: EngineRanking, col?: Column) { if (!this.options.summaryHeader) { return; @@ -276,6 +284,7 @@ export default class EngineRenderer extends AEventDispatcher { ranking.on(suffix('.renderer', Ranking.EVENT_ORDER_CHANGED), () => this.updateHist(r)); this.rankings.push(r); + this.textureRenderer.addRanking(r); this.update([r]); } @@ -309,6 +318,7 @@ export default class EngineRenderer extends AEventDispatcher { if (slope) { this.table.remove(slope); } + this.textureRenderer.removeRanking(ranking); } update(rankings: EngineRanking[] = this.rankings) { @@ -363,7 +373,7 @@ export default class EngineRenderer extends AEventDispatcher { this.show(); rankings.forEach((r, i) => { - this.render(r, localData[i]); + this.render(r, r.groupData(localData[i])); }); } @@ -373,9 +383,7 @@ export default class EngineRenderer extends AEventDispatcher { this.table.widthChanged(); } - render(r: EngineRanking, localData: IDataRow[]) { - const grouped = r.groupData(localData); - + render(r: EngineRanking, grouped: (IGroupData | IGroupItem)[]) { const {height, defaultHeight, padding} = this.heightsFor(r.ranking, grouped); const that = this; From 391fd547c97a016c004d593b8cb544ea15b31d52 Mon Sep 17 00:00:00 2001 From: Dominik Dirmeier Date: Mon, 27 Aug 2018 08:19:17 +0200 Subject: [PATCH 04/16] added overviewDetail to SidePanel, for Caleydo/lineup_app#3 Caleydo/lineup_app#4 Caleydo/lineup_app#5 Caleydo/lineup_app#6 --- src/model/OverviewDetailColumn.ts | 2 +- src/model/index.ts | 1 + src/ui/panel/SidePanel.ts | 2 ++ src/ui/taggle/Taggle.ts | 14 ++------------ src/ui/toolbar.ts | 10 ++++++++++ 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/model/OverviewDetailColumn.ts b/src/model/OverviewDetailColumn.ts index 5288e4763..be5522708 100644 --- a/src/model/OverviewDetailColumn.ts +++ b/src/model/OverviewDetailColumn.ts @@ -27,7 +27,7 @@ export interface IDetailColumnDesc extends IValueColumnDesc { * a checkbox column for selections */ @SupportType() -@toolbar('sort', 'stratify') +@toolbar('sort', 'stratify', 'selectionToOverviewDetail', 'overviewDetailToSelection') @Category('support') export default class OverviewDetailColumn extends ValueColumn { private static DETAILED_GROUP: IGroup = { diff --git a/src/model/index.ts b/src/model/index.ts index e51de4c68..4f5217ea2 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -89,6 +89,7 @@ export {default as Ranking, ISortCriteria} from './Ranking'; export {default as ReduceColumn, createReduceDesc, IReduceDesc, IReduceColumnDesc} from './ReduceColumn'; export {default as ScriptColumn, createScriptDesc, IScriptDesc, IScriptColumnDesc} from './ScriptColumn'; export {default as SelectionColumn, createSelectionDesc, ISelectionColumnDesc} from './SelectionColumn'; +export {default as DeteilColumn, createDetailDesc, IDetailColumnDesc} from './OverviewDetailColumn'; export * from './SetColumn'; export {default as SetColumn} from './SetColumn'; export {default as StackColumn, createStackDesc} from './StackColumn'; diff --git a/src/ui/panel/SidePanel.ts b/src/ui/panel/SidePanel.ts index 20085e707..af5e7edcd 100644 --- a/src/ui/panel/SidePanel.ts +++ b/src/ui/panel/SidePanel.ts @@ -9,6 +9,7 @@ import { createGroupDesc, createAggregateDesc, createSelectionDesc, + createDetailDesc, IColumnDesc } from '../../model'; import {categoryOfDesc} from '../../model/annotations'; @@ -56,6 +57,7 @@ export default class SidePanel { createImpositionDesc(), createRankDesc(), createSelectionDesc(), + createDetailDesc(), createGroupDesc(), createAggregateDesc(), ], diff --git a/src/ui/taggle/Taggle.ts b/src/ui/taggle/Taggle.ts index 60c2ce521..5a672902a 100644 --- a/src/ui/taggle/Taggle.ts +++ b/src/ui/taggle/Taggle.ts @@ -43,10 +43,8 @@ export default class Taggle extends ALineUp { this.panel.node.insertAdjacentHTML('afterbegin', `
`); const spaceFilling = spaceFillingRule(this.options); @@ -67,14 +65,6 @@ export default class Taggle extends ALineUp { const selected = expandInput.checked; this.renderer!.expandTextureRenderer(selected); }; - const s2dInput = this.spaceFilling.querySelector('input.s2d'); - s2dInput.onclick = () => { - this.renderer!.s2d(); - }; - const d2sInput = this.spaceFilling.querySelector('input.d2s'); - d2sInput.onclick = () => { - this.renderer!.d2s(); - }; } this.forward(this.renderer, `${ALineUp.EVENT_HIGHLIGHT_CHANGED}.main`); } diff --git a/src/ui/toolbar.ts b/src/ui/toolbar.ts index bc711115b..dea758d36 100644 --- a/src/ui/toolbar.ts +++ b/src/ui/toolbar.ts @@ -297,6 +297,16 @@ export const toolbarActions: { [key: string]: IToolbarAction | IToolbarDialogAdd const ss = new Set(s); const others = order.filter((d) => !ss.has(d)); ctx.provider.setSelection(others); + }), + selectionToOverviewDetail: ui('S2D', (_col, _evt, ctx, level) => { + ctx.dialogManager.removeAboveLevel(level - 1); // close itself + const s = ctx.provider.getSelection(); + ctx.provider.setDetail(s); + }), + overviewDetailToSelection: ui('D2S', (_col, _evt, ctx, level) => { + ctx.dialogManager.removeAboveLevel(level - 1); // close itself + const d = ctx.provider.getDetail(); + ctx.provider.setSelection(d); }) }, toolbarAddons); From 80fc7515f82fa8c20e50775ea691b863bea8cbc7 Mon Sep 17 00:00:00 2001 From: Dominik Dirmeier Date: Fri, 7 Sep 2018 10:12:04 +0200 Subject: [PATCH 05/16] splitting big canvas into chunks and minor fixes, for Caleydo/lineup_app#3 Caleydo/lineup_app#4 Caleydo/lineup_app#5 Caleydo/lineup_app#6 --- src/model/OverviewDetailColumn.ts | 2 +- src/styles/engine/_texture_renderer.scss | 3 + src/ui/CanvasTextureRenderer.ts | 100 ++++++++++++----------- src/ui/taggle/Taggle.ts | 28 +++++-- 4 files changed, 78 insertions(+), 55 deletions(-) diff --git a/src/model/OverviewDetailColumn.ts b/src/model/OverviewDetailColumn.ts index be5522708..15d67d323 100644 --- a/src/model/OverviewDetailColumn.ts +++ b/src/model/OverviewDetailColumn.ts @@ -27,7 +27,7 @@ export interface IDetailColumnDesc extends IValueColumnDesc { * a checkbox column for selections */ @SupportType() -@toolbar('sort', 'stratify', 'selectionToOverviewDetail', 'overviewDetailToSelection') +@toolbar('sort', 'sortBy', 'group', 'groupBy', 'selectionToOverviewDetail', 'overviewDetailToSelection') @Category('support') export default class OverviewDetailColumn extends ValueColumn { private static DETAILED_GROUP: IGroup = { diff --git a/src/styles/engine/_texture_renderer.scss b/src/styles/engine/_texture_renderer.scss index d311e1c9a..4dc2f3b5d 100644 --- a/src/styles/engine/_texture_renderer.scss +++ b/src/styles/engine/_texture_renderer.scss @@ -18,6 +18,9 @@ .columnContainer { flex: 0 0 auto; margin-right: 5px; + display: flex; + flex-direction: column; + justify-content: space-between; &.partOfComposite { margin-right: 6px; diff --git a/src/ui/CanvasTextureRenderer.ts b/src/ui/CanvasTextureRenderer.ts index f20845596..e5c9761a1 100644 --- a/src/ui/CanvasTextureRenderer.ts +++ b/src/ui/CanvasTextureRenderer.ts @@ -30,6 +30,7 @@ export interface ITextureRenderer { } export default class CanvasTextureRenderer implements ITextureRenderer { + static readonly MAX_CANVAS_SIZE = 32767; readonly node: HTMLElement; readonly canvas: any; @@ -134,26 +135,23 @@ export default class CanvasTextureRenderer implements ITextureRenderer { this.detailParts.splice(j, 0, g); aggregateIndices.push(j); return; - } else { - if (g[1] < this.detailParts[j][1]) { - this.detailParts.splice(j, 1, g, [g[1] + 1, this.detailParts[j][1]]); - aggregateIndices.push(j); - return; - } else { - this.detailParts.splice(j, 1); - } } - } else { - if (g[0] <= this.detailParts[j][1]) { - if (g[1] < this.detailParts[j][1]) { - this.detailParts.splice(j, 1, [this.detailParts[j][0], g[0] - 1], g, [g[1] + 1, this.detailParts[j][1]]); - aggregateIndices.push(j + 1); - return; - } else { - this.detailParts.splice(j, 1, [this.detailParts[j][0], g[0] - 1]); - } + if (g[1] < this.detailParts[j][1]) { + this.detailParts.splice(j, 1, g, [g[1] + 1, this.detailParts[j][1]]); + aggregateIndices.push(j); + return; } + this.detailParts.splice(j, 1); + } + if (g[0] > this.detailParts[j][1]) { + return; } + if (g[1] < this.detailParts[j][1]) { + this.detailParts.splice(j, 1, [this.detailParts[j][0], g[0] - 1], g, [g[1] + 1, this.detailParts[j][1]]); + aggregateIndices.push(j + 1); + return; + } + this.detailParts.splice(j, 1, [this.detailParts[j][0], g[0] - 1]); } this.detailParts.push(g); aggregateIndices.push(this.detailParts.length - 1); @@ -183,7 +181,6 @@ export default class CanvasTextureRenderer implements ITextureRenderer { dataParts.push(localData[i].length); } } -// let curIndex = 0; const rankingDiv = d3.select(this.node).select(`[data-ranking="${rankingIndex}"]`)!.node(); if (!rankingDiv) { @@ -211,15 +208,18 @@ export default class CanvasTextureRenderer implements ITextureRenderer { aggregateOffset += newOffset; if (!aggregated) { - const textureDiv = this.node.ownerDocument.createElement('div'); - textureDiv.style.height = `${data.length / localData[i].length * this.currentNodeHeight}px`; - textureDiv.classList.add('textureContainer'); - if (!expandable) { - textureDiv.classList.add('always'); + const height = data.length / localData[i].length * this.currentNodeHeight; + if (height >= 1) { //only render parts larger than 1px + const textureDiv = this.node.ownerDocument.createElement('div'); + textureDiv.style.height = `${height}px`; + textureDiv.classList.add('textureContainer'); + if (!expandable) { + textureDiv.classList.add('always'); + } + this.renderedColumns = []; + r.ranking.flatColumns.forEach((column) => this.createColumn(column, data, textureDiv, false, expandable)); + rowDiv.appendChild(textureDiv); } - this.renderedColumns = []; - r.ranking.flatColumns.forEach((column) => this.createColumn(column, data, textureDiv, false, expandable)); - rowDiv.appendChild(textureDiv); } if (expandable) { const expandLater = () => { @@ -294,35 +294,35 @@ export default class CanvasTextureRenderer implements ITextureRenderer { columnContainer.classList.add('partOfComposite'); } - let newElement = null; + let newElements = []; if (column instanceof NumbersColumn) { const col = column; - newElement = this.generateImage(grouped.map((value) => { + newElements = this.generateImage(grouped.map((value) => { return (value).v[(col.desc).column]; }), CanvasTextureRenderer.getColorScale(col)); } else if (column instanceof NumberColumn) { const col = column; - newElement = this.generateImage(grouped.map((value) => { + newElements = this.generateImage(grouped.map((value) => { return [(value).v[(col.desc).column]]; }), CanvasTextureRenderer.getColorScale(col)); } else if (column instanceof CategoricalsColumn) { const col = column; - newElement = this.generateImage(grouped.map((value) => { + newElements = this.generateImage(grouped.map((value) => { return (value).v[(col.desc).column]; }), CanvasTextureRenderer.getColorScale(col)); } else if (column instanceof CategoricalColumn) { const col = column; - newElement = this.generateImage(grouped.map((value) => { + newElements = this.generateImage(grouped.map((value) => { return [(value).v[(col.desc).column]]; }), CanvasTextureRenderer.getColorScale(col)); } else if (column instanceof SelectionColumn) { const col = column; - newElement = this.generateImage(grouped.map((value) => { + newElements = this.generateImage(grouped.map((value) => { return [this.engineRenderer.ctx.provider.isSelected((value).i)]; }), CanvasTextureRenderer.getColorScale(col)); } else if (column instanceof OverviewDetailColumn) { const col = column; - newElement = this.generateImage(grouped.map((value) => { + newElements = this.generateImage(grouped.map((value) => { return [this.engineRenderer.ctx.provider.isDetail((value).i)]; }), CanvasTextureRenderer.getColorScale(col)); } else if ('children' in column) { @@ -330,10 +330,10 @@ export default class CanvasTextureRenderer implements ITextureRenderer { (column).children.forEach((c) => this.createColumn(c, grouped, container, true, expandable)); return; } else { - newElement = this.node.ownerDocument.createElement('canvas'); + newElements.push(this.node.ownerDocument.createElement('canvas')); } - columnContainer.appendChild(newElement); + newElements.forEach((newElement: any) => columnContainer.appendChild(newElement)); container.appendChild(columnContainer); this.renderedColumns.push(column.id); @@ -356,10 +356,10 @@ export default class CanvasTextureRenderer implements ITextureRenderer { return colorScale; } if (column instanceof CategoricalColumn) { - const colorScale = scaleOrdinal(); + const colorScale = scaleOrdinal(); const categories = column.categories; colorScale - .domain(categories.map((v) => v.value)) + .domain(categories.map((v) => v.name)) .range(categories.map((v) => v.color)); return colorScale; } @@ -383,16 +383,24 @@ export default class CanvasTextureRenderer implements ITextureRenderer { } private generateImage(data: any[][], colorScale: any) { - const height = data.length; - let width = 0; - if(height > 0) { - width = data[0].length; + const totalLength = data.length; + const newElements = []; + while (data.length > 0) { + const chunk = data.splice(0, CanvasTextureRenderer.MAX_CANVAS_SIZE); + const height = chunk.length; + let width = 0; + if(height > 0) { + width = chunk[0].length; + } + const canvas = this.node.ownerDocument.createElement('canvas'); + canvas.setAttribute('height', `${height}`); + canvas.setAttribute('width', `${width}`); + canvas.style.flexGrow = `${height}`; + canvas.style.height = `${chunk.length / totalLength * 100}%`; + this.drawOntoCanvas(chunk, colorScale, canvas); + newElements.push(canvas); } - const canvas = this.node.ownerDocument.createElement('canvas'); - canvas.setAttribute('height', `${height}`); - canvas.setAttribute('width', `${width}`); - this.drawOntoCanvas(data, colorScale, canvas); - return canvas; + return newElements; } private drawOntoCanvas(data: any[][], colorScale: any, canvas: any) { diff --git a/src/ui/taggle/Taggle.ts b/src/ui/taggle/Taggle.ts index 5a672902a..accc83421 100644 --- a/src/ui/taggle/Taggle.ts +++ b/src/ui/taggle/Taggle.ts @@ -40,12 +40,22 @@ export default class Taggle extends ALineUp { this.renderer.pushUpdateAble((ctx) => this.panel!.update(ctx)); this.node.insertBefore(this.panel.node, this.node.firstChild); { + this.panel.node.insertAdjacentHTML('afterbegin', `
`); + const expandButton = this.node.querySelector('.lu-expand-button-chooser')!; + const expandInput = expandButton.querySelector('input.expand'); + expandInput.onchange = () => { + const selected = expandInput.checked; + this.renderer!.expandTextureRenderer(selected); + }; + expandButton.onclick = () => { + expandInput.checked = !expandInput.checked; + }; this.panel.node.insertAdjacentHTML('afterbegin', `
`); const spaceFilling = spaceFillingRule(this.options); this.spaceFilling = this.node.querySelector('.lu-rule-button-chooser')!; @@ -54,17 +64,19 @@ export default class Taggle extends ALineUp { const selected = this.spaceFilling!.classList.toggle('chosen'); //self.setTimeout(() => this.renderer.switchRule(selected ? spaceFilling : null)); this.renderer!.useTextureRenderer(selected); + if (selected) { + expandButton.style.display = ''; + } else { + expandButton.style.display = 'none'; + } }; if (this.options.overviewMode) { ruleInput.checked = true; this.spaceFilling.classList.toggle('chosen'); this.renderer.switchRule(spaceFilling); + } else { + expandButton.style.display = 'none'; } - const expandInput = this.spaceFilling.querySelector('input.expand'); - expandInput.onchange = () => { - const selected = expandInput.checked; - this.renderer!.expandTextureRenderer(selected); - }; } this.forward(this.renderer, `${ALineUp.EVENT_HIGHLIGHT_CHANGED}.main`); } From 56c676a73305b99a915d33ccd4e1e6cca3ed9b07 Mon Sep 17 00:00:00 2001 From: Dominik Dirmeier Date: Sun, 7 Oct 2018 19:56:03 +0200 Subject: [PATCH 06/16] bug fixes, for Caleydo/lineup_app#20 Caleydo/lineup_app#21 Caleydo/lineup_app#22 Caleydo/lineup_app#23 --- src/ui/CanvasTextureRenderer.ts | 71 ++++++++++++--------------------- src/ui/EngineRanking.ts | 70 ++++++++++++++++---------------- src/ui/taggle/Taggle.ts | 23 ++++++++--- 3 files changed, 78 insertions(+), 86 deletions(-) diff --git a/src/ui/CanvasTextureRenderer.ts b/src/ui/CanvasTextureRenderer.ts index e5c9761a1..cd791e9b1 100644 --- a/src/ui/CanvasTextureRenderer.ts +++ b/src/ui/CanvasTextureRenderer.ts @@ -31,6 +31,7 @@ export interface ITextureRenderer { export default class CanvasTextureRenderer implements ITextureRenderer { static readonly MAX_CANVAS_SIZE = 32767; + static readonly AGGRIGATED_ROW_HEIGHT = 45; readonly node: HTMLElement; readonly canvas: any; @@ -45,7 +46,7 @@ export default class CanvasTextureRenderer implements ITextureRenderer { private currentRankingWidths: number[] = []; private engineRenderer: EngineRenderer; private engineRankings: EngineRanking[][] = []; - private skipUpdateEvents: number = 0; + //private skipUpdateEvents: number = 0; private alreadyExpanded: boolean = false; private expandLaterRows: any[] = []; private readonly options: Readonly; @@ -80,7 +81,10 @@ export default class CanvasTextureRenderer implements ITextureRenderer { update(rankings: EngineRanking[] = this.currentRankings, localData: IDataRow[][] = this.currentLocalData) { this.detailParts = []; - this.currentLocalData = localData; + rankings.forEach((r, i) => { + const rankingIndex = this.currentRankings.findIndex((v) => v === r); + this.currentLocalData[rankingIndex] = localData[i]; + }); this.currentNodeHeight = this.node.offsetHeight; let totalWidth = 0; rankings.forEach((r, i) => { @@ -99,18 +103,20 @@ export default class CanvasTextureRenderer implements ITextureRenderer { private renderColumns (rankings: EngineRanking[], localData: IDataRow[][]) { rankings.forEach((r, i) => { + let notAggregatedCount = localData[i].length; let gIndex = 0; const aggregatedParts = []; r.ranking.getGroups().forEach((g) => { if (this.engineRenderer.ctx.provider.isAggregated(r.ranking, g)) { aggregatedParts.push([gIndex, gIndex + g.order.length - 1]); + notAggregatedCount -= g.order.length; } gIndex += g.order.length; }); + this.currentNodeHeight -= CanvasTextureRenderer.AGGRIGATED_ROW_HEIGHT * aggregatedParts.length; const rankingIndex = this.currentRankings.findIndex((v) => v === r); this.engineRankings[rankingIndex] = []; - //TODO: combine this.detailParts = []; let startIndex = -1; for (let j = 0; j < localData[i].length; j++) { @@ -131,7 +137,7 @@ export default class CanvasTextureRenderer implements ITextureRenderer { aggregatedParts.forEach((g: any) => { for (let j = 0; j < this.detailParts.length; j++) { if (g[0] <= this.detailParts[j][0]) { - if (g[1] <= this.detailParts[j][0]) { + if (g[1] < this.detailParts[j][0]) { this.detailParts.splice(j, 0, g); aggregateIndices.push(j); return; @@ -142,16 +148,18 @@ export default class CanvasTextureRenderer implements ITextureRenderer { return; } this.detailParts.splice(j, 1); + j--; + continue; } if (g[0] > this.detailParts[j][1]) { - return; + continue; } if (g[1] < this.detailParts[j][1]) { - this.detailParts.splice(j, 1, [this.detailParts[j][0], g[0] - 1], g, [g[1] + 1, this.detailParts[j][1]]); - aggregateIndices.push(j + 1); - return; - } - this.detailParts.splice(j, 1, [this.detailParts[j][0], g[0] - 1]); + this.detailParts.splice(j, 1, [this.detailParts[j][0], g[0] - 1], g, [g[1] + 1, this.detailParts[j][1]]); + aggregateIndices.push(j + 1); + return; + } + this.detailParts.splice(j, 1, [this.detailParts[j][0], g[0] - 1]); } this.detailParts.push(g); aggregateIndices.push(this.detailParts.length - 1); @@ -208,7 +216,7 @@ export default class CanvasTextureRenderer implements ITextureRenderer { aggregateOffset += newOffset; if (!aggregated) { - const height = data.length / localData[i].length * this.currentNodeHeight; + const height = data.length / notAggregatedCount * this.currentNodeHeight; if (height >= 1) { //only render parts larger than 1px const textureDiv = this.node.ownerDocument.createElement('div'); textureDiv.style.height = `${height}px`; @@ -242,14 +250,14 @@ export default class CanvasTextureRenderer implements ITextureRenderer { const engineRanking = table.pushTable((header, body, tableId, style) => new EngineRanking(r.ranking, header, body, tableId, style, this.engineRenderer.ctx, { animation: this.options.animated, customRowUpdate: this.options.customRowUpdate || (() => undefined), - levelOfDetail: this.options.levelOfDetail || (() => 'high'), + levelOfDetail: this.options.levelOfDetail || (() => 'high'),// flags: this.options.flags - })); + }, true)); this.engineRenderer.render(engineRanking, data); this.engineRankings[rankingIndex].push(engineRanking); - engineRanking.on(EngineRanking.EVENT_UPDATE_DATA, () => this.handleUpdateEvent(r)); - this.skipUpdateEvents++; + //engineRanking.on(EngineRanking.EVENT_UPDATE_DATA, () => this.handleUpdateEvent(r)); + //this.skipUpdateEvents++; }; if (this.alreadyExpanded || aggregated) { expandLater(); @@ -480,6 +488,7 @@ export default class CanvasTextureRenderer implements ITextureRenderer { addRanking(ranking: EngineRanking) { this.currentRankings.push(ranking); + this.currentLocalData.push([]); const rankingDiv = this.node.ownerDocument.createElement('div'); rankingDiv.classList.add('rankingContainer'); rankingDiv.setAttribute('data-ranking', `${this.currentRankings.length-1}`); @@ -496,6 +505,7 @@ export default class CanvasTextureRenderer implements ITextureRenderer { } this.currentRankings.splice(index, 1); this.engineRankings.splice(index, 1); + this.currentLocalData.splice(index, 1); d3.select(this.node).select(`[data-ranking="${index}"]`).remove(); } @@ -513,33 +523,10 @@ export default class CanvasTextureRenderer implements ITextureRenderer { s2d() { this.engineRenderer.ctx.provider.setDetail(this.engineRenderer.ctx.provider.getSelection()); - //this.detailParts = []; - //let startIndex = -1; - //for (let i = 0; i < this.currentLocalData[0].length; i++) { - // if (this.engineRenderer.ctx.provider.isSelected(this.currentLocalData[0][i].i)) { - // if (startIndex === -1) { - // startIndex = i; - // } - // } else if (startIndex !== -1) { - // this.detailParts.push([startIndex, i-1]); - // startIndex = -1; - // } - //} - //if (startIndex !== -1) { - // this.detailParts.push([startIndex, this.currentLocalData[0].length-1]); - //} - //this.renderColumns(this.currentRankings, this.currentLocalData); } d2s() { this.engineRenderer.ctx.provider.setSelection(this.engineRenderer.ctx.provider.getDetail()); - //d3.select(this.node).selectAll('.engineRendererContainer').nodes().forEach((v: any, i: number) => { - // const first = d3.select(v).select('.lu-row:first-child').node(); - // const last = d3.select(v).select('.lu-row:last-child').node(); - // if (typeof first !== 'undefined' && typeof last !== 'undefined') { - // this.engineRankings[i].selection.select(i !== 0, first, last); - // } - //}); } drawSelection() { @@ -564,12 +551,4 @@ export default class CanvasTextureRenderer implements ITextureRenderer { ctx.save(); }); } - - private handleUpdateEvent (r: EngineRanking) { - if (this.skipUpdateEvents > 0) { - this.skipUpdateEvents--; - } else { - this.engineRenderer.update([r]); - } - } } diff --git a/src/ui/EngineRanking.ts b/src/ui/EngineRanking.ts index ae01d1753..037032356 100644 --- a/src/ui/EngineRanking.ts +++ b/src/ui/EngineRanking.ts @@ -156,7 +156,7 @@ export default class EngineRanking extends ACellTableSection imple } }; - constructor(public readonly ranking: Ranking, header: HTMLElement, body: HTMLElement, tableId: string, style: GridStyleManager, private readonly ctx: IEngineRankingContext, roptions: Partial = {}) { + constructor(public readonly ranking: Ranking, header: HTMLElement, body: HTMLElement, tableId: string, style: GridStyleManager, private readonly ctx: IEngineRankingContext, roptions: Partial = {}, readonly noEvents: boolean = false) { super(header, body, tableId, style, {mixins: [PrefetchMixin], batchSize: 10}); Object.assign(this.roptions, roptions); body.classList.add('lu-row-body'); @@ -179,43 +179,45 @@ export default class EngineRanking extends ACellTableSection imple this.delayedUpdateAll = debounce(() => this.updateAll(), 50); this.delayedUpdateColumnWidths = debounce(() => this.updateColumnWidths(), 50); - ranking.on(`${Ranking.EVENT_ADD_COLUMN}.hist`, (col: Column, index: number) => { - this.columns.splice(index, 0, this.createCol(col, index)); - this.reindex(); - this.updateHist(col); - this.delayedUpdateAll(); - }); - ranking.on(`${Ranking.EVENT_REMOVE_COLUMN}.body`, (col: Column, index: number) => { - EngineRanking.disableListener(col); - this.columns.splice(index, 1); - this.reindex(); - this.delayedUpdateAll(); - }); - ranking.on(`${Ranking.EVENT_MOVE_COLUMN}.body`, (col: Column, index: number, old: number) => { - //delete first - const c = this.columns.splice(old, 1)[0]; - console.assert(c.c === col); - // adapt target index based on previous index, i.e shift by one - this.columns.splice(old < index ? index - 1 : index, 0, c); - this.reindex(); - this.delayedUpdateAll(); - }); - ranking.on(`${Ranking.EVENT_COLUMN_VISIBILITY_CHANGED}.body`, (col: Column, _oldValue: boolean, newValue: boolean) => { - if (newValue) { - // become visible - const index = ranking.children.indexOf(col); + if (!noEvents) { + ranking.on(`${Ranking.EVENT_ADD_COLUMN}.hist`, (col: Column, index: number) => { this.columns.splice(index, 0, this.createCol(col, index)); + this.reindex(); this.updateHist(col); - } else { - // hide - const index = this.columns.findIndex((d) => d.c === col); + this.delayedUpdateAll(); + }); + ranking.on(`${Ranking.EVENT_REMOVE_COLUMN}.body`, (col: Column, index: number) => { EngineRanking.disableListener(col); this.columns.splice(index, 1); - } - this.reindex(); - this.delayedUpdateAll(); - }); - ranking.on(`${Ranking.EVENT_ORDER_CHANGED}.body`, this.delayedUpdate); + this.reindex(); + this.delayedUpdateAll(); + }); + ranking.on(`${Ranking.EVENT_MOVE_COLUMN}.body`, (col: Column, index: number, old: number) => { + //delete first + const c = this.columns.splice(old, 1)[0]; + console.assert(c.c === col); + // adapt target index based on previous index, i.e shift by one + this.columns.splice(old < index ? index - 1 : index, 0, c); + this.reindex(); + this.delayedUpdateAll(); + }); + ranking.on(`${Ranking.EVENT_COLUMN_VISIBILITY_CHANGED}.body`, (col: Column, _oldValue: boolean, newValue: boolean) => { + if (newValue) { + // become visible + const index = ranking.children.indexOf(col); + this.columns.splice(index, 0, this.createCol(col, index)); + this.updateHist(col); + } else { + // hide + const index = this.columns.findIndex((d) => d.c === col); + EngineRanking.disableListener(col); + this.columns.splice(index, 1); + } + this.reindex(); + this.delayedUpdateAll(); + }); + ranking.on(`${Ranking.EVENT_ORDER_CHANGED}.body`, this.delayedUpdate); + } this.selection = new SelectionManager(this.ctx, body); this.selection.on(SelectionManager.EVENT_SELECT_RANGE, (from: number, to: number, additional: boolean) => { diff --git a/src/ui/taggle/Taggle.ts b/src/ui/taggle/Taggle.ts index accc83421..7481d0834 100644 --- a/src/ui/taggle/Taggle.ts +++ b/src/ui/taggle/Taggle.ts @@ -6,6 +6,7 @@ import {ALineUp} from '../ALineUp'; import SidePanel from '../panel/SidePanel'; import spaceFillingRule from './spaceFillingRule'; import TaggleRenderer from './TaggleRenderer'; +import LocalDataProvider from '../../provider/LocalDataProvider'; export {ITaggleOptions} from '../../interfaces'; @@ -61,14 +62,24 @@ export default class Taggle extends ALineUp { this.spaceFilling = this.node.querySelector('.lu-rule-button-chooser')!; const ruleInput = this.spaceFilling.querySelector('input.spaceFilling'); ruleInput.onchange = () => { + let useTextureRenderer = true; + if (data instanceof LocalDataProvider) { + const ldp = data; + if (this.node.offsetHeight > ldp.data.length) { + useTextureRenderer = false; + } + } const selected = this.spaceFilling!.classList.toggle('chosen'); - //self.setTimeout(() => this.renderer.switchRule(selected ? spaceFilling : null)); - this.renderer!.useTextureRenderer(selected); - if (selected) { - expandButton.style.display = ''; - } else { - expandButton.style.display = 'none'; + if (useTextureRenderer) { + this.renderer!.useTextureRenderer(selected); + if (selected) { + expandButton.style.display = ''; + } else { + expandButton.style.display = 'none'; + } + return; } + self.setTimeout(() => this.renderer!.switchRule(selected ? spaceFilling : null)); }; if (this.options.overviewMode) { ruleInput.checked = true; From f1a760de2b549ac8b8edf3f80abb9e11cf0d1981 Mon Sep 17 00:00:00 2001 From: Dominik Dirmeier Date: Mon, 22 Oct 2018 18:01:58 +0200 Subject: [PATCH 07/16] code clean up --- src/ui/CanvasTextureRenderer.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/ui/CanvasTextureRenderer.ts b/src/ui/CanvasTextureRenderer.ts index cd791e9b1..c2338ef1a 100644 --- a/src/ui/CanvasTextureRenderer.ts +++ b/src/ui/CanvasTextureRenderer.ts @@ -31,7 +31,10 @@ export interface ITextureRenderer { export default class CanvasTextureRenderer implements ITextureRenderer { static readonly MAX_CANVAS_SIZE = 32767; - static readonly AGGRIGATED_ROW_HEIGHT = 45; + static readonly AGGREGATED_ROW_HEIGHT = 45; + static readonly RENDER_ROW_PADDING = 10; + static readonly EXPANDED_ROW_CLASS = 'expand'; + static readonly SELECTION_DRAW_WIDTH = 2; readonly node: HTMLElement; readonly canvas: any; @@ -46,7 +49,6 @@ export default class CanvasTextureRenderer implements ITextureRenderer { private currentRankingWidths: number[] = []; private engineRenderer: EngineRenderer; private engineRankings: EngineRanking[][] = []; - //private skipUpdateEvents: number = 0; private alreadyExpanded: boolean = false; private expandLaterRows: any[] = []; private readonly options: Readonly; @@ -96,13 +98,14 @@ export default class CanvasTextureRenderer implements ITextureRenderer { if (totalWidth > this.node.clientWidth) { this.currentNodeHeight -= 20; } - this.alreadyExpanded = this.node.classList.contains('expand'); + this.alreadyExpanded = this.node.classList.contains(CanvasTextureRenderer.EXPANDED_ROW_CLASS); this.renderColumns(rankings, localData); } private renderColumns (rankings: EngineRanking[], localData: IDataRow[][]) { rankings.forEach((r, i) => { + //first find the aggregated parts let notAggregatedCount = localData[i].length; let gIndex = 0; const aggregatedParts = []; @@ -113,8 +116,9 @@ export default class CanvasTextureRenderer implements ITextureRenderer { } gIndex += g.order.length; }); - this.currentNodeHeight -= CanvasTextureRenderer.AGGRIGATED_ROW_HEIGHT * aggregatedParts.length; + this.currentNodeHeight -= CanvasTextureRenderer.AGGREGATED_ROW_HEIGHT * aggregatedParts.length; + //then find the parts to show details const rankingIndex = this.currentRankings.findIndex((v) => v === r); this.engineRankings[rankingIndex] = []; this.detailParts = []; @@ -133,6 +137,7 @@ export default class CanvasTextureRenderer implements ITextureRenderer { this.detailParts.push([startIndex, localData[i].length-1]); } + //combine aggregated parts with detail parts const aggregateIndices = []; aggregatedParts.forEach((g: any) => { for (let j = 0; j < this.detailParts.length; j++) { @@ -232,15 +237,14 @@ export default class CanvasTextureRenderer implements ITextureRenderer { if (expandable) { const expandLater = () => { const engineRendererDiv = this.node.ownerDocument.createElement('article'); - //const id = `renderRow_${di}`; engineRendererDiv.classList.add('engineRendererContainer'); if (aggregated) { engineRendererDiv.classList.add('always'); } if (aggregated) { - engineRendererDiv.style.height = `${45}px`; + engineRendererDiv.style.height = `${CanvasTextureRenderer.AGGREGATED_ROW_HEIGHT}px`; } else { - engineRendererDiv.style.height = `${(this.options.rowHeight + this.options.rowPadding) * data.length + 10}px`; + engineRendererDiv.style.height = `${(this.options.rowHeight + this.options.rowPadding) * data.length + CanvasTextureRenderer.RENDER_ROW_PADDING}px`; } engineRendererDiv.style.width = `${this.currentRankingWidths[i]}px`; @@ -256,8 +260,6 @@ export default class CanvasTextureRenderer implements ITextureRenderer { this.engineRenderer.render(engineRanking, data); this.engineRankings[rankingIndex].push(engineRanking); - //engineRanking.on(EngineRanking.EVENT_UPDATE_DATA, () => this.handleUpdateEvent(r)); - //this.skipUpdateEvents++; }; if (this.alreadyExpanded || aggregated) { expandLater(); @@ -426,7 +428,7 @@ export default class CanvasTextureRenderer implements ITextureRenderer { } expandTextureRenderer(use: boolean) { - d3.select(this.node).classed('expand', use); + d3.select(this.node).classed(CanvasTextureRenderer.EXPANDED_ROW_CLASS, use); if (!this.alreadyExpanded) { this.expandLaterRows.forEach((r) => r()); this.alreadyExpanded = true; @@ -546,7 +548,7 @@ export default class CanvasTextureRenderer implements ITextureRenderer { } else { ctx.fillStyle = '#ffffff'; } - ctx.fillRect(2, i, 2, 1); + ctx.fillRect(CanvasTextureRenderer.SELECTION_DRAW_WIDTH, i, CanvasTextureRenderer.SELECTION_DRAW_WIDTH, 1); } ctx.save(); }); From 43c240f49f45218d32cb1024721eb64bbf1372d6 Mon Sep 17 00:00:00 2001 From: Holger Stitz Date: Mon, 29 Oct 2018 10:33:19 +0100 Subject: [PATCH 08/16] Fix typo --- src/model/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/index.ts b/src/model/index.ts index f70316dc9..e918f2961 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -92,7 +92,7 @@ export {default as Ranking, ISortCriteria} from './Ranking'; export {default as ReduceColumn, createReduceDesc, IReduceDesc, IReduceColumnDesc} from './ReduceColumn'; export {default as ScriptColumn, createScriptDesc, IScriptDesc, IScriptColumnDesc} from './ScriptColumn'; export {default as SelectionColumn, createSelectionDesc, ISelectionColumnDesc} from './SelectionColumn'; -export {default as DeteilColumn, createDetailDesc, IDetailColumnDesc} from './OverviewDetailColumn'; +export {default as DetailColumn, createDetailDesc, IDetailColumnDesc} from './OverviewDetailColumn'; export * from './SetColumn'; export {default as SetColumn} from './SetColumn'; export {default as StackColumn, createStackDesc} from './StackColumn'; From 7aa6ec46706cc394cc62638c13a4c263e0c8d710 Mon Sep 17 00:00:00 2001 From: Holger Stitz Date: Mon, 29 Oct 2018 10:39:08 +0100 Subject: [PATCH 09/16] Fix compile error --- src/ui/taggle/Taggle.ts | 78 ++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/ui/taggle/Taggle.ts b/src/ui/taggle/Taggle.ts index 614f8ed51..198796450 100644 --- a/src/ui/taggle/Taggle.ts +++ b/src/ui/taggle/Taggle.ts @@ -42,50 +42,50 @@ export default class Taggle extends ALineUp { }); this.renderer.pushUpdateAble((ctx) => this.panel!.update(ctx)); this.node.insertBefore(this.panel.node, this.node.firstChild); - { - this.panel.node.insertAdjacentHTML('afterbegin', `