diff --git a/.config/jest-setup.js b/.config/jest-setup.js
index 1b9fc2f..08c3168 100644
--- a/.config/jest-setup.js
+++ b/.config/jest-setup.js
@@ -22,4 +22,4 @@ Object.defineProperty(global, 'matchMedia', {
})),
});
-HTMLCanvasElement.prototype.getContext = () => {};
+//HTMLCanvasElement.prototype.getContext = () => {};
diff --git a/jest-setup.js b/jest-setup.js
index 35a700b..90c39c0 100644
--- a/jest-setup.js
+++ b/jest-setup.js
@@ -1,2 +1,6 @@
// Jest setup provided by Grafana scaffolding
import './.config/jest-setup';
+
+SVGElement.prototype.getComputedTextLength = function () {
+ return 10;
+};
diff --git a/jest.config.js b/jest.config.js
index ed0a0ab..75fe5d5 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -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': {
diff --git a/package-lock.json b/package-lock.json
index 19ddc2f..8b0aed5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "cubism-grafana-panel",
- "version": "0.0.6",
+ "version": "0.0.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cubism-grafana-panel",
- "version": "0.0.6",
+ "version": "0.0.7",
"license": "Apache-2.0",
"dependencies": {
"@emotion/css": "^11.10.6",
@@ -45,6 +45,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",
diff --git a/package.json b/package.json
index d491d63..0e096eb 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/CubismPanel.tsx b/src/components/CubismPanel.tsx
index 6f62310..fd36dbb 100644
--- a/src/components/CubismPanel.tsx
+++ b/src/components/CubismPanel.tsx
@@ -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';
@@ -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 !== '') {
@@ -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
;
};
-export const CubismPanel: React.FC = ({ options, data, width, height }) => {
+export const CubismPanel: React.FC = ({ options, data, width, height, eventBus }) => {
return (
-
+
);
};
diff --git a/src/components/CubismPanelHelper.ts b/src/components/CubismPanelHelper.ts
index 25d4a37..2917d25 100644
--- a/src/components/CubismPanelHelper.ts
+++ b/src/components/CubismPanelHelper.ts
@@ -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';
@@ -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) => {
@@ -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);
}
diff --git a/src/fixes/cubism-es.d.ts b/src/fixes/cubism-es.d.ts
index 7a1d06d..4c22586 100644
--- a/src/fixes/cubism-es.d.ts
+++ b/src/fixes/cubism-es.d.ts
@@ -23,6 +23,7 @@ declare module 'cubism-es' {
zoom(f?: zoomCallback): ZoomContext;
setCSSClass(string, string);
getCSSClass(string): string;
+ focus(number);
_scale: d3.scale;
}
diff --git a/src/tests/CubismPanelHelper.test.ts b/src/tests/CubismPanelHelper.test.ts
index c4bf793..e05fdec 100644
--- a/src/tests/CubismPanelHelper.test.ts
+++ b/src/tests/CubismPanelHelper.test.ts
@@ -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();
});
@@ -188,7 +192,6 @@ describe('D3GraphRender', () => {
});
// Mock console functions for testing
- //global.console.log = jest.fn();
});
afterEach(() => {
@@ -196,19 +199,33 @@ describe('D3GraphRender', () => {
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;
@@ -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;
@@ -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;
@@ -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;
@@ -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);
@@ -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('');
@@ -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('');
@@ -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('');
@@ -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('');
@@ -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('');
@@ -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('');
@@ -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('');
@@ -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 = '';
+
+ 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;