Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions projects/ngx-reactome-cytoscape-style/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@
"peerDependencies": {
"cytoscape": "^3.30.0",
"cytoscape-layers": "^3.0.0",
"chroma-js": "^3.1.2",
"lodash": "^4.17.21"
"chroma-js": "^3.1.2"
},
"dependencies": {
"tslib": "^2.3.0"
},
"devDependencies": {
"@types/chroma-js": "^3.1.1",
"@types/lodash": "^4.17.0"
"@types/chroma-js": "^3.1.1"
},
"sideEffects": false
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import cytoscape from 'cytoscape';
import { gene } from './shape/gene-shape';
import { isArray, isNumber, memoize } from 'lodash';
import { memoize } from '../util/memoize';
import { molecule } from './shape/molecule-shape';
import { protein } from './shape/protein-shape';
import { rna } from './shape/rna-shape';
Expand Down Expand Up @@ -105,8 +105,10 @@ export const imageBuilder = (properties: Properties, style: Style) =>
...l,
'background-image': svgStr(
l['background-image'] as string,
isNumber(l['background-width']) ? l['background-width'] : drawerParams.width,
isNumber(l['background-height']) ? l['background-height'] : drawerParams.height
typeof l['background-width'] === 'number' ? l['background-width'] : drawerParams.width,
typeof l['background-height'] === 'number'
? l['background-height']
: drawerParams.height
),
}));

Expand Down Expand Up @@ -167,20 +169,23 @@ function _expToGradient(
exp: number | undefined;
width: number;
}[] = [];
const size = exps.reduce((l: number, e) => (e !== undefined && isArray(e) ? l + e[1] : l + 1), 0);
const size = exps.reduce(
(l: number, e) => (e !== undefined && Array.isArray(e) ? l + e[1] : l + 1),
0
);
// Nothing to divide the width between: no gradient, not NaN widths.
if (!(size > 0)) return undefined;
const delta = 1 / size;
exps.forEach((exp, _i) => {
const p = stops.length - 1;
const realExp = isArray(exp) ? exp[0]! : exp;
const realExp = Array.isArray(exp) ? exp[0]! : exp;
if (stops.length !== 0 && stops[p].exp === realExp) {
// A [value, count] pair is count shares of the width, merged or not.
const share = isArray(exp) ? delta * exp[1] : delta;
const share = Array.isArray(exp) ? delta * exp[1] : delta;
stops[p].stop += share;
stops[p].width += share;
} else {
if (isArray(exp)) {
if (Array.isArray(exp)) {
stops.push({
start: stops[p]?.stop || 0,
stop: (stops[p]?.stop || 0) + delta * exp[1],
Expand Down Expand Up @@ -316,7 +321,7 @@ export function clearDrawersCache() {
for (const value of classToDrawers.values()) {
value.cache.clear();
}
OMMITED_ICON.cache.clear!();
OMMITED_ICON.cache.clear();
}

function aggregate<T extends object, K extends keyof T>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { extract } from './properties-utils';
import { Properties } from './properties';
import { ReactomeEvent, ReactomeEventTypes } from './model/reactome-event.model';
import Layers, { IHTMLLayer, layers, LayersPlugin } from 'cytoscape-layers';
import * as _ from 'lodash';
import { throttle } from './util/throttle';

cytoscape.use(Layers);
type RenderableHTMLElement = HTMLElement & {
render: _.DebouncedFunc<() => void>;
render: () => void;
};

/**
Expand Down Expand Up @@ -334,7 +334,7 @@ export class Interactivity {
elem.style.display = 'flex';
const video = elem.children[0] as HTMLVideoElement;

elem.render = _.throttle(() => {
elem.render = throttle(() => {
if (isElementInViewport(elem)) {
// console.log('rendering', name)
if (
Expand Down
2 changes: 1 addition & 1 deletion projects/ngx-reactome-cytoscape-style/src/lib/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -910,7 +910,7 @@ export class Style {
}

clearCache() {
this.imageBuilder.cache.clear!();
this.imageBuilder.cache.clear();
clearDrawersCache();
}

Expand Down
52 changes: 52 additions & 0 deletions projects/ngx-reactome-cytoscape-style/src/lib/util/memoize.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, expect, it, vi } from 'vitest';
import { memoize as lodashMemoize } from 'lodash';
import { memoize } from './memoize';

// Replaces lodash's memoize, so it is held to lodash's behaviour.
/** What both offer, so the same checks run against each. */
type Impl = <F extends (...args: never[]) => unknown>(
fn: F,
resolver?: (...args: Parameters<F>) => unknown
) => F & { cache: { clear(): void } };

describe('memoize', () => {
for (const [name, impl] of [
['lodash', lodashMemoize as unknown as Impl],
['ours', memoize as Impl],
] as const) {
it(`${name}: keys on the first argument by default`, () => {
const fn = vi.fn((_a: object, b: number) => b);
const m = impl(fn);
const key = {};
expect(m(key, 1)).toBe(1);
expect(m(key, 2)).toBe(1);
expect(m({}, 3)).toBe(3);
expect(fn).toHaveBeenCalledTimes(2);
});

it(`${name}: keys on what the resolver returns`, () => {
const fn = vi.fn((_p: object, size: { w: number }) => size.w * 2);
const m = impl(fn, (_p: object, size: { w: number }) => `${size.w}`);
expect(m({}, { w: 2 })).toBe(4);
expect(m({}, { w: 2 })).toBe(4);
expect(fn).toHaveBeenCalledTimes(1);
});

it(`${name}: computes again once its cache is cleared`, () => {
const fn = vi.fn((a: string) => a.length);
const m = impl(fn);
m('abc');
m.cache.clear();
m('abc');
expect(fn).toHaveBeenCalledTimes(2);
});

it(`${name}: remembers an undefined result`, () => {
const fn = vi.fn((_a: string) => undefined);
const m = impl(fn);
m('x');
m('x');
expect(fn).toHaveBeenCalledTimes(1);
});
}
});
27 changes: 27 additions & 0 deletions projects/ngx-reactome-cytoscape-style/src/lib/util/memoize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* A function that remembers its results, keyed on its first argument or on what
* `resolver` returns for its arguments -- lodash's memoize, which this replaces.
*
* lodash is CommonJS, so a single named import of it carried the whole library
* (about 73 KB) into every bundle that used this one, the embeddable diagram's
* included. Nothing else here needed it.
*/
export type Memoized<F extends (...args: never[]) => unknown> = F & {
/** Every remembered result; clear it to have them computed again. */
readonly cache: Map<unknown, ReturnType<F>>;
};

export function memoize<F extends (...args: never[]) => unknown>(
fn: F,
resolver?: (...args: Parameters<F>) => unknown
): Memoized<F> {
const cache = new Map<unknown, ReturnType<F>>();
const memoized = (...args: Parameters<F>): ReturnType<F> => {
const key = resolver ? resolver(...args) : args[0];
if (cache.has(key)) return cache.get(key) as ReturnType<F>;
const result = fn(...args) as ReturnType<F>;
cache.set(key, result);
return result;
};
return Object.assign(memoized, { cache }) as unknown as Memoized<F>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { throttle as lodashThrottle } from 'lodash';
import { throttle } from './throttle';

// Replaces lodash's throttle(fn, wait), so it is held to lodash's behaviour: the
// first call runs at once, calls inside the window run once at its end, and a
// quiet window runs nothing.
describe('throttle', () => {
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());

/** When `fn` ran, for calls made at the given times. */
function runs(impl: (fn: () => void, wait: number) => () => void, callsAt: number[]) {
const at: number[] = [];
const start = Date.now();
const t = impl(() => at.push(Date.now() - start), 500);
let now = 0;
for (const time of callsAt) {
vi.advanceTimersByTime(time - now);
now = time;
t();
}
vi.advanceTimersByTime(5000);
return at;
}

const sequences: [string, number[]][] = [
['one call', [0]],
['a burst', [0, 10, 20, 30]],
['steady calls', [0, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100]],
['two bursts apart', [0, 50, 2000, 2050]],
['a call just after a window', [0, 499, 501, 1200]],
];

// And some irregular ones, the way render events arrive: fixed seeds, so a
// failure names a sequence that can be replayed.
for (const seed of [1, 7, 42, 1234]) {
let x = seed;
const next = () => (x = (x * 1103515245 + 12345) % 2 ** 31) / 2 ** 31;
const calls: number[] = [];
let time = 0;
for (let i = 0; i < 40; i++) calls.push((time += Math.floor(next() * 400)));
sequences.push([`irregular, seed ${seed}`, calls]);
}

for (const [name, calls] of sequences) {
it(`runs when lodash's does: ${name}`, () => {
expect(runs(throttle, calls)).toEqual(runs(lodashThrottle, calls));
});
}

/** When `fn` ran when the clock is set back mid-sequence, for each implementation. */
function runsWithClockBack(impl: (fn: () => void, wait: number) => () => void) {
vi.setSystemTime(100_000);
const at: number[] = [];
const t = impl(() => at.push(Date.now()), 500);
t();
vi.advanceTimersByTime(100);
t();
// The clock goes back, as a system clock can: lodash treats that as a fresh start.
vi.setSystemTime(50_000);
t();
vi.advanceTimersByTime(5000);
return at;
}

it("runs when lodash's does when the clock goes backwards", () => {
expect(runsWithClockBack(throttle)).toEqual(runsWithClockBack(lodashThrottle));
});

/** When `fn` ran when `fn` itself calls the throttled function, as a render can. */
function runsReentrant(impl: (fn: () => void, wait: number) => () => void) {
const start = Date.now();
const at: number[] = [];
let depth = 0;
const t: () => void = impl(() => {
at.push(Date.now() - start);
if (depth++ < 3) t();
}, 500);
t();
vi.advanceTimersByTime(5000);
return at;
}

it("runs when lodash's does when fn calls it again", () => {
expect(runsReentrant(throttle)).toEqual(runsReentrant(lodashThrottle));
});
});
61 changes: 61 additions & 0 deletions projects/ngx-reactome-cytoscape-style/src/lib/util/throttle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* `fn`, run at most once every `wait` milliseconds: lodash's throttle(fn, wait),
* which this replaces (see memoize.ts for why).
*
* A port of lodash's algorithm -- its throttle is its debounce with `leading`,
* `trailing` and `maxWait = wait` -- rather than a simpler timer. The first call
* runs at once; calls inside a window run `fn` once more when it ends; and a
* window's end follows the last call, as lodash's does. throttle.spec.ts holds
* the two to the same timings.
*/
export function throttle(fn: () => void, wait: number): () => void {
let lastCallTime: number | undefined;
let lastInvokeTime = 0;
let timer: ReturnType<typeof setTimeout> | undefined;
let pending = false;

const invoke = (time: number) => {
pending = false;
lastInvokeTime = time;
fn();
};

const shouldInvoke = (time: number) => {
if (lastCallTime === undefined) return true;
const sinceCall = time - lastCallTime;
return sinceCall >= wait || sinceCall < 0 || time - lastInvokeTime >= wait;
};

const remainingWait = (time: number) =>
Math.min(wait - (time - (lastCallTime ?? time)), wait - (time - lastInvokeTime));

const timerExpired = () => {
const time = Date.now();
if (shouldInvoke(time)) {
timer = undefined;
if (pending) invoke(time);
return;
}
timer = setTimeout(timerExpired, remainingWait(time));
};

return () => {
const time = Date.now();
const invoking = shouldInvoke(time);
pending = true;
lastCallTime = time;
if (invoking) {
if (timer === undefined) {
lastInvokeTime = time;
timer = setTimeout(timerExpired, wait);
invoke(time);
return;
}
clearTimeout(timer);
timer = setTimeout(timerExpired, wait);
invoke(time);
return;
}
timer ??= setTimeout(timerExpired, wait);
};
}
Loading