Skip to content

Commit

Permalink
implementation of a canvas texture renderer, for Caleydo/lineup_app#3
Browse files Browse the repository at this point in the history
  • Loading branch information
domdir committed Jun 24, 2018
1 parent 06c3ab6 commit 581e36b
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 34 deletions.
1 change: 1 addition & 0 deletions src/styles/engine/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ $engine_assets: '~lineupengine/src/assets';
@import './header';
@import './selection';
@import './row';
@import './texture_renderer';
15 changes: 15 additions & 0 deletions src/styles/engine/_texture_renderer.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
170 changes: 170 additions & 0 deletions src/ui/CanvasTextureRenderer.ts
Original file line number Diff line number Diff line change
@@ -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 = <HTMLElement>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 = <NumbersColumn>column;
newElement = this.generateImage(grouped.map((value) => {
return (<any>value).v[(<any>col.desc).column];
}), CanvasTextureRenderer.getColorScale(col));
} else if (column instanceof NumberColumn) {
const col = <NumberColumn>column;
newElement = this.generateImage(grouped.map((value) => {
return [(<any>value).v[(<any>col.desc).column]];
}), CanvasTextureRenderer.getColorScale(col));
} else if (column instanceof CategoricalsColumn) {
const col = <CategoricalsColumn>column;
newElement = this.generateImage(grouped.map((value) => {
return (<any>value).v[(<any>col.desc).column];
}), CanvasTextureRenderer.getColorScale(col));
} else if (column instanceof CategoricalColumn) {
const col = <CategoricalColumn>column;
newElement = this.generateImage(grouped.map((value) => {
return [(<any>value).v[(<any>col.desc).column]];
}), CanvasTextureRenderer.getColorScale(col));
} else if ('children' in column) {
//handle composite columns
(<CompositeColumn>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<string, string>();
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<number, string>();
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 = <CanvasRenderingContext2D>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';
}
}

91 changes: 58 additions & 33 deletions src/ui/EngineRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<ILineUpOptions>) {
super();
Expand Down Expand Up @@ -133,6 +137,8 @@ export default class EngineRenderer extends AEventDispatcher {
}`);
}

this.textureRenderer = new CanvasTextureRenderer(this.node);

this.initProvider(data);
}

Expand Down Expand Up @@ -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]) || (<IGroupItem>grouped[index]).meta === 'last' || (<IGroupItem>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]) || (<IGroupItem>grouped[index]).meta === 'last' || (<IGroupItem>grouped[index]).meta === 'first last')) {
return groupPadding + pad;
}
return pad;
});
r.render(grouped, rowContext);
});
r.render(grouped, rowContext);
});
}

this.updateSlopeGraphs(rankings);

Expand Down Expand Up @@ -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');
}
}

3 changes: 2 additions & 1 deletion src/ui/taggle/Taggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ export default class Taggle extends ALineUp {
const input = <HTMLInputElement>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;
Expand Down
5 changes: 5 additions & 0 deletions src/ui/taggle/TaggleRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit 581e36b

Please sign in to comment.