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;