Skip to content
Open
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: 4 additions & 2 deletions example/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { TimeGraphChartGrid } from "timeline-chart/lib/layer/time-graph-chart-gr
import { TimeGraphVerticalScrollbar } from "timeline-chart/lib/layer/time-graph-vertical-scrollbar";
import { TimeGraphChartArrows } from "timeline-chart/lib/layer/time-graph-chart-arrows";
import { TimeGraphRangeEventsLayer } from "timeline-chart/lib/layer/time-graph-range-events-layer";
import { TimeGraphChartRichCursor } from "timeline-chart/lib/layer/time-graph-chart-rich-cursor";

const styleConfig = {
mainWidth: 1000,
Expand Down Expand Up @@ -154,10 +155,11 @@ const timeGraphChartArrows = new TimeGraphChartArrows('timeGraphChartArrows', ro
const timeGraphSelectionRange = new TimeGraphChartSelectionRange('chart-selection-range', { color: styleConfig.cursorColor });
const timeGraphChartCursors = new TimeGraphChartCursors('chart-cursors', timeGraphChart, rowController, { color: styleConfig.cursorColor });
const timeGraphChartRangeEvents = new TimeGraphRangeEventsLayer('timeGraphChartRangeEvents', providers);
const timeGraphRichCursor = new TimeGraphChartRichCursor('chart-rich-cursor', timeGraphChart, rowController);

timeGraphChartContainer.addLayers([timeGraphChartGridLayer, timeGraphChart,
timeGraphChartArrows, timeGraphSelectionRange,
timeGraphChartCursors, timeGraphChartRangeEvents]);
timeGraphChartArrows, timeAxisCursors, timeGraphSelectionRange,
timeGraphChartCursors, timeGraphChartRangeEvents, timeGraphRichCursor]);

timeGraphChart.registerMouseInteractions({
click: el => {
Expand Down
68 changes: 68 additions & 0 deletions example/src/test-data-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ export class TestDataProvider {
timeGraphEntries.model.entries.forEach(entry => {
rowIds.push(entry.id);
});
// Add XY plot row IDs
rowIds.push(-100, -101);
return rowIds;
}

Expand Down Expand Up @@ -292,6 +294,13 @@ export class TestDataProvider {
});
});

// Generate XY plot rows with fixed-time EKG-style data
const xyRows: TimelineChart.TimeGraphRowModel[] = [
this.createXYRow(-100, 'Heart Rate (EKG)', range, (t) => this.ekgWaveform(t)),
this.createXYRow(-101, 'Respiration (EKG)', range, (t) => this.ekgWaveform(t * 0.4 + 0.1))
];
rows.push(...xyRows);

return {
id: "",
arrows,
Expand All @@ -300,4 +309,63 @@ export class TestDataProvider {
totalLength: this.totalLength
};
}

private createXYRow(id: number, name: string, range: TimelineChart.TimeGraphRange, fn: (t: number) => number): TimelineChart.TimeGraphRowModel {
// Generate points with fixed time positions spanning the full trace.
// Only emit points that fall within the requested range (with small buffer).
const totalLen = Number(this.totalLength);
const rangeStart = Number(range.start);
const rangeEnd = Number(range.end);
const rangeLen = rangeEnd - rangeStart;

// Use enough points to get ~1 point per 2 pixels at current resolution
const numPoints = Math.max(200, Math.min(2000, Math.round(rangeLen / (totalLen / 2000))));
const step = rangeLen / (numPoints - 1);

const points: TimelineChart.TimeGraphXYPoint[] = [];
for (let i = 0; i < numPoints; i++) {
const timeNum = rangeStart + i * step;
const t = timeNum / totalLen; // normalized position in full trace
const time = BigInt(Math.round(timeNum));
points.push({ time, value: Math.max(0, Math.min(1, fn(t))) });
}
return {
id,
name,
range: { start: range.start, end: range.end },
states: [],
annotations: [],
xySeries: [{ id: `xy_${id}`, label: name, color: id === -100 ? 0x22cc44 : 0x44aaff, points }],
prevPossibleState: range.start,
nextPossibleState: range.end
};
}

/** EKG-style waveform: flat baseline with periodic sharp QRS-like spikes */
private ekgWaveform(t: number): number {
// Repeat every cycle; ~20 beats across the full trace
const cycles = 20;
const phase = (t * cycles) % 1;

// P wave (small bump)
if (phase > 0.1 && phase < 0.2) {
const p = (phase - 0.1) / 0.1;
return 0.5 + 0.08 * Math.sin(p * Math.PI);
}
// QRS complex (sharp spike)
if (phase > 0.25 && phase < 0.45) {
const q = (phase - 0.25) / 0.2;
if (q < 0.2) return 0.5 - 0.1 * (q / 0.2); // Q dip
if (q < 0.5) return 0.5 - 0.1 + 0.9 * ((q - 0.2) / 0.3); // R spike up
if (q < 0.7) return 0.5 + 0.8 - 1.0 * ((q - 0.5) / 0.2); // R spike down
return 0.5 - 0.2 + 0.2 * ((q - 0.7) / 0.3); // S recovery
}
// T wave (broad bump)
if (phase > 0.55 && phase < 0.75) {
const tw = (phase - 0.55) / 0.2;
return 0.5 + 0.12 * Math.sin(tw * Math.PI);
}
// Baseline
return 0.5;
}
}
3 changes: 2 additions & 1 deletion example/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ module.exports = {
{
enforce: "pre",
test: /\.js$/,
loader: "source-map-loader"
loader: "source-map-loader",
exclude: /node_modules\/@pixi/
},
{
test: /\.png$/,
Expand Down
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@
"devDependencies": {
"@eclipse-dash/nodejs-wrapper": "^0.0.1",
"lerna": "^8.0.0",
"typescript": "^5.2.2"
"typescript": "^5.9.3"
},
"workspaces": [
"timeline-chart",
"example"
],
"dependencies": {
"pixi.js": "^5.0.0"
}
"dependencies": {},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
2 changes: 1 addition & 1 deletion timeline-chart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"glob": "^7.1.6",
"keyboard-key": "1.1.0",
"lodash.throttle": "^4.1.1",
"pixi.js-legacy": "^5.3.3"
"pixi.js-legacy": "^7.3.2"
},
"devDependencies": {
"@types/enzyme": "^3.10.10",
Expand Down
247 changes: 247 additions & 0 deletions timeline-chart/src/__tests__/pixi-v7-migration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import * as PIXI from 'pixi.js-legacy';
import { TimeGraphComponent } from '../components/time-graph-component';
import { TimeGraphContainer } from '../time-graph-container';
import { TimeGraphUnitController } from '../time-graph-unit-controller';
import { FontController } from '../time-graph-font-controller';

// Concrete implementation for testing abstract TimeGraphComponent
class TestComponent extends TimeGraphComponent<any> {
render() {
this.rect({
position: { x: 0, y: 0 },
width: 50,
height: 20,
color: 0xff0000,
opacity: 1
});
}
}

describe('Pixi v7 Migration', () => {
describe('TimeGraphComponent - FederatedPointerEvent', () => {
let component: TestComponent;

beforeEach(() => {
component = new TestComponent('test-comp');
});

afterEach(() => {
component.destroy();
});

it('creates a Graphics display object', () => {
expect(component.displayObject).toBeInstanceOf(PIXI.Graphics);
});

it('renders rect without errors', () => {
expect(() => component.render()).not.toThrow();
});

it('addEvent sets interactive on display object', () => {
const handler = jest.fn();
const target = new PIXI.Graphics();
component.addEvent('click', handler, target);
expect(target.interactive).toBe(true);
});

it('addEvent registers event handler via on()', () => {
const handler = jest.fn();
const target = new PIXI.Graphics();
const onSpy = jest.spyOn(target, 'on');
component.addEvent('mouseover', handler, target);
expect(onSpy).toHaveBeenCalledWith('mouseover', expect.any(Function));
});

it('addEvent handler is callable', () => {
const handler = jest.fn();
const target = new PIXI.Graphics();
component.addEvent('click', handler, target);
// Simulate event emission
target.emit('click', { type: 'click' });
expect(handler).toHaveBeenCalled();
});

it('supports all interaction types', () => {
const types: Array<'mouseover' | 'mouseout' | 'mousemove' | 'mousedown' | 'mouseup' | 'mouseupoutside' | 'rightdown' | 'click'> = [
'mouseover', 'mouseout', 'mousemove', 'mousedown', 'mouseup', 'mouseupoutside', 'rightdown', 'click'
];
types.forEach(type => {
const handler = jest.fn();
const target = new PIXI.Graphics();
expect(() => component.addEvent(type, handler, target)).not.toThrow();
});
});

it('clear() clears graphics without error', () => {
component.render();
expect(() => component.clear()).not.toThrow();
});

it('update() re-renders', () => {
const renderSpy = jest.spyOn(component, 'render');
component.update();
expect(renderSpy).toHaveBeenCalled();
});

it('getPIXIOpacity returns correct values', () => {
expect((component as any).getPIXIOpacity(undefined)).toBe(1);
expect((component as any).getPIXIOpacity(0)).toBe(0.001);
expect((component as any).getPIXIOpacity(0.5)).toBe(0.5);
expect((component as any).getPIXIOpacity(1)).toBe(1);
});
});

describe('TimeGraphContainer - v7 Application options', () => {
it('creates container without transparent option error', () => {
const unitController = new TimeGraphUnitController(BigInt(1000), { start: BigInt(0), end: BigInt(500) });
expect(() => {
const container = new TimeGraphContainer(
{ id: 'test', width: 200, height: 100, transparent: true },
unitController
);
container.destroy();
}).not.toThrow();
});

it('creates container with backgroundColor', () => {
const unitController = new TimeGraphUnitController(BigInt(1000), { start: BigInt(0), end: BigInt(500) });
expect(() => {
const container = new TimeGraphContainer(
{ id: 'test-bg', width: 200, height: 100, backgroundColor: 0x1a1a1a },
unitController
);
container.destroy();
}).not.toThrow();
});

it('canvas property returns HTMLCanvasElement', () => {
const unitController = new TimeGraphUnitController(BigInt(1000), { start: BigInt(0), end: BigInt(500) });
const container = new TimeGraphContainer(
{ id: 'test-canvas', width: 200, height: 100 },
unitController
);
expect(container.canvas).toBeInstanceOf(HTMLCanvasElement);
container.destroy();
});

it('accepts external canvas', () => {
const unitController = new TimeGraphUnitController(BigInt(1000), { start: BigInt(0), end: BigInt(500) });
const extCanvas = document.createElement('canvas');
const container = new TimeGraphContainer(
{ id: 'test-ext', width: 300, height: 150 },
unitController,
extCanvas
);
expect(container.canvas).toBe(extCanvas);
container.destroy();
});

it('updateCanvas resizes without error', () => {
const unitController = new TimeGraphUnitController(BigInt(1000), { start: BigInt(0), end: BigInt(500) });
const container = new TimeGraphContainer(
{ id: 'test-resize', width: 200, height: 100 },
unitController
);
expect(() => container.updateCanvas(400, 200)).not.toThrow();
container.destroy();
});
});

describe('FontController - v7 TextStyle', () => {
it('creates without error', () => {
expect(() => new FontController()).not.toThrow();
});

it('getDefaultFont returns a font name string', () => {
const fc = new FontController();
const { fontName } = fc.getDefaultFont();
expect(typeof fontName).toBe('string');
expect(fontName.length).toBeGreaterThan(0);
});

it('createFont creates bitmap font without error', () => {
const fc = new FontController();
expect(() => fc.createFont('White', 10)).not.toThrow();
expect(() => fc.createFont('Black', 12)).not.toThrow();
});

it('getFont returns correct font for dark background', () => {
const fc = new FontController();
// Dark color (0x000000) should get white font
const { fontName } = fc.getFont(0x000000, 8);
expect(fontName).toContain('White');
});

it('getFont returns correct font for light background', () => {
const fc = new FontController();
// Light color (0xffffff) should get black font
const { fontName } = fc.getFont(0xffffff, 8);
expect(fontName).toContain('Black');
});

it('getFont caches font colors', () => {
const fc = new FontController();
const { fontName: name1 } = fc.getFont(0x123456, 8);
const { fontName: name2 } = fc.getFont(0x123456, 8);
expect(name1).toBe(name2);
});

it('getFont creates new size maps on demand', () => {
const fc = new FontController();
const { fontName } = fc.getFont(0xffffff, 14);
expect(fontName.length).toBeGreaterThan(0);
});

it('respects minimum font size of 6', () => {
const fc = new FontController();
// Size 2 should be clamped to 6
const { fontName } = fc.getFont(0xffffff, 2);
expect(fontName.length).toBeGreaterThan(0);
});

it('accepts custom font family', () => {
expect(() => new FontController('Arial')).not.toThrow();
});
});

describe('DisplayObject event augmentation', () => {
it('Container supports interactive property', () => {
const c = new PIXI.Container();
c.interactive = true;
expect(c.interactive).toBe(true);
});

it('Container supports cursor property', () => {
const c = new PIXI.Container();
c.cursor = 'pointer';
expect(c.cursor).toBe('pointer');
});

it('Container supports on/off', () => {
const c = new PIXI.Container();
const fn = jest.fn();
expect(() => c.on('click', fn)).not.toThrow();
expect(() => c.off('click', fn)).not.toThrow();
});

it('Graphics supports interactive property', () => {
const g = new PIXI.Graphics();
g.interactive = true;
expect(g.interactive).toBe(true);
});

it('Graphics supports on/emit', () => {
const g = new PIXI.Graphics();
const fn = jest.fn();
g.on('click', fn);
g.emit('click');
expect(fn).toHaveBeenCalled();
});

it('Graphics supports cursor property', () => {
const g = new PIXI.Graphics();
g.cursor = 'crosshair';
expect(g.cursor).toBe('crosshair');
});
});
});
Loading
Loading