Skip to content

Commit

Permalink
perf(core): stretch certain marks instead of redraw (#1018)
Browse files Browse the repository at this point in the history
* feat: stretchable marks

* feat: stretchGraphics option

* feat: stretchGraphicsThreshold

Co-authored-by: Sehi L'Yi <sehilyi@gmail.com>

* feat: add stretchGraphicsThreshold to schema

* fix: check all resolved tracks stretchable

* chore: make first render more clear

* chore(editor): add a demo for comparing rendering approaches (#1023)

* chore: add a demo for perf comparison

* chore: add a demo thumbnail

---------

Co-authored-by: sehilyi <sehliyi@gmail.com>

* fix: make alignment example wider

* chore: linting

* chore: cleanup

* fix: update schema

* feat: add title

* fix: no stretching when mouse interactions

---------

Co-authored-by: Sehi L'Yi <sehilyi@gmail.com>
Co-authored-by: sehilyi <sehliyi@gmail.com>
  • Loading branch information
3 people committed Jan 8, 2024
1 parent ca315b1 commit 0b405ba
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 2 deletions.
11 changes: 11 additions & 0 deletions editor/example/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export type ExampleGroup =
| 'Coordinated Multiple Views'
| 'Applications'
| 'Track Templates'
| 'Experimental'
| 'Doc'
| 'Unassigned';

Expand Down Expand Up @@ -75,6 +76,10 @@ export const ExampleGroups: {
name: 'Track Templates',
description: 'Built-in track templates that allow creating common tracks, like ideograms and gene annotations.'
},
{
name: 'Experimental',
description: 'Examples that include experimental features, such as performance improvements.'
},
{
name: 'Doc',
description: 'Examples used in the official documentation.'
Expand Down Expand Up @@ -412,6 +417,12 @@ export const editorExampleObj: {
spec: JsonExampleSpecs.EX_SPEC_ALIGNMENT_CHART,
image: THUMBNAILS.ALIGNMENT
},
PERF_ALIGNMENT: {
group: 'Experimental',
name: 'Performance Comparison: Stretching Tiles',
spec: JsonExampleSpecs.EX_SPEC_PERF_ALIGNMENT,
image: THUMBNAILS.PERF_ALIGNMENT
},
CORCES_ET_AL: {
group: 'Coordinated Multiple Views',
name: 'Corces et al. 2020',
Expand Down
2 changes: 2 additions & 0 deletions editor/example/json-spec/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { EX_SPEC_CYTOBANDS } from './ideograms';
import { EX_SPEC_PILEUP } from './pileup';
import { EX_SPEC_TEMPLATE } from './track-template';
import { EX_SPEC_MOUSE_EVENT } from './mouse-event';
import { EX_SPEC_PERF_ALIGNMENT } from './perf-alignment'
import { EX_SPEC_DEBUG } from './debug';

export const JsonExampleSpecs = {
Expand All @@ -42,6 +43,7 @@ export const JsonExampleSpecs = {
EX_SPEC_RESPONSIVE_TRACK_WISE_COMPARISON,
EX_SPEC_ALIGNMENT_CHART,
EX_SPEC_RESPONSIVE_ALIGNMENT_CHART,
EX_SPEC_PERF_ALIGNMENT,
EX_SPEC_MARK_DISPLACEMENT,
EX_SPEC_CIRCULAR_OVERVIEW_LINEAR_DETAIL,
EX_SPEC_SARS_COV_2,
Expand Down
30 changes: 30 additions & 0 deletions editor/example/json-spec/perf-alignment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { GoslingSpec } from '@gosling-lang/gosling-schema';
import { alignmentWithText } from './responsive-alignment';

const commonProps = { width: 800, height: 400, xAxis: false, rowLegend: false, colorLegend: false };
export const EX_SPEC_PERF_ALIGNMENT: GoslingSpec = {
zoomLimits: [1, 396],
xDomain: { interval: [350, 396] },
assembly: 'unknown',
title: 'Smoother Zoom',
subtitle: 'Rather than redrawing every element at every frame, we can scale existing elements',
views: [
{
tracks: [
{
...alignmentWithText(commonProps),
title: 'New Approach: Stretching Tiles',
experimental: { stretchGraphics: true }
}
]
},
{
tracks: [
{
...alignmentWithText(commonProps),
title: 'Original Approach'
}
]
}
]
};
2 changes: 2 additions & 0 deletions editor/example/thumbnails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import MARK_DISPLACEMENT from './thumbnails/MARK_DISPLACEMENT.png';
import MATRIX_HFFC6 from './thumbnails/MATRIX_HFFC6.gif';
import MATRIX from './thumbnails/MATRIX.png';
import MOUSE_EVENT from './thumbnails/MOUSE_EVENT.png';
import PERF_ALIGNMENT from './thumbnails/PERF_ALIGNMENT.png';
import RESPONSIVE_COMPARATIVE_MATRICES from './thumbnails/RESPONSIVE_COMPARATIVE_MATRICES.gif';
import RESPONSIVE_IDEOGRAM from './thumbnails/RESPONSIVE_IDEOGRAM.gif';
import RESPONSIVE_MULTIVEC from './thumbnails/RESPONSIVE_MULTIVEC.gif';
Expand Down Expand Up @@ -48,6 +49,7 @@ export const THUMBNAILS = {
MATRIX_HFFC6,
MATRIX,
MOUSE_EVENT,
PERF_ALIGNMENT,
RESPONSIVE_COMPARATIVE_MATRICES,
RESPONSIVE_IDEOGRAM,
RESPONSIVE_MULTIVEC,
Expand Down
Binary file added editor/example/thumbnails/PERF_ALIGNMENT.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
144 changes: 144 additions & 0 deletions src/gosling-schema/gosling.schema.json

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions src/gosling-schema/gosling.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,25 @@ interface SingleTrackBase extends CommonTrackDef {
* @default false
*/
performanceMode?: boolean;

/**
* Performance rendering option.
* By default, certain marks ('bar', 'line', 'rect', 'area') are stretched when zooming in/out to improve
* rendering performance. No marks will be stretched in circular layouts.
*
* When this option is set to true, all marks will be stretched when zooming in/out.
* When this option is set to false, all marks will be rerendered when zooming in/out.
*
*/
stretchGraphics?: boolean;

/**
* Threshold for stretching graphics. If the graphics are scaled larger than the threshold, then the graphic
* will be rerendered. If the graphics are scaled smaller than 1/threshold (e.g., 1/2), then the graphic will
* be rerendered. This is to prevent the graphics from being stretched too much.
* @default 1.5
*/
stretchGraphicsThreshold?: number;
};

// Mark
Expand Down
71 changes: 69 additions & 2 deletions src/tracks/gosling-track/gosling-track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,14 +333,44 @@ const factory: PluginTrackFactory<Tile, GoslingTrackOptions> = (HGC, context, op
override drawTile(tile: Tile) {
if (PRINT_RENDERING_CYCLE) console.warn('drawTile(tile)');

tile.drawnAtScale = this._xScale.copy(); // being used in `super.draw()`

/**
* If we don't have info about the tile, we can't draw anything.
*/
const tileInfo = this.#processedTileInfo[tile.tileId];
if (!tileInfo) {
// We do not have a track model prepared to visualize
return;
}

/**
* Add a copy of the track scale to the tile. The tile needs its own scale because we will use it to
* determine how much the tile has been stretched (if we are stretching the graphics)
*/
if (!tile.drawnAtScale) {
// This is the first time this tile is being drawn
tile.drawnAtScale = this._xScale.copy();
}

/**
* For certain types of marks and layouts (linear), we can stretch the graphics to avoid redrawing
* This is much more performant than redrawing everything at every frame
*/
const [graphicsXScale, graphicsXPos] = this.getXScaleAndOffset(tile.drawnAtScale);
const isFirstRender = graphicsXScale === 1; // The graphicsXScale is 1 if first time the tile is being drawn
if (!this.#isTooStretched(graphicsXScale) && this.#hasStretchableGraphics() && !isFirstRender) {
// Stretch the graphics
tile.graphics.scale.x = graphicsXScale;
tile.graphics.position.x = graphicsXPos;
return;
}

/**
* If we can't stretch the graphics, we need to redraw everything!
*/

// We need the tile scale to match the scale of the track
tile.drawnAtScale = this._xScale.copy();
// Clear the graphics and redraw everything
tile.graphics?.clear();
tile.graphics?.removeChildren();

Expand Down Expand Up @@ -1472,6 +1502,43 @@ const factory: PluginTrackFactory<Tile, GoslingTrackOptions> = (HGC, context, op
}
});
}

/**
* Used in drawTile()
* Checks if the track has marks which are stretchable. Stretching
* is not supported for circular layouts or 2D tracks
*/
#hasStretchableGraphics() {
const hasStretchOption = this.options.spec.experimental?.stretchGraphics;
if (hasStretchOption === true) {
return true;
} else if (hasStretchOption === false) {
return false;
}
// The default behavior is that we stretch when stretching looks acceptable
const isFirstTrack1D = !Is2DTrack(this.getResolvedTracks()[0]);
const isNotCircularLayout = this.options.spec.layout !== 'circular';
const stretchableMarks = ['bar', 'line', 'rect', 'area'];
const hasStretchableMark = this.getResolvedTracks().reduce(
(acc, spec) => acc && stretchableMarks.includes(spec.mark),
true
);
const noMouseInteractions = !this.options.spec.experimental?.mouseEvents;

return isFirstTrack1D && isNotCircularLayout && hasStretchableMark && noMouseInteractions;
}

/**
* Used in drawTile()
* Checks if the tile Graphic is too stretched. If so, it returns true.
* @param stretchFactor The factor by which the tile is stretched
* @returns True if the tile is too stretched, false otherwise
*/
#isTooStretched(stretchFactor: number) {
const defaultThreshold = 1.5;
const threshold = this.options.spec.experimental?.stretchGraphicsThreshold ?? defaultThreshold;
return stretchFactor > threshold || stretchFactor < 1 / threshold;
}
}
return new GoslingTrackClass();
};
Expand Down

0 comments on commit 0b405ba

Please sign in to comment.