Skip to content

Commit

Permalink
Allow shared cross hair
Browse files Browse the repository at this point in the history
This allows to move the cross-hair on one graph and have the move
synchronized on others graph, very useful if you do a deep dive.
  • Loading branch information
ekacnet committed Jun 16, 2024
1 parent 1eb3d41 commit dffcb06
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 25 deletions.
2 changes: 1 addition & 1 deletion .config/jest-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ Object.defineProperty(global, 'matchMedia', {
})),
});

HTMLCanvasElement.prototype.getContext = () => {};
//HTMLCanvasElement.prototype.getContext = () => {};
4 changes: 4 additions & 0 deletions jest-setup.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
// Jest setup provided by Grafana scaffolding
import './.config/jest-setup';

SVGElement.prototype.getComputedTextLength = function () {
return 10;
};
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ let ignoredModules = [nodeModulesToTransform(ESModules)];
exports = {
// Jest configuration provided by Grafana scaffolding
...require('./.config/jest.config'),
setupFiles: ['jest-canvas-mock'],
coverageDirectory: 'coverage/jest',
globals: {
'ts-jest': {
Expand Down
5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"glob": "^10.2.7",
"identity-obj-proxy": "3.0.0",
"jest": "^29.7.0",
"jest-canvas-mock": "^2.5.2",
"jest-environment-jsdom": "^29.5.0",
"node-sass": "^9.0.0",
"prettier": "^2.8.7",
Expand Down
26 changes: 20 additions & 6 deletions src/components/CubismPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {useEffect} from 'react';
import { PanelProps, PanelData, GrafanaTheme2 } from '@grafana/data';
import { DataHoverEvent, PanelProps, PanelData, GrafanaTheme2, EventBus } from '@grafana/data';
import { CubismOptions } from 'types';
import { css } from '@emotion/css';
import { useStyles2, useTheme2 } from '@grafana/ui';
Expand Down Expand Up @@ -163,12 +163,21 @@ const getStyles = (showText: boolean, theme: GrafanaTheme2): StylesGetter => {
};
};

export const adjustCubismCrossHair = (context: cubism.Context, hoverEventData: DataHoverEvent) => {
if (hoverEventData.payload!.data) {
let ts = hoverEventData.payload.data.fields[0].values[hoverEventData.payload.rowIndex!];
let index = context._scale(new Date(ts))
context.focus(Math.floor(index))
}
}

export const D3Graph: React.FC<{
height: number;
width: number;
data: PanelData;
options: CubismOptions;
}> = ({ height, width, data, options }) => {
eventBus: EventBus;
}> = ({ height, width, data, options, eventBus }) => {
let context = cubism.context();
let showText = false;
if (options.text !== undefined && options.text !== null && options.text !== '') {
Expand All @@ -179,20 +188,25 @@ export const D3Graph: React.FC<{
// useState() ...
// eslint-disable-next-line react-hooks/exhaustive-deps
const renderD3 = React.useCallback(
D3GraphRender(context, data, options, styles)
D3GraphRender(context, data, options, styles, eventBus)
, [context, data, options, styles]
)
useEffect(() => {
// Like componentDidMount()
let subscribe = eventBus.getStream(DataHoverEvent).subscribe((data)=>{
adjustCubismCrossHair(context, data);
});
return () => {
context.stop()
context.stop();
subscribe.unsubscribe();
};
});
return <div
ref={renderD3} />;
};

export const CubismPanel: React.FC<Props> = ({ options, data, width, height }) => {
export const CubismPanel: React.FC<Props> = ({ options, data, width, height, eventBus }) => {
return (
<D3Graph height={height} width={width} data={data} options={options} />
<D3Graph height={height} width={width} data={data} options={options} eventBus={eventBus} />
);
};
11 changes: 10 additions & 1 deletion src/components/CubismPanelHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { CubismOptions } from 'types';
import * as cubism from 'cubism-es';
import * as d3 from 'd3';

import { PanelData, DataFrame } from '@grafana/data';
import { DataHoverEvent, EventBus, PanelData, DataFrame } from '@grafana/data';
import { getSerieByName, convertAllDataToCubism } from '../cubism_utils';
import { log_debug } from '../misc_utils';
import { calculateSecondOffset } from '../date_utils';
Expand All @@ -18,6 +18,7 @@ export const D3GraphRender = (
data: PanelData,
options: CubismOptions,
styles: CSSStyles,
eventBus: EventBus,
convertDatahelper: (d: DataFrame[], n: number[], o: any, z: number) => cubism.Metric[] = convertAllDataToCubism
): ((wrapperDiv: HTMLDivElement | null) => void) => {
return (panelDiv: HTMLDivElement | null) => {
Expand Down Expand Up @@ -168,6 +169,14 @@ export const D3GraphRender = (
if (i === null) {
canvasDiv.selectAll('.value').style('right', null);
} else {
let val = context._scale.invert(i);
eventBus.publish(
new DataHoverEvent({
point: {
time: val,
},
})
);
const rightStyle: string = context.size() - i + 'px';
canvasDiv.selectAll('.value').style('right', rightStyle);
}
Expand Down
1 change: 1 addition & 0 deletions src/fixes/cubism-es.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ declare module 'cubism-es' {
zoom(f?: zoomCallback): ZoomContext;
setCSSClass(string, string);
getCSSClass(string): string;
focus(number);
_scale: d3.scale;
}

Expand Down
121 changes: 106 additions & 15 deletions src/tests/CubismPanelHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,13 @@ describe('D3GraphRender', () => {
let mockPanelDiv: HTMLDivElement;
let mockTicks: any;
let mockExtent: any;
let mockEventBus: any;
const oldConsole = global.console.log;

beforeEach(() => {
mockEventBus = {
publish: jest.fn(() => {}),
};
mockTicks = jest.fn(() => {
return mockContext.axis();
});
Expand Down Expand Up @@ -188,27 +192,40 @@ describe('D3GraphRender', () => {
});

// Mock console functions for testing
//global.console.log = jest.fn();
});

afterEach(() => {
global.console.log = oldConsole;
jest.clearAllMocks();
});
it('should not render if panelDiv is null or data.series is empty', () => {
const renderFn = D3GraphRender(mockContext, getData(86400), mockOptions, mockStyles, convertAllDataToCubism);
const renderFn = D3GraphRender(
mockContext,
getData(86400),
mockOptions,
mockStyles,
mockEventBus,
convertAllDataToCubism
);
expect(renderFn(null)).toBeUndefined();
});

it('should not render graph when data series is empty ', () => {
const renderFn = D3GraphRender(mockContext, mockData, mockOptions, mockStyles, convertAllDataToCubism);
const renderFn = D3GraphRender(
mockContext,
mockData,
mockOptions,
mockStyles,
mockEventBus,
convertAllDataToCubism
);
expect(renderFn(mockPanelDiv)).toBeUndefined();
});
it('should call convertDataToCubism with Auto', () => {
let data = getData(86400);
data.series[0].length = 0;
const mockHelper = jest.fn(() => [null]);
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, mockHelper);
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, mockEventBus, mockHelper);
renderFn(mockPanelDiv);

const calls = mockHelper.mock.calls;
Expand All @@ -228,7 +245,7 @@ describe('D3GraphRender', () => {
let data = getData(86400);
data.series[0].length = 0;
const mockHelper = jest.fn(() => []);
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, mockHelper);
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, mockEventBus, mockHelper);
renderFn(mockPanelDiv);

const calls = mockHelper.mock.calls;
Expand All @@ -250,7 +267,7 @@ describe('D3GraphRender', () => {
const mockHelper = jest.fn(() => []);
mockOptions.automaticSampling = false;
mockOptions.sampleType = false;
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, mockHelper);
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, mockEventBus, mockHelper);
renderFn(mockPanelDiv);

const calls = mockHelper.mock.calls;
Expand All @@ -272,7 +289,7 @@ describe('D3GraphRender', () => {
const mockHelper = jest.fn(() => []);
mockOptions.automaticSampling = false;
mockOptions.sampleType = true;
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, mockHelper);
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, mockEventBus, mockHelper);
renderFn(mockPanelDiv);

const calls = mockHelper.mock.calls;
Expand All @@ -293,7 +310,7 @@ describe('D3GraphRender', () => {
data.series = [getValidSerie(width, 1, 86400)];
mockOptions.automaticSampling = false;
mockOptions.sampleType = false;
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, convertAllDataToCubism);
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, mockEventBus, convertAllDataToCubism);
// Create a spy on the function

renderFn(mockPanelDiv);
Expand All @@ -311,7 +328,7 @@ describe('D3GraphRender', () => {
const data = getData(86400);
data.series = [getValidSerie(width, 1, 86400)];
mockOptions.automaticExtents = true;
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, convertAllDataToCubism);
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, mockEventBus, convertAllDataToCubism);
renderFn(mockPanelDiv);

expect(mockPanelDiv.innerHTML).not.toBe('');
Expand All @@ -327,7 +344,7 @@ describe('D3GraphRender', () => {
it('should render graph and text when panelDiv and data are valid', () => {
const data = getData(86400);
data.series = [getValidSerie(width, 1, 86400)];
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, convertAllDataToCubism);
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, mockEventBus, convertAllDataToCubism);
renderFn(mockPanelDiv);

expect(mockPanelDiv.innerHTML).not.toBe('');
Expand All @@ -344,7 +361,7 @@ describe('D3GraphRender', () => {
let time = 14 * 86400;
const data = getData(time);
data.series = [getValidSerie(width, 1, time)];
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, convertAllDataToCubism);
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, mockEventBus, convertAllDataToCubism);
renderFn(mockPanelDiv);

expect(mockPanelDiv.innerHTML).not.toBe('');
Expand All @@ -360,7 +377,7 @@ describe('D3GraphRender', () => {
let time = 14 * 86400 - 1;
const data = getData(time);
data.series = [getValidSerie(width, 1, time)];
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, convertAllDataToCubism);
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, mockEventBus, convertAllDataToCubism);
renderFn(mockPanelDiv);

expect(mockPanelDiv.innerHTML).not.toBe('');
Expand All @@ -376,7 +393,7 @@ describe('D3GraphRender', () => {
let time = 86400 / 2 - 1;
const data = getData(time);
data.series = [getValidSerie(width, 1, time)];
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, convertAllDataToCubism);
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, mockEventBus, convertAllDataToCubism);
renderFn(mockPanelDiv);

expect(mockPanelDiv.innerHTML).not.toBe('');
Expand All @@ -392,7 +409,7 @@ describe('D3GraphRender', () => {
let time = 86400 - 1;
const data = getData(time);
data.series = [getValidSerie(width, 1, time)];
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, convertAllDataToCubism);
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, mockEventBus, convertAllDataToCubism);
renderFn(mockPanelDiv);

expect(mockPanelDiv.innerHTML).not.toBe('');
Expand All @@ -407,7 +424,7 @@ describe('D3GraphRender', () => {
it('should render graph and text when panelDiv and data are valid and called for an hour ', () => {
const data = getData(3500);
data.series = [getValidSerie(width, 1, 3500)];
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, convertAllDataToCubism);
const renderFn = D3GraphRender(mockContext, data, mockOptions, mockStyles, mockEventBus, convertAllDataToCubism);
renderFn(mockPanelDiv);

expect(mockPanelDiv.innerHTML).not.toBe('');
Expand All @@ -421,6 +438,80 @@ describe('D3GraphRender', () => {
});
});

describe('focusCallback', () => {
let context: any;
let mockOptions: any;
let mockStyles: any;
let mockPanelDiv: HTMLDivElement;
let mockEventBus: any;

beforeEach(() => {
context = cubism.context();

mockEventBus = {
publish: jest.fn(() => {}),
};

mockOptions = {
text: 'Hello, World!',
automaticExtents: false,
extentMin: 0,
extentMax: 100,
automaticSampling: true,
sampleType: false,
};
mockStyles = {
'cubism-panel': 'panel',
cubismgraph: 'graph',
canvas: 'canvas',
axis: 'axis',
horizon: 'horizon',
rule: 'rule',
value: 'value',
title: 'title',
textBox: 'text-box',
};

mockPanelDiv = document.createElement('div');
Object.defineProperty(mockPanelDiv, 'clientWidth', {
value: 300,
writable: false, // Ensuring the property remains read-only
});
});
it('should call eventBus when context.focus() is called', () => {
const width = 300;
document.body.innerHTML = '<div id="demo"></div>';

mockPanelDiv = document.createElement('div');
Object.defineProperty(mockPanelDiv, 'clientWidth', {
value: width,
writable: false, // Ensuring the property remains read-only
});

// Mock console functions for testing
context = cubism.context();
let data = getData(3500);
data.series = [getValidSerie(width, 1, 3500)];
mockEventBus.publish();
const renderFn = D3GraphRender(context, data, mockOptions, mockStyles, mockEventBus);

// @ts-ignore
renderFn(mockPanelDiv);

context.focus(12);
expect(mockEventBus.publish).toHaveBeenCalled();
expect(mockEventBus.publish).toHaveBeenCalledWith({
origin: undefined,
payload: {
point: { time: new Date('2020-09-01T00:18:11.283Z') },
},
type: 'data-hover',
});
expect(mockPanelDiv.innerHTML).not.toBe('');
expect(mockPanelDiv.className).toBe(mockStyles['cubism-panel']);
});
});

describe('zoomCallbackGen', () => {
let context: cubism.Context;
let data: PanelData;
Expand Down

0 comments on commit dffcb06

Please sign in to comment.