Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

perf(core): stretch certain marks instead of redraw #1018

Merged
merged 13 commits into from
Jan 8, 2024
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