├── constants.ts ├── .gitignore ├── .eslintignore ├── src ├── index.ts ├── tooltipInterfaces.ts └── tooltipService.ts ├── .eslintrc.js ├── tsconfig.json ├── .github └── workflows │ ├── build.yml │ ├── release.yml │ └── codeql-analysis.yml ├── LICENSE ├── README.md ├── CHANGELOG.md ├── package.json ├── webpack.config.js ├── CONTRIBUTING.md ├── SECURITY.md ├── karma.conf.ts └── test └── tooltipServiceTests.ts /constants.ts: -------------------------------------------------------------------------------- 1 | export const DefaultHandleTouchDelay: number = 500; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | /typings 4 | /coverage 5 | /lib 6 | lib/*.map 7 | .tmp -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | test 5 | .eslintrc.js 6 | karma.conf.ts 7 | webpack.config.js 8 | test.webpack.config.js 9 | lib 10 | .tmp -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ITooltipServiceWrapper, TooltipEventArgs, TooltipEnabledDataPoint } from "./tooltipInterfaces"; 2 | import { createTooltipServiceWrapper, TooltipServiceWrapper } from "./tooltipService"; 3 | 4 | export { 5 | ITooltipServiceWrapper, TooltipEventArgs, TooltipEnabledDataPoint, 6 | createTooltipServiceWrapper, TooltipServiceWrapper 7 | }; -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | "browser": true, 4 | "es6": true, 5 | "es2017": true 6 | }, 7 | root: true, 8 | parser: "@typescript-eslint/parser", 9 | parserOptions: { 10 | project: "tsconfig.json", 11 | tsconfigRootDir: ".", 12 | }, 13 | plugins: [ 14 | "powerbi-visuals" 15 | ], 16 | extends: [ 17 | "plugin:powerbi-visuals/recommended" 18 | ], 19 | rules: {} 20 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "declaration": true, 7 | "sourceMap": true, 8 | "module": "ES2015", 9 | "moduleResolution": "node", 10 | "target": "ES2015", 11 | "lib": [ 12 | "es2015", 13 | "dom" 14 | ], 15 | "outDir": "./lib" 16 | }, 17 | "files": [ 18 | "src/index.ts" 19 | ], 20 | "include": [ 21 | "src/index.ts" 22 | ] 23 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: build 5 | 6 | on: 7 | push: 8 | branches: [ main, dev ] 9 | pull_request: 10 | branches: [ main, dev ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18.x, 20.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm audit 28 | continue-on-error: true 29 | - run: npm ci 30 | - run: npm outdated 31 | continue-on-error: true 32 | - run: npm run lint 33 | - run: npm test 34 | env: 35 | CI: true 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | environment: automated-release 10 | runs-on: ubuntu-latest 11 | env: 12 | GH_TOKEN: ${{secrets.GH_TOKEN}} 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Add config details 16 | run: | 17 | git config --global user.name ${{secrets.NAME}} 18 | git config --global user.email ${{secrets.EMAIL}} 19 | - name: Move release to draft 20 | run: gh release edit $TAG_NAME --draft=true 21 | env: 22 | TAG_NAME: ${{ github.event.release.tag_name }} 23 | - name: Run npm install, build and test 24 | run: | 25 | npm i 26 | npm run build 27 | npm run test 28 | npm run lint 29 | - run: zip -r release.zip lib *.md LICENSE package-lock.json package.json -x 'lib/test/*' 30 | - name: Upload production artifacts 31 | run: | 32 | gh release upload $TAG_NAME "release.zip#release" 33 | gh release edit $TAG_NAME --draft=false 34 | env: 35 | TAG_NAME: ${{ github.event.release.tag_name }} -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [main, dev, certification] 6 | pull_request: 7 | branches: [main, dev, certification] 8 | schedule: 9 | - cron: '0 0 * * 3' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 60 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: ['typescript'] 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 2 31 | 32 | - name: Use Node.js 18 33 | uses: actions/setup-node@v2 34 | with: 35 | node-version: 18.x 36 | 37 | - name: Install Dependencies 38 | run: npm ci 39 | 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v3 42 | with: 43 | languages: ${{ matrix.language }} 44 | 45 | - name: Autobuild 46 | uses: github/codeql-action/autobuild@v3 47 | 48 | - name: Perform CodeQL Analysis 49 | uses: github/codeql-action/analyze@v3 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Power BI Visualizations 2 | 3 | Copyright (c) Microsoft Corporation 4 | 5 | All rights reserved. 6 | 7 | MIT License 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsoft Power BI visuals TooltipUtils 2 | ![Build](https://github.com/microsoft/powerbi-visuals-utils-tooltiputils/workflows/build/badge.svg) [![npm version](https://img.shields.io/npm/v/powerbi-visuals-utils-tooltiputils.svg)](https://www.npmjs.com/package/powerbi-visuals-utils-tooltiputils) [![npm](https://img.shields.io/npm/dm/powerbi-visuals-utils-tooltiputils.svg)](https://www.npmjs.com/package/powerbi-visuals-utils-tooltiputils) 3 | 4 | > TooltipUtils is a set of functions and classes in order to simplify usage of the Tooltip API for Power BI custom visuals 5 | 6 | ## Usage 7 | Learn how to install and use the TooltipUtils in your custom visuals: 8 | * [Installation Guide](./docs/usage/installation-guide.md) 9 | * [Usage Guide](./docs/usage/usage-guide.md) 10 | 11 | ## Contributing 12 | * Read our [contribution guideline](./CONTRIBUTING.md) to find out how to contribute bugs fixes and improvements 13 | * [Issue Tracker](https://github.com/Microsoft/powerbi-visuals-utils-tooltiputils/issues) 14 | * [Development workflow](./docs/dev/development-workflow.md) 15 | * [How to build](./docs/dev/development-workflow.md#how-to-build) 16 | * [How to run unit tests locally](./docs/dev/development-workflow.md#how-to-run-unit-tests-locally) 17 | 18 | ## License 19 | See the [LICENSE](./LICENSE) file for license rights and limitations (MIT). 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 6.0.4 2 | * powerbi-visuals-api update to 5.9.0 3 | 4 | ## 6.0.3 5 | * Update powerbi-visuals-utils-testutils to 6.0.3 6 | 7 | ## 6.0.2 8 | * Vulnerabilities patched 9 | * Packages update 10 | * Update build.yml to use node 18, 20 11 | 12 | ## 6.0.1 13 | * Packages update 14 | * Removed coveralls 15 | 16 | ## 6.0.0 17 | * Packages update 18 | * Vulnerabilities patched 19 | 20 | ## 3.0.0 21 | * Now we use pointer events instead of mouse and touch events; 22 | * Fixed web and mobile tooltip defenition logic; 23 | * Fixed mobile tooltip "glitch" issue (fast 'opening - closing - opening' of tooltip on mobile devices); 24 | * Fixed mobile tooltipe coordinates calculation; 25 | * Migrated to ESlint; 26 | * Replaced `istanbul-instrumenter-loader` by `coverage-istanbul-loader`; 27 | * Fixed vulnerabilities and updated libs; 28 | * Removed unused libs; 29 | 30 | ### **⚠ IMPORTANT CHANGES** 31 | * `rootElement` argument in `createTooltipServiceWrapper` has been deprecated, it is now optional and can be removed completely in the future; 32 | 33 | ## 2.5.2 34 | * Fixed touchstart/touchend events for iOS devices; 35 | ## 2.5.1 36 | * addToolips fix; 37 | 38 | ## 2.5.0 39 | * D3 update / adaptive for v5(or less) and v6 d3 in visuals 40 | * Handle contextMenu on mobile devices 41 | 42 | ## 2.4.0 43 | * Packages update 44 | * No-jquery tests 45 | 46 | ## 2.3.1 47 | * Packages update 48 | * No-jquery tests 49 | 50 | ## 2.3.0 51 | * Tooltiputils doesn't close tooltip on touch end event. 52 | 53 | ## 2.2.0 54 | * Update packages to fix vulnerabilities 55 | * Update powerbi-visuals-api to 2.6.0 56 | * Update powerbi-visuals-utils-testutils to 2.2.0 57 | 58 | ## 2.1.3 59 | * Update packages to fix vulnerabilities 60 | 61 | ## 2.0.3 62 | * Allow to provide custom getEvent method to tooltip service 63 | 64 | ## 2.0.2 65 | * Convert tooltiputils to es2015 modules 66 | 67 | ## 1.0.0 68 | * Removed `typings` 69 | * Unified dependencies versions 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "powerbi-visuals-utils-tooltiputils", 3 | "version": "6.0.4", 4 | "description": "TooltipUtils", 5 | "main": "lib/src/index.js", 6 | "module": "lib/src/index.js", 7 | "jsnext:main": "lib/src/index.js", 8 | "types": "lib/src/index.d.ts", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Microsoft/powerbi-visuals-utils-tooltiputils.git" 12 | }, 13 | "keywords": [ 14 | "powerbi-visuals-utils" 15 | ], 16 | "author": "Microsoft", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/Microsoft/powerbi-visuals-utils-tooltiputils/issues" 20 | }, 21 | "homepage": "https://github.com/Microsoft/powerbi-visuals-utils-tooltiputils#readme", 22 | "files": [ 23 | "lib" 24 | ], 25 | "scripts": { 26 | "build:map": "tsc --sourceMap", 27 | "build": "tsc", 28 | "test": "karma start", 29 | "pretest": "npm run build:map", 30 | "lint": "npx eslint . --ext .js,.jsx,.ts,.tsx" 31 | }, 32 | "devDependencies": { 33 | "@types/d3-selection": "^3.0.10", 34 | "@types/jasmine": "^5.1.4", 35 | "@types/karma": "^6.3.8", 36 | "@types/node": "^20.12.4", 37 | "@types/webpack": "^5.28.5", 38 | "@typescript-eslint/eslint-plugin": "^5.62.0", 39 | "@typescript-eslint/parser": "^5.62.0", 40 | "coverage-istanbul-loader": "^3.0.5", 41 | "eslint": "^8.57.0", 42 | "eslint-plugin-powerbi-visuals": "^0.8.1", 43 | "jasmine": "^5.1.0", 44 | "karma": "^6.4.3", 45 | "karma-chrome-launcher": "3.2.0", 46 | "karma-coverage": "^2.2.1", 47 | "karma-coverage-istanbul-reporter": "3.0.3", 48 | "karma-jasmine": "5.1.0", 49 | "karma-sourcemap-loader": "0.4.0", 50 | "karma-typescript": "^5.5.4", 51 | "karma-typescript-preprocessor": "0.4.0", 52 | "karma-webpack": "^5.0.1", 53 | "playwright-chromium": "^1.43.0", 54 | "powerbi-visuals-api": "^5.9.0", 55 | "powerbi-visuals-utils-testutils": "^6.1.1", 56 | "ts-loader": "^9.5.1", 57 | "ts-node": "^10.9.2", 58 | "typescript": "^4.9.5", 59 | "webpack": "^5.91.0" 60 | }, 61 | "dependencies": { 62 | "d3-selection": "^3.0.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | const path = require('path'); 28 | 29 | module.exports = { 30 | entry: './src/index.ts', 31 | devtool: 'source-map', 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.(ts)x?$/, 36 | use: 'ts-loader', 37 | exclude: /node_modules/ 38 | }, 39 | { 40 | test: /\.(ts)x?$/i, 41 | enforce: 'post', 42 | include: /(src)/, 43 | exclude: /(node_modules|resources\/js\/vendor)/, 44 | loader: 'coverage-istanbul-loader', 45 | options: { esModules: true } 46 | }, 47 | { 48 | test: /\.json$/, 49 | loader: 'json-loader' 50 | } 51 | ] 52 | }, 53 | externals: { 54 | "powerbi-visuals-api": '{}' 55 | }, 56 | resolve: { 57 | extensions: ['.tsx', '.ts', '.js','.css'] 58 | }, 59 | output: { 60 | path: path.resolve(__dirname, ".tmp") 61 | } 62 | }; -------------------------------------------------------------------------------- /src/tooltipInterfaces.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | import powerbi from "powerbi-visuals-api"; 27 | import { Selection } from 'd3-selection'; 28 | import ITooltipService = powerbi.extensibility.ITooltipService; 29 | 30 | export interface TooltipEventArgs { 31 | data: TData; 32 | coordinates: number[]; 33 | elementCoordinates: number[]; 34 | context: HTMLElement; 35 | isTouchEvent: boolean; 36 | } 37 | 38 | export interface ITooltipServiceWrapper { 39 | addTooltip( 40 | selection: Selection, 41 | getTooltipInfoDelegate: (datapoint: T) => powerbi.extensibility.VisualTooltipDataItem[], 42 | getDataPointIdentity?: (datapoint: T) => powerbi.extensibility.ISelectionId, 43 | reloadTooltipDataOnMouseMove?: boolean): void; 44 | hide(): void; 45 | cancelTouchTimeoutEvents(): void; 46 | } 47 | 48 | export interface TooltipEnabledDataPoint { 49 | tooltipInfo?: powerbi.extensibility.VisualTooltipDataItem[]; 50 | } 51 | 52 | 53 | export interface TooltipServiceWrapperOptions { 54 | tooltipService: ITooltipService; 55 | rootElement?: Element; 56 | handleTouchDelay?: number; 57 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | If you would like to contribute to the Power BI visuals TooltipUtils there are many ways you can help. 3 | 4 | ## Reporting issues 5 | We use [GitHub issues](https://github.com/Microsoft/powerbi-visuals-utils-tooltiputils/issues) as an issue tracker for the repository. Firstly, please search in open issues and try to make sure your problem doesn't exist. If there is an issue, add your comments to this issue. 6 | If there are no issues yet, please open a new one. 7 | 8 | ## Contributing Code 9 | If you would like to contribute an improvement or a fix please take a look at our [Development Workflow](./docs/dev/development-workflow.md) 10 | 11 | ## Sending a Pull Request 12 | Before submitting a pull request please make sure the following is done: 13 | 14 | 1. Fork [the repository](https://github.com/Microsoft/powerbi-visuals-utils-tooltiputils) 15 | 2. Create a branch from the ```master``` 16 | 3. Ensure that the code style checks are passed ([How to lint the source code](./docs/dev/development-workflow.md#how-to-lint-the-source-code)) 17 | 4. Ensure that the unit tests are passed ([How to run unit tests locally](./docs/dev/development-workflow.md#how-to-run-unit-tests-locally)) 18 | 5. Complete the [CLA](#contributor-license-agreement-cla) 19 | 20 | ### Code of Conduct 21 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 22 | 23 | ### Contributor License Agreement (CLA) 24 | You will need to complete a Contributor License Agreement (CLA). Briefly, this agreement testifies that you are granting us permission to use the submitted change according to the terms of the project's license, and that the work being submitted is under appropriate copyright. 25 | 26 | Please submit a Contributor License Agreement (CLA) before submitting a pull request. You may visit [https://cla.microsoft.com](https://cla.microsoft.com) to sign digitally. Alternatively, download the agreement ([Microsoft Contribution License Agreement.docx](https://www.codeplex.com/Download?ProjectName=typescript&DownloadId=822190)), sign, scan, and email it back to . Be sure to include your github user name along with the agreement. Once we have received the signed CLA, we'll review the request. 27 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /karma.conf.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | "use strict"; 28 | 29 | const webpackConfig = require("./webpack.config.js"); 30 | const tsconfig = require("./tsconfig.json"); 31 | 32 | const testRecursivePath = "test/**/*.ts"; 33 | const srcOriginalRecursivePath = "src/**/*.ts"; 34 | const srcRecursivePath = "lib/**/*.js"; 35 | const coverageFolder = "coverage"; 36 | 37 | process.env.CHROME_BIN = require("playwright-chromium").chromium.executablePath(); 38 | module.exports = (config) => { 39 | config.set({ 40 | browsers: ["ChromeHeadless"], 41 | colors: true, 42 | frameworks: ["jasmine"], 43 | reporters: [ 44 | "progress", 45 | "coverage-istanbul" 46 | ], 47 | coverageIstanbulReporter: { 48 | reports: ["html", "lcovonly", "text-summary"], 49 | combineBrowserReports: true, 50 | fixWebpackSourcePaths: true 51 | }, 52 | singleRun: true, 53 | plugins: [ 54 | "karma-coverage", 55 | "karma-typescript", 56 | "karma-webpack", 57 | "karma-jasmine", 58 | "karma-sourcemap-loader", 59 | "karma-chrome-launcher", 60 | "karma-coverage-istanbul-reporter" 61 | ], 62 | files: [ 63 | srcRecursivePath, 64 | testRecursivePath, 65 | { 66 | pattern: srcOriginalRecursivePath, 67 | included: false, 68 | served: true 69 | } 70 | ], 71 | preprocessors: { 72 | [testRecursivePath]: ["webpack"], 73 | [srcRecursivePath]: ["webpack", "coverage"] 74 | }, 75 | typescriptPreprocessor: { 76 | options: tsconfig.compilerOptions 77 | }, 78 | coverageReporter: { 79 | dir: coverageFolder, 80 | reporters: [ 81 | { type: "html" }, 82 | { type: "lcov" } 83 | ] 84 | }, 85 | remapIstanbulReporter: { 86 | reports: { 87 | lcovonly: coverageFolder + "/lcov.info", 88 | html: coverageFolder, 89 | "text-summary": null 90 | } 91 | }, 92 | mime: { 93 | "text/x-typescript": ["ts", "tsx"] 94 | }, 95 | webpack: webpackConfig, 96 | webpackMiddleware: { 97 | stats: "errors-only" 98 | } 99 | }); 100 | }; 101 | -------------------------------------------------------------------------------- /src/tooltipService.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | import { ITooltipServiceWrapper, TooltipServiceWrapperOptions } from "./tooltipInterfaces"; 27 | import { Selection, selectAll } from "d3-selection"; 28 | import { DefaultHandleTouchDelay } from "../constants" 29 | 30 | // powerbi.visuals 31 | import powerbi from "powerbi-visuals-api"; 32 | import ISelectionId = powerbi.visuals.ISelectionId; 33 | 34 | // powerbi.extensibility 35 | import ITooltipService = powerbi.extensibility.ITooltipService; 36 | import VisualTooltipDataItem = powerbi.extensibility.VisualTooltipDataItem; 37 | import TooltipMoveOptions = powerbi.extensibility.TooltipMoveOptions; 38 | import TooltipShowOptions = powerbi.extensibility.TooltipShowOptions; 39 | 40 | export function createTooltipServiceWrapper( 41 | tooltipService: ITooltipService, 42 | rootElement?: Element, // this argument is deprecated and is optional now, just to maintain visuals with tooltiputils logic written for versions bellow 3.0.0 43 | handleTouchDelay: number = DefaultHandleTouchDelay 44 | ): ITooltipServiceWrapper { 45 | 46 | return new TooltipServiceWrapper({ 47 | tooltipService: tooltipService, 48 | handleTouchDelay: handleTouchDelay, 49 | }); 50 | } 51 | 52 | export class TooltipServiceWrapper implements ITooltipServiceWrapper { 53 | private handleTouchTimeoutId: number; 54 | private visualHostTooltipService: ITooltipService; 55 | private handleTouchDelay: number; 56 | 57 | constructor(options: TooltipServiceWrapperOptions) { 58 | this.visualHostTooltipService = options.tooltipService; 59 | this.handleTouchDelay = options.handleTouchDelay; 60 | } 61 | 62 | public addTooltip( 63 | selection: Selection, 64 | getTooltipInfoDelegate: (datapoint: T) => VisualTooltipDataItem[], 65 | getDataPointIdentity?: (datapoint: T) => ISelectionId, 66 | reloadTooltipDataOnMouseMove?: boolean): void { 67 | 68 | if (!selection || !this.visualHostTooltipService.enabled()) { 69 | return; 70 | } 71 | 72 | const internalSelection = selectAll(selection.nodes()); 73 | 74 | const callTooltip = (func: (options: TooltipMoveOptions | TooltipShowOptions) => void, event: PointerEvent, tooltipInfo: VisualTooltipDataItem[], selectionIds: ISelectionId[]): void => { 75 | const coordinates = [event.clientX, event.clientY]; 76 | func.call(this.visualHostTooltipService, { 77 | coordinates: coordinates, 78 | isTouchEvent: event.pointerType === "touch", 79 | dataItems: tooltipInfo, 80 | identities: selectionIds 81 | }); 82 | }; 83 | 84 | internalSelection.on("pointerover", (event: PointerEvent, data: T) => { 85 | const tooltipInfo = getTooltipInfoDelegate(data); 86 | if (tooltipInfo == null) { 87 | return; 88 | } 89 | const selectionIds: ISelectionId[] = getDataPointIdentity ? [getDataPointIdentity(data)] : []; 90 | 91 | if (event.pointerType === "mouse") { 92 | callTooltip(this.visualHostTooltipService.show, event, tooltipInfo, selectionIds); 93 | } 94 | if (event.pointerType === "touch") { 95 | this.handleTouchTimeoutId = window.setTimeout(() => { 96 | callTooltip(this.visualHostTooltipService.show, event, tooltipInfo, selectionIds); 97 | this.handleTouchTimeoutId = undefined; 98 | }, this.handleTouchDelay); 99 | } 100 | }); 101 | 102 | internalSelection.on("pointerout", (event: PointerEvent) => { 103 | if (event.pointerType === "mouse") { 104 | this.visualHostTooltipService.hide({ 105 | isTouchEvent: false, 106 | immediately: false, 107 | }); 108 | } 109 | if (event.pointerType === "touch") { 110 | this.cancelTouchTimeoutEvents(); 111 | } 112 | }); 113 | 114 | internalSelection.on("pointermove", (event: PointerEvent, data: T) => { 115 | if (event.pointerType === "mouse") { 116 | let tooltipInfo: VisualTooltipDataItem[]; 117 | if (reloadTooltipDataOnMouseMove) { 118 | tooltipInfo = getTooltipInfoDelegate(data); 119 | if (tooltipInfo == null) { 120 | return; 121 | } 122 | } 123 | const selectionIds: ISelectionId[] = getDataPointIdentity ? [getDataPointIdentity(data)] : []; 124 | callTooltip(this.visualHostTooltipService.move, event, tooltipInfo, selectionIds); 125 | } 126 | }); 127 | } 128 | 129 | public cancelTouchTimeoutEvents() { 130 | if (this.handleTouchTimeoutId) { 131 | clearTimeout(this.handleTouchTimeoutId); 132 | } 133 | } 134 | 135 | public hide(): void { 136 | this.visualHostTooltipService.hide({ immediately: true, isTouchEvent: false }); 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /test/tooltipServiceTests.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | import { 28 | testDom, 29 | PointerEventType, 30 | PointerType, 31 | pointerEvent 32 | } from "powerbi-visuals-utils-testutils"; 33 | import { select, Selection } from "d3-selection"; 34 | import powerbi from "powerbi-visuals-api"; 35 | 36 | // powerbi.visuals 37 | import ISelectionId = powerbi.visuals.ISelectionId; 38 | 39 | // powerbi.extensibility 40 | import VisualTooltipDataItem = powerbi.extensibility.VisualTooltipDataItem; 41 | 42 | import { TooltipServiceWrapper } from "./../src/tooltipService"; 43 | 44 | import { DefaultHandleTouchDelay } from "../constants" 45 | 46 | describe("TooltipService", () => { 47 | const handleTouchDelay: number = 10; 48 | 49 | let tooltipService: TooltipServiceWrapper, 50 | hostVisualTooltip: IMockHostTooltipService, 51 | onSpy: jasmine.Spy, 52 | d3Selection: Selection, 53 | tooltipRoot: HTMLElement, 54 | element: HTMLElement; 55 | 56 | beforeEach(() => { 57 | 58 | hostVisualTooltip = jasmine.createSpyObj("tooltipService", [ 59 | "show", 60 | "move", 61 | "hide", 62 | "enabled" 63 | ]); 64 | 65 | hostVisualTooltip.enabled.and.returnValue(true); 66 | 67 | tooltipRoot = testDom("100px", "100px"); 68 | 69 | // avoids having to deal with offset mouse coordinates. 70 | tooltipRoot.style.position = "absolute"; 71 | tooltipRoot.style.top = "0px"; 72 | tooltipRoot.style.left = "0px"; 73 | 74 | element = document.createElement("div"); 75 | tooltipRoot.appendChild(element); 76 | 77 | d3Selection = select(element); 78 | onSpy = spyOn(d3Selection, "on").and.callThrough(); 79 | 80 | tooltipService = new TooltipServiceWrapper({ 81 | tooltipService: hostVisualTooltip, 82 | rootElement: tooltipRoot, 83 | handleTouchDelay: handleTouchDelay 84 | }); 85 | }); 86 | 87 | describe("addTooltip", () => { 88 | describe("events", () => { 89 | let identity: ISelectionId; 90 | let tooltipData: VisualTooltipDataItem[]; 91 | let getTooltipInfoDelegate: jasmine.Spy; 92 | let getDataPointIdentity: jasmine.Spy; 93 | let coordinateX: number = 50; 94 | let coordinateY: number = 50; 95 | 96 | beforeEach(() => { 97 | tooltipData = [{ 98 | displayName: "group", 99 | value: "100", 100 | }]; 101 | 102 | getTooltipInfoDelegate = jasmine.createSpy("getTooltipInfoDelegate", (args) => tooltipData).and.callThrough(); 103 | getDataPointIdentity = jasmine.createSpy("getDataPointIdentity", (args) => identity).and.callThrough(); 104 | 105 | tooltipService.addTooltip( 106 | d3Selection, 107 | getTooltipInfoDelegate, 108 | getDataPointIdentity 109 | ); 110 | 111 | d3Selection.data(["datum"]); 112 | }); 113 | 114 | describe("pointerover: ", () => { 115 | describe("for mouse type device, ", () => { 116 | it("shows tooltip", () => { 117 | pointerEvent.call(element, element, PointerEventType.pointerover, PointerType.mouse, coordinateX, coordinateY); 118 | 119 | let selectionId: ISelectionId = getDataPointIdentity(d3Selection.datum()); 120 | 121 | expect(hostVisualTooltip.show).toHaveBeenCalledWith({ 122 | coordinates: [coordinateX, coordinateY], 123 | isTouchEvent: false, 124 | dataItems: tooltipData, 125 | identities: [selectionId] 126 | }); 127 | }); 128 | 129 | it("calls into visual to get identities and tooltip data", () => { 130 | pointerEvent.call(element, element, PointerEventType.pointerover, PointerType.mouse, coordinateX, coordinateY); 131 | 132 | expect(getTooltipInfoDelegate).toHaveBeenCalledWith(d3Selection.datum()); 133 | expect(getDataPointIdentity).toHaveBeenCalledWith(d3Selection.datum()); 134 | }); 135 | 136 | it("calls into visual even when no data", () => { 137 | d3Selection.data([undefined]); 138 | 139 | pointerEvent.call(element, element, PointerEventType.pointerover, PointerType.mouse, coordinateX, coordinateY); 140 | 141 | expect(getTooltipInfoDelegate).toHaveBeenCalledWith(d3Selection.datum()); 142 | expect(getDataPointIdentity).toHaveBeenCalledWith(d3Selection.datum()); 143 | }); 144 | }) 145 | describe("for touch type device", () => { 146 | it("shows tooltip", (done) => { 147 | pointerEvent.call(element, element, PointerEventType.pointerover, PointerType.touch, coordinateX, coordinateY); 148 | 149 | let selectionId: ISelectionId = getDataPointIdentity(d3Selection.datum()); 150 | 151 | setTimeout(() => { 152 | expect(hostVisualTooltip.show).toHaveBeenCalledWith({ 153 | coordinates: [coordinateX, coordinateY], 154 | isTouchEvent: true, 155 | dataItems: tooltipData, 156 | identities: [selectionId] 157 | }); 158 | done(); 159 | }, DefaultHandleTouchDelay); 160 | }); 161 | 162 | it("calls into visual to get identities and tooltip data", () => { 163 | pointerEvent.call(element, element, PointerEventType.pointerover, PointerType.touch, coordinateX, coordinateY); 164 | 165 | expect(getTooltipInfoDelegate).toHaveBeenCalledWith(d3Selection.datum()); 166 | expect(getDataPointIdentity).toHaveBeenCalledWith(d3Selection.datum()); 167 | }); 168 | 169 | it("calls into visual even when no data", () => { 170 | d3Selection.data([undefined]); 171 | 172 | pointerEvent.call(element, element, PointerEventType.pointerover, PointerType.touch, coordinateX, coordinateY); 173 | 174 | expect(getTooltipInfoDelegate).toHaveBeenCalledWith(d3Selection.datum()); 175 | expect(getDataPointIdentity).toHaveBeenCalledWith(d3Selection.datum()); 176 | }); 177 | }); 178 | }); 179 | 180 | describe("pointermove", () => { 181 | it("moves tooltip", () => { 182 | pointerEvent.call(element, element, PointerEventType.pointermove, PointerType.mouse, coordinateX, coordinateY); 183 | 184 | let selectionId: ISelectionId = getDataPointIdentity(d3Selection.datum()); 185 | 186 | expect(hostVisualTooltip.move).toHaveBeenCalledWith({ 187 | coordinates: [coordinateX, coordinateY], 188 | isTouchEvent: false, 189 | dataItems: undefined, 190 | identities: [selectionId] 191 | }); 192 | }); 193 | 194 | it("calls into visual to get identities", () => { 195 | pointerEvent.call(element, element, PointerEventType.pointermove, PointerType.mouse, coordinateX, coordinateY); 196 | 197 | expect(getDataPointIdentity).toHaveBeenCalledWith(d3Selection.datum()); 198 | }); 199 | 200 | it("calls into visual to get identities even when no data", () => { 201 | d3Selection.data([undefined]); 202 | 203 | pointerEvent.call(element, element, PointerEventType.pointermove, PointerType.mouse, coordinateX, coordinateY); 204 | 205 | expect(getDataPointIdentity).toHaveBeenCalledWith(d3Selection.datum()); 206 | }); 207 | 208 | it("does not reload tooltip data if reloadTooltipDataOnMouseMove is false", () => { 209 | // reloadTooltipDataOnMouseMove is false by default 210 | pointerEvent.call(element, element, PointerEventType.pointermove, PointerType.mouse, coordinateX, coordinateY); 211 | 212 | expect(getTooltipInfoDelegate).not.toHaveBeenCalled(); 213 | }); 214 | 215 | it("reloads tooltip data if reloadTooltipDataOnMouseMove is true", () => { 216 | tooltipService.addTooltip( 217 | d3Selection, 218 | getTooltipInfoDelegate, 219 | getDataPointIdentity, 220 | true /* reloadTooltipDataOnMouseMove */ 221 | ); 222 | 223 | pointerEvent.call(element, element, PointerEventType.pointermove, PointerType.mouse, coordinateX, coordinateY); 224 | 225 | let selectionId: ISelectionId = getDataPointIdentity(d3Selection.datum()); 226 | 227 | expect(getTooltipInfoDelegate).toHaveBeenCalledWith(d3Selection.datum()); 228 | expect(hostVisualTooltip.move).toHaveBeenCalledWith({ 229 | coordinates: [coordinateX, coordinateY], 230 | isTouchEvent: false, 231 | dataItems: tooltipData, 232 | identities: [selectionId] 233 | }); 234 | }); 235 | }); 236 | 237 | describe("pointerout", () => { 238 | it("hides tooltip", () => { 239 | pointerEvent.call(element, element, PointerEventType.pointerout, PointerType.mouse, coordinateX, coordinateY); 240 | 241 | expect(hostVisualTooltip.hide).toHaveBeenCalledWith({ 242 | isTouchEvent: false, 243 | immediately: false, 244 | }); 245 | }); 246 | }); 247 | 248 | 249 | it("mouseover does show tooltip after touchend delay", (done) => { 250 | pointerEvent.call(element, element, PointerEventType.pointerout, PointerType.mouse, coordinateX, coordinateY); 251 | 252 | setTimeout(() => { 253 | pointerEvent.call(element, element, PointerEventType.pointerover, PointerType.mouse, coordinateX, coordinateY); 254 | 255 | expect(hostVisualTooltip.show).toHaveBeenCalled(); 256 | done(); 257 | }, /* slightly more than handleTouchDelay */ 20); 258 | }); 259 | }); 260 | 261 | }); 262 | 263 | describe("hide", () => { 264 | it("calls host tooltip service", () => { 265 | tooltipService.hide(); 266 | 267 | expect(hostVisualTooltip.hide).toHaveBeenCalled(); 268 | }); 269 | }); 270 | 271 | interface IMockHostTooltipService { 272 | show: jasmine.Spy; 273 | move: jasmine.Spy; 274 | hide: jasmine.Spy; 275 | enabled: jasmine.Spy; 276 | } 277 | }); 278 | --------------------------------------------------------------------------------