From 275c51c82813ffecb82bc444bea78cedcdc6ab91 Mon Sep 17 00:00:00 2001 From: Ilfat Galiev Date: Sat, 9 Dec 2023 14:31:17 +0400 Subject: [PATCH] Tooltips support --- charticulator | 2 +- charticulator-visual/capabilities.json | 37 +++++++++++-- charticulator-visual/src/utils/dataParser.ts | 42 +++++++++++++-- .../src/views/Application.tsx | 54 ++++++++++++++++++- .../src/views/ChartViewer.tsx | 11 ++-- 5 files changed, 132 insertions(+), 14 deletions(-) diff --git a/charticulator b/charticulator index 0895b84..f2edbdb 160000 --- a/charticulator +++ b/charticulator @@ -1 +1 @@ -Subproject commit 0895b846d34151da2c84bc8344ecec766c892941 +Subproject commit f2edbdbba2a41bc0562bf0fc011517049fa7eae9 diff --git a/charticulator-visual/capabilities.json b/charticulator-visual/capabilities.json index b6ce36c..a2f4b8f 100644 --- a/charticulator-visual/capabilities.json +++ b/charticulator-visual/capabilities.json @@ -11,6 +11,12 @@ "displayNameKey": "Charticulator_Links", "name": "links", "kind": "GroupingOrMeasure" + }, + { + "displayName": "Tooltips", + "displayNameKey": "Charticulator_Tooltips", + "name": "powerBITooltips", + "kind": "GroupingOrMeasure" } ], "privileges": [ @@ -171,15 +177,36 @@ "categorical": { "categories": { "select": [ - { "bind": { "to": "primarykey" } }, - { "bind": { "to": "links" } } + { + "bind": { + "to": "primarykey" + } + }, + { + "bind": { + "to": "links" + } + }, + { + "bind": { + "to": "powerBITooltips" + } + } ], "dataReductionAlgorithm": { "top": { "count": 30000 } } }, "values": { "select": [ - { "bind": { "to": "primarykey" } }, - { "bind": { "to": "links" } } + { + "bind": { + "to": "primarykey" + } + }, + { + "bind": { + "to": "links" + } + } ] } } @@ -196,7 +223,7 @@ "canvas": true, "default": true }, - "roles": ["data"], + "roles": ["tooltips"], "supportEnhancedTooltips": true } } diff --git a/charticulator-visual/src/utils/dataParser.ts b/charticulator-visual/src/utils/dataParser.ts index 367e623..a0d542c 100644 --- a/charticulator-visual/src/utils/dataParser.ts +++ b/charticulator-visual/src/utils/dataParser.ts @@ -11,6 +11,7 @@ import { Dataset } from "charticulator/src/container"; //2014-12-31T20:00:00.000Z const timeFormat = '%Y-%m-%dT%H:%M:%S.000Z' +export const tooltipsTablename = "powerBITooltips" export function mapColumnType(pbiType: ValueTypeDescriptor): Dataset.DataType { if (pbiType.bool) { @@ -79,6 +80,14 @@ export function convertData( type: Dataset.TableType.Main } + const tooltipsTable: Dataset.Table = { + displayName: "Tooltips", + name: tooltipsTablename, + columns: [], + rows: [], + type: Dataset.TableType.Main + } + const mainTableSelectionIds = new Map(); const linksTable: Dataset.Table = { @@ -89,13 +98,14 @@ export function convertData( type: Dataset.TableType.Links } + if (categories?.length || values?.length) { const allColumns = [...(categories ?? []), ...(values ?? [])]; allColumns.forEach(category => { const source = category.source; const displayName = source.displayName; - if (source.roles['primarykey']) { + if (source.roles['primarykey'] && !mainTable.columns.find(c => c.displayName === displayName)) { mainTable.columns.push({ displayName, name: displayName, @@ -106,7 +116,18 @@ export function convertData( }); } - if (source.roles['links']) { + if (source.roles['powerBITooltips'] && !tooltipsTable.columns.find(c => c.displayName === displayName)) { + tooltipsTable.columns.push({ + displayName, + name: displayName, + type: mapColumnType(source.type), + metadata: { + kind: mapColumnKind(source.type), + } + }); + } + + if (source.roles['links'] && !linksTable.columns.find(c => c.displayName === displayName)) { linksTable.columns.push({ displayName, name: displayName, @@ -132,6 +153,20 @@ export function convertData( return row; }); + tooltipsTable.rows = allColumns[0].values.map((_cat, index) => { + const row: Dataset.Row = { + _id: `${index}` + } + + tooltipsTable.columns.forEach(column => { + const categoryColumn = allColumns.find(category => category.source.displayName === column.displayName); + + row[column.displayName] = categoryColumn.values[index] as any; + }) + + return row; + }); + const uniqueIndex = new Set(); mainTable.rows = []; allColumns[0].values.forEach((_cat, index) => { @@ -177,7 +212,8 @@ export function convertData( name: "main", tables: [ mainTable, - linksTable + linksTable, + tooltipsTable ].filter(table => table.columns.length) }, mainTableSelectionIds diff --git a/charticulator-visual/src/views/Application.tsx b/charticulator-visual/src/views/Application.tsx index ef096f0..41d7793 100644 --- a/charticulator-visual/src/views/Application.tsx +++ b/charticulator-visual/src/views/Application.tsx @@ -1,7 +1,7 @@ import React from 'react'; import powerbi from "powerbi-visuals-api"; -import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions; +import VisualTooltipDataItem = powerbi.extensibility.VisualTooltipDataItem; import { Editor } from './Editor'; import { Mapping, UnmappedColumnName } from './Mapping'; @@ -21,6 +21,7 @@ import { importTempalte } from '../utils/importTemplate'; import { FluentProvider, Label, Tooltip, makeStyles, shorthands, teamsLightTheme } from "@fluentui/react-components"; import switchVisual from "./../../assets/label_tip.png" +import { tooltipsTablename } from '../utils/dataParser'; const useStyles = makeStyles({ tooltipWidthClass: { @@ -139,7 +140,54 @@ export const Application: React.FC = () => { modifiers.event.preventDefault(); } return true; - }, [dataView, selections, selectionManager]); + }, [dataset, selections, selectionManager]); + + const onMouseEnter = React.useCallback((table: string, rowIndices?: number[], modifiers?: IModifiers) => { + host.tooltipService.hide({ + immediately: true, + isTouchEvent: false + }); + if (!rowIndices) { + return; + } + + const identities = rowIndices + .map(index => selections.get(index)) + .filter(s => s) + + const tooltipsTable = dataset.tables.find(t => t.name === tooltipsTablename); + + if (!tooltipsTable) { + return; + } + + const dataRows = rowIndices + .map(index => tooltipsTable.rows[index]) + .filter(s => s) + + const dataItems = dataRows.flatMap(row => { + return tooltipsTable.columns.map(column => { + return { + displayName: column.displayName, + value : `${row[column.displayName]}` + } + }) + }) + + host.tooltipService.show({ + coordinates: [modifiers.clientX, modifiers.clientY], + dataItems, + identities, + isTouchEvent: false + }) + }, [dataset]); + + const onMouseLeave = React.useCallback((table: string, rowIndices: number[], modifiers?: IModifiers) => { + host.tooltipService.hide({ + immediately: true, + isTouchEvent: false + }); + }, [dataset]); const onBackgroundContextMenu = React.useCallback((e: React.MouseEvent) => { onContextMenu(null, [], { @@ -270,6 +318,8 @@ export const Application: React.FC = () => { dataset={dataset} onSelect={onSelect} onContextMenu={onContextMenu} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} localization={localizaiton} utcTimeZone={settings.localization.utcTimeZone} /> diff --git a/charticulator-visual/src/views/ChartViewer.tsx b/charticulator-visual/src/views/ChartViewer.tsx index f473dae..4f8209c 100644 --- a/charticulator-visual/src/views/ChartViewer.tsx +++ b/charticulator-visual/src/views/ChartViewer.tsx @@ -20,10 +20,11 @@ export interface ViewerProps { localization?: LocalizationConfig, utcTimeZone: boolean, onSelect?: (table: string, rowIndices: number[], modifiers?: IModifiers) => Promise; - onContextMenu?: (table: string, rowIndices: number[], modifiers: IModifiers) => void; + onContextMenu?: (table: string, rowIndices: number[], modifiers?: IModifiers) => void; + onMouseEnter?: (table: string, rowIndices: number[], modifiers?: IModifiers) => void; + onMouseLeave?: (table: string, rowIndices: number[], modifiers?: IModifiers) => void; } - export const ChartViewer: React.FC = ({ width, height, @@ -33,7 +34,9 @@ export const ChartViewer: React.FC = ({ localization, utcTimeZone, onSelect, - onContextMenu + onContextMenu, + onMouseEnter, + onMouseLeave }) => { // console.log('ChartViewer'); @@ -55,6 +58,8 @@ export const ChartViewer: React.FC = ({ }); }); container.addContextMenuListener(onContextMenu); + container.addMouseEnterListener(onMouseEnter); + container.addMouseLeaveListener(onMouseLeave); return container.reactMount(width, height); } \ No newline at end of file