├── .gitignore ├── .editorconfig ├── src ├── label │ ├── index.ts │ ├── units.ts │ ├── fontProperties.ts │ ├── familyInfo.ts │ ├── labelUtils.ts │ ├── dataLabelPointPositioner.ts │ ├── newDataLabelUtils.ts │ └── dataLabelRectPositioner.ts ├── index.ts ├── legend │ ├── markers.ts │ ├── styles │ │ └── legend.less │ ├── legendPosition.ts │ ├── legendData.ts │ ├── legend.ts │ └── legendInterfaces.ts ├── axis │ ├── axisScale.ts │ ├── axisStyle.ts │ ├── labelLayoutStrategy.ts │ └── axisInterfaces.ts ├── styles │ └── style.less └── dataLabel │ ├── dataLabelStyle.ts │ ├── styles │ └── dataLabel.less │ ├── locationConverter.ts │ ├── dataLabelArrangeGrid.ts │ ├── dataLabelInterfaces.ts │ └── dataLabelUtils.ts ├── eslint.config.mjs ├── tsconfig.json ├── docs ├── usage │ ├── usage-guide.md │ └── installation-guide.md ├── api │ ├── data-label-manager.md │ ├── data-label-utils.md │ ├── legend.md │ └── axis-helper.md └── dev │ └── development-workflow.md ├── .github └── workflows │ ├── build.yml │ ├── release.yml │ └── codeql-analysis.yml ├── LICENSE ├── README.md ├── test ├── helpers │ └── helpers.ts ├── axis │ └── helpers │ │ ├── axisTickLabelBuilder.ts │ │ └── axisPropertiesBuilder.ts └── dataLabel │ ├── dataLabelManagerTest.ts │ └── dataLabelUtilsTest.ts ├── CONTRIBUTING.md ├── webpack.config.js ├── package.json ├── SECURITY.md ├── CHANGELOG.md └── karma.conf.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | lib/*.map 4 | /typings 5 | /coverage 6 | /lib 7 | /.tmp -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /src/label/index.ts: -------------------------------------------------------------------------------- 1 | import * as familyInfo from "./familyInfo"; 2 | import * as fontProperties from "./fontProperties"; 3 | import * as labelLayout from "./labelLayout"; 4 | import * as labelUtils from "./labelUtils"; 5 | import * as newDataLabelUtils from "./newDataLabelUtils"; 6 | import * as units from "./units"; 7 | 8 | export { 9 | familyInfo, 10 | fontProperties, 11 | labelLayout, 12 | labelUtils, 13 | newDataLabelUtils, 14 | units 15 | }; 16 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import powerbiVisualsConfigs from "eslint-plugin-powerbi-visuals"; 2 | import tseslint from 'typescript-eslint'; 3 | 4 | export default [ 5 | { 6 | ignores: ["node_modules/", "dist/", ".vscode/", ".tmp/", "test/", "lib/", "mocks/", "coverage/", "webpack.config.js", "karma.conf.ts"], 7 | }, 8 | ...tseslint.configs.recommended, 9 | powerbiVisualsConfigs.configs.recommended, 10 | { 11 | rules: { 12 | "@typescript-eslint/no-explicit-any": "off" 13 | } 14 | }, 15 | ]; -------------------------------------------------------------------------------- /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 | "src/dataLabel/dataLabelStyle.ts" 20 | ], 21 | "include": [ 22 | "src/index.ts" 23 | ] 24 | } -------------------------------------------------------------------------------- /docs/usage/usage-guide.md: -------------------------------------------------------------------------------- 1 | # Usage Guide 2 | > The Usage Guide describes a public API of the package. You will find a description and a few examples for each public interface of the package. 3 | 4 | This package contains the following classes, interfaces and methods: 5 | 6 | * [Axis Helper](../api/axis-helper.md) - provide all necessary methods to maintain chart axes 7 | * [DataLabelManager](../api/data-label-manager.md) - helps to create and maintain labels 8 | * [DataLabelUtils](../api/data-label-utils.md) - label manager utils 9 | * [Legend](../api/legend.md) - helps to create and mantain legend 10 | -------------------------------------------------------------------------------- /.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 ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-22.04 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 build --if-present 33 | - run: npm run lint 34 | - run: npm test 35 | env: 36 | CI: true 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | environment: automated-release 10 | runs-on: ubuntu-22.04 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 SECURITY.md package.json package-lock.json -x 'lib/test/*' 'lib/src/*' 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 ChartUtils 2 | ![Build status](https://github.com/microsoft/powerbi-visuals-utils-chartutils/workflows/build/badge.svg) [![npm version](https://img.shields.io/npm/v/powerbi-visuals-utils-chartutils.svg)](https://www.npmjs.com/package/powerbi-visuals-utils-chartutils) [![npm](https://img.shields.io/npm/dm/powerbi-visuals-utils-chartutils.svg)](https://www.npmjs.com/package/powerbi-visuals-utils-chartutils) 3 | 4 | > ChartUtils is a set of interfaces for creating powerbi custom visuals 5 | 6 | ## Usage 7 | Learn how to install and use the chartutils in your custom visuals: 8 | * [Usage Guide](https://docs.microsoft.com/en-us/power-bi/developer/visuals/utils-chart) 9 | 10 | ## Contributing 11 | * Read our [contribution guideline](./CONTRIBUTING.md) to find out how to contribute bugs fixes and improvements 12 | * [Issue Tracker](https://github.com/Microsoft/powerbi-visuals-utils-chartutils/issues) 13 | * [Development workflow](./docs/dev/development-workflow.md) 14 | * [How to build](./docs/dev/development-workflow.md#how-to-build) 15 | * [How to run unit tests locally](./docs/dev/development-workflow.md#how-to-run-unit-tests-locally) 16 | 17 | ## License 18 | See the [LICENSE](./LICENSE) file for license rights and limitations (MIT). 19 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as axisInterfaces from "./axis/axisInterfaces"; 2 | import * as axis from "./axis/axis"; 3 | import * as axisScale from "./axis/axisScale"; 4 | import * as axisStyle from "./axis/axisStyle"; 5 | 6 | import DataLabelArrangeGrid from "./dataLabel/dataLabelArrangeGrid"; 7 | import * as dataLabelInterfaces from "./dataLabel/dataLabelInterfaces"; 8 | import DataLabelManager from "./dataLabel/dataLabelManager"; 9 | import * as dataLabelUtils from "./dataLabel/dataLabelUtils"; 10 | import * as locationConverter from "./dataLabel/locationConverter"; 11 | 12 | import * as legend from "./legend/legend"; 13 | import * as legendData from "./legend/legendData"; 14 | import * as legendInterfaces from "./legend/legendInterfaces"; 15 | import * as legendPosition from "./legend/legendPosition"; 16 | import * as svgLegend from "./legend/svgLegend"; 17 | 18 | import * as label from "./label/index"; 19 | 20 | export { 21 | axisInterfaces, 22 | axis, 23 | axisScale, 24 | axisStyle, 25 | DataLabelArrangeGrid, 26 | dataLabelInterfaces, 27 | DataLabelManager, 28 | dataLabelUtils, 29 | locationConverter, 30 | label, 31 | legend, 32 | legendData, 33 | legendInterfaces, 34 | legendPosition, 35 | svgLegend 36 | }; 37 | -------------------------------------------------------------------------------- /src/legend/markers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MarkerShape 3 | } from "./legendInterfaces"; 4 | 5 | export const defaultSize = 5; 6 | 7 | export const LegendIconLineTotalWidth: number = 31; 8 | 9 | const circlePath = "M 0 0 m -5 0 a 5 5 0 1 0 10 0 a 5 5 0 1 0 -10 0"; 10 | const squarePath = "M 0 0 m -5 -5 l 10 0 l 0 10 l -10 0 z"; 11 | const longDashPath = "M -" + (LegendIconLineTotalWidth / 2) + " 0 L " + (LegendIconLineTotalWidth / 2) + " 0"; 12 | 13 | const shapeStroke = 0; 14 | const thickStroke = 2; 15 | 16 | export function getPath(shape: string): string { 17 | switch (shape) { 18 | case MarkerShape.circle: { 19 | return circlePath; 20 | } 21 | case MarkerShape.square: { 22 | return squarePath; 23 | } 24 | case MarkerShape.longDash: { 25 | return longDashPath; 26 | } 27 | default: { 28 | return undefined; 29 | } 30 | } 31 | } 32 | 33 | export function getStrokeWidth(shape: string): number { 34 | switch (shape) { 35 | case MarkerShape.longDash: { 36 | return thickStroke; 37 | } 38 | case MarkerShape.circle: 39 | case MarkerShape.square: 40 | default: { 41 | return shapeStroke; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/axis/axisScale.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 | export const linear: string = "linear"; 28 | export const log: string = "log"; 29 | -------------------------------------------------------------------------------- /src/styles/style.less: -------------------------------------------------------------------------------- 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 "../legend/styles/legend.less"; 28 | @import "../dataLabel/styles/dataLabel.less"; 29 | -------------------------------------------------------------------------------- /src/dataLabel/dataLabelStyle.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 | export const category: string = "Category"; 28 | export const data: string = "Data"; 29 | export const both: string = "Both"; -------------------------------------------------------------------------------- /src/axis/axisStyle.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 | export const showBoth: string = "showBoth"; 28 | export const showTitleOnly: string = "showTitleOnly"; 29 | export const showUnitOnly: string = "showUnitOnly"; 30 | -------------------------------------------------------------------------------- /docs/api/data-label-manager.md: -------------------------------------------------------------------------------- 1 | # DataLabelManager 2 | > The ```DataLabelManager``` helps to create and maintain labels. It arranges label elements using the anchor point or rectangle. Collisions between elements can be automatically detected and as a result elements can be repositioned or get hidden. 3 | 4 | The ```powerbi.extensibility.utils.chart.dataLabel.DataLabelManager``` class provides the following methods: 5 | 6 | * [hideCollidedLabels](#hidecollidedlabels) 7 | * [IsValid](#isvalid) 8 | 9 | ## hideCollidedLabels 10 | 11 | This method arranges the lables position and visibility on the canvas according to labels sizes and overlapping. 12 | 13 | ```typescript 14 | function hideCollidedLabels( 15 | viewport: IViewport, 16 | data: any[], 17 | layout: any, 18 | addTransform: boolean = false 19 | ): LabelEnabledDataPoint[]; 20 | ``` 21 | 22 | ### Example 23 | 24 | ```typescript 25 | let dataLabelManager = new DataLabelManager(); 26 | let filteredData = dataLabelManager.hideCollidedLabels(this.viewport, values, labelLayout, true); 27 | ``` 28 | 29 | ## IsValid 30 | 31 | This static method checks if provided rectangle is valid(has positive width and height). 32 | 33 | ```typescript 34 | function isValid(rect: IRect): boolean; 35 | ``` 36 | 37 | ### Example 38 | 39 | ```typescript 40 | let rectangle = { 41 | left: 150, 42 | top: 130, 43 | width: 120, 44 | height: 110 45 | }; 46 | 47 | DataLabelManager.isValid(rectangle); 48 | 49 | // returns: true 50 | ``` 51 | -------------------------------------------------------------------------------- /src/dataLabel/styles/dataLabel.less: -------------------------------------------------------------------------------- 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 | .data-labels { 28 | font-size: 12px; 29 | text-anchor: middle; 30 | font-family: helvetica, arial, sans-serif; 31 | pointer-events: none; 32 | } 33 | -------------------------------------------------------------------------------- /src/legend/styles/legend.less: -------------------------------------------------------------------------------- 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 | @neutralPrimaryColor: #333333; 28 | @whiteColor: #FFF; 29 | 30 | .legend { 31 | .navArrow { 32 | fill: @neutralPrimaryColor; 33 | cursor: pointer; 34 | /* stroke enabled for a bigger click target, but smaller visible arrow size */ 35 | stroke: @whiteColor; 36 | stroke-width: 2px; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/legend/legendPosition.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 | export const top: string = "Top"; 28 | export const bottom: string = "Bottom"; 29 | export const left: string = "Left"; 30 | export const right: string = "Right"; 31 | export const topCenter: string = "TopCenter"; 32 | export const bottomCenter: string = "BottomCenter"; 33 | export const leftCenter: string = "LeftCenter"; 34 | export const rightCenter: string = "RightCenter"; 35 | -------------------------------------------------------------------------------- /src/label/units.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 { pixelConverter } from "powerbi-visuals-utils-typeutils"; 28 | 29 | export class FontSize { 30 | public static createFromPt(pt: number): FontSize { 31 | return new FontSize(pt, pixelConverter.fromPointToPixel(pt)); 32 | } 33 | 34 | public static createFromPx(px: number): FontSize { 35 | return new FontSize(pixelConverter.toPoint(px), px); 36 | } 37 | 38 | private constructor(public readonly pt: number, public readonly px: number) { } 39 | } 40 | -------------------------------------------------------------------------------- /test/helpers/helpers.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 { parseColorString , RgbColor} from "powerbi-visuals-utils-colorutils"; 28 | 29 | export function assertColorsMatch(actual: string, expected: string, invert: boolean = false): boolean { 30 | const rgbActual: RgbColor = parseColorString(actual), 31 | rgbExpected: RgbColor = parseColorString(expected); 32 | try { 33 | if (invert) { 34 | expect(rgbActual).not.toEqual(rgbExpected); 35 | } else { 36 | expect(rgbActual).toEqual(rgbExpected); 37 | } 38 | return true 39 | } catch (error) { 40 | return false 41 | } 42 | } 43 | 44 | /** 45 | * Checks if value is in the given range 46 | * @val Value to check 47 | * @min Min value of range 48 | * @max Max value of range 49 | * @returns True, if value falls in range. False, otherwise 50 | **/ 51 | export function isInRange(val: number, min: number, max: number): Boolean { 52 | return min <= val && val <= max; 53 | } 54 | 55 | export function findElementTitle(element: HTMLElement): string { 56 | return element.querySelector("title")?.textContent ?? ""; 57 | } 58 | 59 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | If you would like to contribute to the Power BI visuals ChartUtils there are many ways you can help. 3 | 4 | ## Reporting issues 5 | We use [GitHub issues](https://github.com/Microsoft/powerbi-visuals-utils-chartutils/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-chartutils) 15 | 2. Create a branch from the ```main``` 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 | -------------------------------------------------------------------------------- /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 | const webpack = require("webpack"); 29 | 30 | module.exports = { 31 | entry: './src/index.ts', 32 | devtool: 'source-map', 33 | module: { 34 | rules: [{ 35 | test: /\.tsx?$/, 36 | use: 'ts-loader', 37 | exclude: /node_modules/ 38 | }, 39 | { 40 | test: /\.tsx?$/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-tools": '{}', 55 | "powerbi-visuals-api": '{}' 56 | }, 57 | resolve: { 58 | extensions: ['.tsx', '.ts', '.js', '.css'] 59 | }, 60 | output: { 61 | path: path.resolve(__dirname, ".tmp") 62 | }, 63 | plugins: [ 64 | new webpack.ProvidePlugin({ 65 | 'powerbi-visuals-api': null 66 | }) 67 | ] 68 | }; 69 | -------------------------------------------------------------------------------- /src/label/fontProperties.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 { FontSize } from "./units"; 28 | 29 | export interface FontProperties { 30 | readonly color?: string; 31 | readonly family?: string; 32 | readonly lineHeight?: string; 33 | readonly size?: FontSize; 34 | readonly style?: string; 35 | readonly variant?: string; 36 | readonly weight?: string; 37 | readonly whiteSpace?: string; 38 | readonly decoration?: string; 39 | } 40 | 41 | export interface MutableFontProperties { 42 | color?: string; 43 | family?: string; 44 | lineHeight?: string; 45 | size?: FontSize; 46 | style?: string; 47 | variant?: string; 48 | weight?: string; 49 | whiteSpace?: string; 50 | } 51 | 52 | /** 53 | * Inherits a `FontProperties` object allowing specific properties to be overriden. 54 | * Typically used for changing values on an existing object as all properties are readonly. 55 | * @param fontProperties The existing `FontProperties` object 56 | * @param newFontProperties The properties to override 57 | * @returns A new object inherited from `fontProperties`. 58 | */ 59 | export function inherit(fontProperties: FontProperties, newFontProperties: FontProperties): FontProperties { 60 | return { 61 | ...fontProperties, 62 | ...newFontProperties 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /docs/api/data-label-utils.md: -------------------------------------------------------------------------------- 1 | # DataLabelUtils 2 | > The ```DataLabelUtils``` provides utils to manipulate data labels. 3 | 4 | The ```powerbi.extensibility.utils.chart.dataLabel.utils``` module provides the following functions, interfaces and classes: 5 | 6 | * [getLabelPrecision](#getlabelprecision) 7 | * [getLabelFormattedText](#getlabelformattedtext) 8 | * [enumerateDataLabels](#enumeratedatalabels) 9 | * [enumerateCategoryLabels](#enumeratecategorylabels) 10 | * [createColumnFormatterCacheManager](#createcolumnformattercachemanager) 11 | 12 | ## getLabelPrecision 13 | This function calculates precision from given format. 14 | 15 | ```typescript 16 | function getLabelPrecision(precision: number, format: string): number 17 | ``` 18 | 19 | ## getLabelFormattedText 20 | 21 | This function returns format precision from given format. 22 | 23 | ```typescript 24 | function getLabelFormattedText(options: LabelFormattedTextOptions): string 25 | ``` 26 | 27 | #### Example 28 | 29 | ```typescript 30 | import dataLabelUtils = powerbi.extensibility.utils.chart.dataLabel.utils; 31 | 32 | let options: LabelFormattedTextOptions = { 33 | text: 'some text', 34 | fontFamily: 'sans', 35 | fontSize: '15', 36 | fontWeight: 'normal', 37 | }; 38 | 39 | dataLabelUtils.getLabelFormattedText(options); 40 | ``` 41 | 42 | ## enumerateDataLabels 43 | 44 | This function returns VisualObjectInstance for data labels. 45 | 46 | ```typescript 47 | function enumerateDataLabels(options: VisualDataLabelsSettingsOptions): VisualObjectInstance 48 | ``` 49 | 50 | ## enumerateCategoryLabels 51 | 52 | This function adds VisualObjectInstance for Category data labels to enumeration object. 53 | 54 | ```typescript 55 | function enumerateCategoryLabels( 56 | enumeration: VisualObjectInstanceEnumerationObject, 57 | dataLabelsSettings: VisualDataLabelsSettings, 58 | withFill: boolean, 59 | isShowCategory: boolean = false, 60 | fontSize?: number): void 61 | ``` 62 | 63 | ## createColumnFormatterCacheManager 64 | 65 | This function returns Cache Manager that provides quick access to formatted labels 66 | 67 | ```typescript 68 | function createColumnFormatterCacheManager(): IColumnFormatterCacheManager 69 | ``` 70 | 71 | #### Example 72 | 73 | ```typescript 74 | import dataLabelUtils = powerbi.extensibility.utils.chart.dataLabel.utils; 75 | 76 | let value: number = 200000; 77 | 78 | labelSettings.displayUnits = 1000000; 79 | labelSettings.precision = 1; 80 | 81 | let formattersCache = DataLabelUtils.createColumnFormatterCacheManager(); 82 | let formatter = formattersCache.getOrCreate(null, labelSettings); 83 | let formattedValue = formatter.format(value); 84 | 85 | // formattedValue == "0.2M" 86 | ``` 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "powerbi-visuals-utils-chartutils", 3 | "version": "8.2.0", 4 | "description": "ChartUtils", 5 | "main": "lib/index.js", 6 | "module": "lib/index.js", 7 | "jsnext:main": "lib/index.js", 8 | "types": "lib/index.d.ts", 9 | "sideEffects": false, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Microsoft/powerbi-visuals-utils-chartutils.git" 13 | }, 14 | "keywords": [ 15 | "powerbi-visuals-utils" 16 | ], 17 | "author": "Microsoft", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/Microsoft/powerbi-visuals-utils-chartutils/issues" 21 | }, 22 | "homepage": "https://github.com/Microsoft/powerbi-visuals-utils-chartutils#readme", 23 | "files": [ 24 | "lib" 25 | ], 26 | "scripts": { 27 | "build": "tsc", 28 | "build:map": "tsc --sourceMap", 29 | "postbuild": "npm run lessc", 30 | "test": "karma start", 31 | "lint": "npx eslint", 32 | "lintfix": "npx eslint --fix", 33 | "lessc": "lessc src/styles/style.less lib/index.css" 34 | }, 35 | "devDependencies": { 36 | "@types/d3-array": "^3.2.2", 37 | "@types/d3-axis": "^3.0.6", 38 | "@types/d3-scale": "^4.0.9", 39 | "@types/d3-selection": "^3.0.11", 40 | "@types/d3-transition": "^3.0.9", 41 | "@types/jasmine": "^5.1.13", 42 | "@typescript-eslint/eslint-plugin": "^8.31.0", 43 | "@typescript-eslint/parser": "^8.31.0", 44 | "coverage-istanbul-loader": "3.0.5", 45 | "eslint": "^9.39.1", 46 | "eslint-plugin-powerbi-visuals": "^1.1.0", 47 | "jasmine": "5.6.0", 48 | "karma": "^6.4.4", 49 | "karma-chrome-launcher": "^3.2.0", 50 | "karma-coverage": "^2.2.1", 51 | "karma-coverage-istanbul-reporter": "^3.0.3", 52 | "karma-jasmine": "^5.1.0", 53 | "karma-sourcemap-loader": "^0.4.0", 54 | "karma-typescript": "^5.5.4", 55 | "karma-webpack": "5.0.1", 56 | "less": "^4.4.2", 57 | "lodash.union": "4.6.0", 58 | "playwright-chromium": "^1.57.0", 59 | "powerbi-visuals-api": "^5.11.0", 60 | "powerbi-visuals-utils-colorutils": "^6.0.5", 61 | "powerbi-visuals-utils-testutils": "^6.1.1", 62 | "ts-loader": "^9.5.4", 63 | "ts-node": "^10.9.2", 64 | "typescript": "^5.9.3", 65 | "webpack": "^5.103.0" 66 | }, 67 | "dependencies": { 68 | "d3-array": "^3.2.4", 69 | "d3-axis": "^3.0.0", 70 | "d3-scale": "^4.0.2", 71 | "d3-selection": "^3.0.0", 72 | "d3-transition": "^3.0.1", 73 | "powerbi-visuals-utils-formattingutils": "^6.1.2", 74 | "powerbi-visuals-utils-svgutils": "^6.0.4", 75 | "powerbi-visuals-utils-typeutils": "^6.0.3", 76 | "typescript-eslint": "^8.48.0" 77 | }, 78 | "optionalDependencies": { 79 | "fsevents": "2.3.3" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/legend/legendData.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 powerbi from "powerbi-visuals-api"; 28 | import { legendProps, LegendData } from "./legendInterfaces"; 29 | import * as position from "./legendPosition"; 30 | 31 | export const DefaultLegendLabelFillColor: string = "#666666"; 32 | 33 | export function update(legendData: LegendData, legendObject: powerbi.DataViewObject): void { 34 | if (legendObject[legendProps.show] == null) { 35 | legendObject[legendProps.show] = true; 36 | } 37 | 38 | if (legendObject[legendProps.show] === false) { 39 | legendData.dataPoints = []; 40 | } 41 | 42 | if (legendObject[legendProps.show] === true && legendObject[legendProps.position] == null) { 43 | legendObject[legendProps.position] = position.top; 44 | } 45 | 46 | if (legendObject[legendProps.fontSize] !== undefined) { 47 | legendData.fontSize = legendObject[legendProps.fontSize]; 48 | } 49 | 50 | if (legendObject[legendProps.labelColor] !== undefined) { 51 | 52 | const fillColor = legendObject[legendProps.labelColor]; 53 | 54 | if (fillColor != null) { 55 | legendData.labelColor = fillColor.solid.color; 56 | } 57 | } 58 | 59 | if (legendObject[legendProps.showTitle] === false) { 60 | legendData.title = ""; 61 | } 62 | else if (legendObject[legendProps.titleText] !== undefined) { 63 | legendData.title = legendObject[legendProps.titleText]; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/legend/legend.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 { Selection } from "d3-selection"; 27 | import { ILegend, LegendPosition } from "./legendInterfaces"; 28 | import { SVGLegend } from "./svgLegend"; 29 | 30 | export function createLegend( 31 | legendParentElement: HTMLElement, 32 | isScrollable: boolean = false, 33 | legendPosition: LegendPosition = LegendPosition.Top 34 | ): ILegend { 35 | 36 | return new SVGLegend(legendParentElement, legendPosition, isScrollable); 37 | } 38 | 39 | export function isLeft(orientation: LegendPosition): boolean { 40 | switch (orientation) { 41 | case LegendPosition.Left: 42 | case LegendPosition.LeftCenter: 43 | return true; 44 | default: 45 | return false; 46 | } 47 | } 48 | 49 | export function isTop(orientation: LegendPosition): boolean { 50 | switch (orientation) { 51 | case LegendPosition.Top: 52 | case LegendPosition.TopCenter: 53 | return true; 54 | default: 55 | return false; 56 | } 57 | } 58 | 59 | export function positionChartArea( 60 | chartArea: Selection, 61 | legend: ILegend 62 | ): void { 63 | const legendMargins = legend.getMargins(), 64 | legendOrientation = legend.getOrientation(); 65 | 66 | chartArea.style( 67 | "margin-left", isLeft(legendOrientation) 68 | ? legendMargins.width + "px" 69 | : null 70 | ); 71 | chartArea.style( 72 | "margin-top", isTop(legendOrientation) 73 | ? legendMargins.height + "px" 74 | : null, 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/dev/development-workflow.md: -------------------------------------------------------------------------------- 1 | # Development workflow 2 | If you would like to contribute an improvement or a fix please pay attention to these items: 3 | * [Requirements](#requirements) 4 | * [Installation](#installation) 5 | * [How to build](#how-to-build) 6 | * [How to lint the source code](#how-to-lint-the-source-code) 7 | * [How to run unit tests locally](#how-to-run-unit-tests-locally) 8 | 9 | ## Requirements 10 | To start development and improvement of the source code you should have the following things: 11 | * [git](https://git-scm.com) 12 | * [node.js](https://nodejs.org) (we recommend the latest LTS version) 13 | * [npm](https://www.npmjs.com) (the minimal supported version is 3.0.0) 14 | * [Google Chrome browser](https://www.google.com/chrome) (it's necessary to run unit tests locally) 15 | 16 | ## Installation 17 | Firstly, you should clone a copy of the repository by using one of the following commands: 18 | * HTTPS: ```git clone https://github.com/Microsoft/powerbi-visuals-utils-chartutils.git``` 19 | * SSH: ```git clone git@github.com:Microsoft/powerbi-visuals-utils-chartutils.git``` 20 | 21 | After that, you should change the current working directory to ```powerbi-visuals-utils-chartutils``` by using the following command: 22 | 23 | ```bash 24 | cd powerbi-visuals-utils-chartutils 25 | ``` 26 | 27 | After that, you should install dependencies by using the following command: 28 | 29 | ```bash 30 | npm install 31 | ``` 32 | 33 | The final step is installation of necessary type declarations by using the following command: 34 | 35 | ```bash 36 | npm run typings:install 37 | ``` 38 | 39 | The repository is ready for development now. 40 | 41 | ## How to build 42 | We use [Less](https://github.com/less/less.js) and [TypeScript](https://github.com/Microsoft/TypeScript) for the repository. To build source code you should run the following command: 43 | 44 | ```bash 45 | npm run build 46 | ``` 47 | 48 | This command compiles less code to CSS and TypeScript code to JavaScript. The result of the compilation is available in the ```lib``` directory. 49 | 50 | ## How to lint the source code 51 | We use [eslint](https://github.com/eslint/eslint) as a linter for TypeScript code. To check source code you should run the following command: 52 | 53 | ```bash 54 | npm run lint 55 | ``` 56 | 57 | This command checks style of TypeScript code and provides a list of problems. Please address all of problems reported by eslint before sending a pull request to the [repository](https://github.com/Microsoft/powerbi-visuals-utils-chartutils). 58 | 59 | ## How to run unit tests locally 60 | We use [Jasmine](https://github.com/jasmine/jasmine) and [Karma](https://github.com/karma-runner/karma) to run unit tests. Please note, Karma requires Google Chrome to run unit tests. 61 | To run unit tests locally on your machine you should run the following command: 62 | 63 | ```bash 64 | npm run test 65 | ``` 66 | 67 | ## How to debug unit tests locally 68 | To debug unit tests in Google Chrome browser you should run the following command: 69 | 70 | ```bash 71 | npm run test -- --single-run=false 72 | ``` 73 | 74 | This command runs unit tests in the browser and watches tests files, in other words, you have an ability to run unit tests automatically after any changing. 75 | -------------------------------------------------------------------------------- /docs/api/legend.md: -------------------------------------------------------------------------------- 1 | # Legend service 2 | The ```Legend``` service provides helper interfaces for creating and managing PBI legends for Custom visuals 3 | 4 | The ```powerbi.visuals``` module provides the following functions and interfaces: 5 | 6 | * [createLegend](#createLegend) 7 | * [ILegend](#ILegend) 8 | * [drawLegend](#drawLegend) 9 | 10 | ## createLegend 11 | This helper function simplifies PowerBI Custom Visual legends creation. 12 | 13 | ```typescript 14 | function createLegend(legendParentElement: HTMLElement, // top visual element, container in which legend will be created 15 | isScrollable: boolean = false, // indicates that legend could be scrollable or not 16 | legendPosition: LegendPosition = LegendPosition.Top // Position of the legend inside of legendParentElement container 17 | ): ILegend; 18 | ``` 19 | ### Example 20 | 21 | ```typescript 22 | public init(options: VisualConstructorOptions) { 23 | this.visualInitOptions = options; 24 | this.layers = []; 25 | 26 | var element = this.element = options.element; 27 | var viewport = this.currentViewport = options.viewport; 28 | var hostServices = options.host; 29 | 30 | //... some other init calls 31 | 32 | this.legend = createLegend( 33 | element, 34 | true); 35 | } 36 | ``` 37 | 38 | ## ILegend 39 | This Interface implements all methods necessary for legend creation 40 | 41 | ```typescript 42 | export interface ILegend { 43 | getMargins(): IViewport; 44 | isVisible(): boolean; 45 | changeOrientation(orientation: LegendPosition): void; // processing legend orientation 46 | getOrientation(): LegendPosition; // get information about current legend orientation 47 | drawLegend(data: LegendData, viewport: IViewport); // all legend rendering code is placing here 48 | /** 49 | * Reset the legend by clearing it 50 | */ 51 | reset(): void;} 52 | ``` 53 | 54 | ### drawLegend 55 | 56 | This function measures the height of the text with the given SVG text properties. 57 | 58 | ```typescript 59 | function drawLegend(data: LegendData, viewport: IViewport): void; 60 | ``` 61 | 62 | ### Example 63 | 64 | ```typescript 65 | private renderLegend(): void { 66 | if (!this.isInteractive) { 67 | let legendObjectProperties = this.data.legendObjectProperties; 68 | if (legendObjectProperties) { 69 | let legendData = this.data.legendData; 70 | LegendData.update(legendData, legendObjectProperties); 71 | let position = legendObjectProperties[legendProps.position]; 72 | if (position) 73 | this.legend.changeOrientation(LegendPosition[position]); 74 | 75 | this.legend.drawLegend(legendData, this.parentViewport); 76 | } else { 77 | this.legend.changeOrientation(LegendPosition.Top); 78 | this.legend.drawLegend({ dataPoints: [] }, this.parentViewport); 79 | } 80 | } 81 | } 82 | ``` 83 | 84 | -------------------------------------------------------------------------------- /src/label/familyInfo.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 | export const FamilyDelimiter = ", "; 28 | 29 | export class FamilyInfo { 30 | constructor(public families: string[]) { } 31 | 32 | /** 33 | * Gets the font-families joined by FamilyDelimiter. 34 | */ 35 | get family(): string { 36 | return this.getFamily(); 37 | } 38 | 39 | /** 40 | * Gets the font-families joined by FamilyDelimiter. 41 | */ 42 | getFamily(): string { 43 | return this.families.join(FamilyDelimiter); 44 | } 45 | 46 | /** 47 | * Gets the CSS string for the "font-family" CSS attribute. 48 | */ 49 | get css(): string { 50 | return this.getCSS(); 51 | } 52 | 53 | /** 54 | * Gets the CSS string for the "font-family" CSS attribute. 55 | */ 56 | getCSS(): string { 57 | return this.families ? this.families.map((font => font.indexOf(" ") > -1 ? `'${font}'` : font)).join(", ") : null; 58 | } 59 | } 60 | 61 | // These should map to the fonts in src\clients\externals\StyleLibrary\less\fontFaces.less 62 | export const Family = { 63 | light: new FamilyInfo(["Segoe UI Light", "wf_segoe-ui_light", "helvetica", "arial", "sans-serif"]), 64 | semilight: new FamilyInfo(["Segoe UI Semilight", "wf_segoe-ui_semilight", "helvetica", "arial", "sans-serif"]), 65 | regular: new FamilyInfo(["Segoe UI", "wf_segoe-ui_normal", "helvetica", "arial", "sans-serif"]), 66 | semibold: new FamilyInfo(["Segoe UI Semibold", "wf_segoe-ui_semibold", "helvetica", "arial", "sans-serif"]), 67 | bold: new FamilyInfo(["Segoe UI Bold", "wf_segoe-ui_bold", "helvetica", "arial", "sans-serif"]), 68 | lightSecondary: new FamilyInfo(["wf_standard-font_light", "helvetica", "arial", "sans-serif"]), 69 | regularSecondary: new FamilyInfo(["wf_standard-font", "helvetica", "arial", "sans-serif"]), 70 | boldSecondary: new FamilyInfo(["wf_standard-font_bold", "helvetica", "arial", "sans-serif"]), 71 | glyphs: new FamilyInfo(["PowerVisuals"]), 72 | }; 73 | -------------------------------------------------------------------------------- /src/legend/legendInterfaces.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 powerbi from "powerbi-visuals-api"; 28 | import { Point } from "powerbi-visuals-utils-svgutils"; 29 | 30 | import ISelectionId = powerbi.visuals.ISelectionId; 31 | 32 | export enum LegendPosition { 33 | Top, 34 | Bottom, 35 | Right, 36 | Left, 37 | None, 38 | TopCenter, 39 | BottomCenter, 40 | RightCenter, 41 | LeftCenter, 42 | } 43 | 44 | export interface ISelectableDataPoint{ 45 | selected: boolean, 46 | identity: ISelectionId; 47 | } 48 | 49 | export interface LegendPosition2D { 50 | textPosition?: Point; 51 | glyphPosition?: Point; 52 | } 53 | 54 | export enum MarkerShape { 55 | circle = "circle", 56 | square = "square", 57 | longDash = "longDash", 58 | } 59 | 60 | export enum LineStyle { 61 | dashed = "dashed", 62 | solid = "solid", 63 | dotted = "dotted", 64 | dotdash = "dotdash", 65 | dashdot = "dashdot", 66 | } 67 | 68 | export interface LegendDataPoint extends LegendPosition2D, ISelectableDataPoint { 69 | label: string; 70 | color: string; 71 | category?: string; 72 | measure?: any; 73 | iconOnlyOnLabel?: boolean; 74 | tooltip?: string; 75 | layerNumber?: number; 76 | lineStyle?: LineStyle; 77 | markerShape?: MarkerShape; 78 | } 79 | 80 | export interface LegendData { 81 | title?: string; 82 | dataPoints: LegendDataPoint[]; 83 | grouped?: boolean; 84 | labelColor?: string; 85 | fontSize?: number; 86 | fontFamily?: string; 87 | fontWeight?: string; 88 | fontStyle?: string; 89 | textDecoration?: string; 90 | } 91 | 92 | export const legendProps = { 93 | show: "show", 94 | position: "position", 95 | titleText: "titleText", 96 | showTitle: "showTitle", 97 | labelColor: "labelColor", 98 | fontSize: "fontSize", 99 | fontWeight: "fontWeight", 100 | fontStyle: "fontStyle", 101 | textDecoration: "textDecoration", 102 | }; 103 | 104 | export interface ILegend { 105 | getMargins(): powerbi.IViewport; 106 | 107 | isVisible(): boolean; 108 | changeOrientation(orientation: LegendPosition): void; 109 | getOrientation(): LegendPosition; 110 | drawLegend(data: LegendData, viewport: powerbi.IViewport); 111 | /** 112 | * Reset the legend by clearing it 113 | */ 114 | reset(): void; 115 | } 116 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ## 8.1.0 4 | 5 | * Added `fontWeight`, `fontStyle`, `textDecoration` to `LegendData` interface 6 | * Added `fontWeight`, `fontStyle`, `textDecoration` to `LabelOld` interface 7 | * Updated packages 8 | 9 | ## 8.0.0 10 | 11 | ### Module `axis` 12 | * `getTickLabelMargins` has changed props interface to `GetTickLabelMarginsOptions` and now returns `IMargin` instead of `TickLabelMargins` 13 | * `TickLabelMargins` interface has been removed 14 | * `getBestNumberOfTicks` has changed props interface to `GetBestNumberOfTicksOptions` 15 | * `createFormatter` has changed props interface to `CreateFormatterOptions` 16 | 17 | ### Module `dataLabel` 18 | * `drawDefaultLabelsForDataPointChart` has changed props interface to `DrawDefaultLabelsForDataPointChartOptions` 19 | 20 | ### Other 21 | * All code was refactored 22 | * Packages update 23 | 24 | ## 7.0.0 25 | * Removed interactivityutils and related code 26 | * Removed interactiveLegend class 27 | * Changed createLegend function signature -> createLegend(HTMLElement, boolean, LegendPosition) 28 | 29 | ## 6.0.4 30 | * Updated powerbi-visuals-api to 5.9.0 and other utils 31 | 32 | ## 6.0.3 33 | * Fixed legend title bug 34 | 35 | ## 6.0.2 36 | * Fixed vulnerabilities 37 | * Packages update 38 | 39 | ## 6.0.1 40 | * Packages update 41 | * Removed coveralls 42 | 43 | ## 6.0.0 44 | * Packages update 45 | * Vulnerabilities fixes 46 | 47 | ## 3.0.0 48 | * Updated powerbi-visuals-utils 49 | * Fixed vulnerabilities 50 | * Migrated to Eslint 51 | * Migrated to playwright 52 | 53 | ## 2.6.0 54 | * Removed Jquery 55 | * D3.v6 code refactored 56 | * Packages update 57 | * Added new tests 58 | 59 | ## 2.5.0 60 | * Packages update 61 | * Github actions 62 | 63 | ## 2.4.3 64 | * FIX: navigation arrows not displayed on first visual render 65 | 66 | ## 2.4.2 67 | * Export as default `DataLabelArrangeGrid`, `DataLabelManager` classes 68 | * Update packages 69 | 70 | ## 2.4.1 71 | * Packages update 72 | * Removal of LabelLayoutStrategy module definition, now it is imported from a file 73 | 74 | ## 2.4.0 75 | * Update interactivity utils to 5.5.0 76 | * Update powerbi-visual-api to 2.6 77 | * Update packages to fix vulnerabilities 78 | 79 | ## 2.3.1 80 | * Fixes measurement of legend items to fit available viewport width 81 | * Supports `fontFamily` for legend component 82 | 83 | ## 2.3.0 84 | * Update interactivity utils to 5.4.0 85 | 86 | ## 2.2.1 87 | * FIX: d3 v5 wrong usage in Label Utils 88 | 89 | ## 2.2.0 90 | * Implements legend marker shapes 91 | * New Label Utils 92 | 93 | ## 2.1.0 94 | * Update packages to fix vulnerabilities 95 | 96 | ## 2.0.6 97 | * Added OpacityLegendBehavior for legend 98 | 99 | ## 1.5.1 100 | * FIX: Was removed a wrong instruction from auto generated code that impacted on tests in visuals 101 | 102 | ## 1.5.0 103 | * Added two new optional parameters for CreateScale function -- innerPadding and useRangePoint. The first lets set inner padding for scale instead of receive it from constant. The second lets use rangePoint instead of rangeBands function for creation of ordinal scale. 104 | 105 | ## 1.4.0 106 | * Remove width restriction of title in legend 107 | * Added new option to drawDefaultLabelsForDataPointChart function to control behavior of collided labels 108 | 109 | ## 1.3.0 110 | * Updated packages 111 | 112 | ## 1.2.0 113 | * Added 'disableNiceOnlyForScale' to 'CreateAxisOptions' interface 114 | and added verification with this property to createAxis func 115 | 116 | ## 1.1.0 117 | * Removed `lodash` 118 | * Updated dependencies 119 | 120 | ## 1.0.1 121 | * Update ChartUtils to use SVG utils version 1.0.0 122 | * Add CHANGELOG 123 | -------------------------------------------------------------------------------- /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 | , srcOriginalRecursivePath = "src/**/*.ts" 34 | , srcRecursivePath = "lib/**/*.js" 35 | , srcCssRecursivePath = "lib/**/*.css" 36 | , coverageFolder = "coverage"; 37 | 38 | process.env.CHROME_BIN = require("playwright-chromium").chromium.executablePath(); 39 | module.exports = (config) => { 40 | config.set({ 41 | browserNoActivityTimeout: 100000, 42 | browsers: ["ChromeHeadless"], 43 | colors: true, 44 | frameworks: ["jasmine"], 45 | reporters: [ 46 | "progress", 47 | "coverage-istanbul" 48 | ], 49 | coverageIstanbulReporter: { 50 | reports: ["html", "lcovonly", "text-summary"], 51 | combineBrowserReports: true, 52 | fixWebpackSourcePaths: true 53 | }, 54 | singleRun: true, 55 | plugins: [ 56 | "karma-coverage", 57 | "karma-typescript", 58 | "karma-webpack", 59 | "karma-jasmine", 60 | "karma-sourcemap-loader", 61 | "karma-chrome-launcher", 62 | "karma-coverage-istanbul-reporter" 63 | ], 64 | files: [ 65 | srcCssRecursivePath, 66 | srcRecursivePath, 67 | testRecursivePath, 68 | { 69 | pattern: srcOriginalRecursivePath, 70 | included: false, 71 | served: true 72 | } 73 | ], 74 | preprocessors: { 75 | [testRecursivePath]: ["webpack"], 76 | [srcRecursivePath]: ["webpack", "coverage"] 77 | }, 78 | typescriptPreprocessor: { 79 | options: tsconfig.compilerOptions 80 | }, 81 | coverageReporter: { 82 | dir: coverageFolder, 83 | reporters: [ 84 | { type: "html" }, 85 | { type: "lcov" } 86 | ] 87 | }, 88 | remapIstanbulReporter: { 89 | reports: { 90 | lcovonly: coverageFolder + "/lcov.info", 91 | html: coverageFolder, 92 | "text-summary": null 93 | } 94 | }, 95 | mime: { 96 | "text/x-typescript": ["ts", "tsx"] 97 | }, 98 | webpack: webpackConfig, 99 | webpackMiddleware: { 100 | stats: "errors-only" 101 | } 102 | }); 103 | }; 104 | -------------------------------------------------------------------------------- /docs/usage/installation-guide.md: -------------------------------------------------------------------------------- 1 | # How to install 2 | If you would like to install the Power BI visuals ChartUtils to your custom visual please pay attention to these items: 3 | * [Requirements](#requirements) 4 | * [Installation](#installation) 5 | * [Including declarations to the build flow](#including-declarations-to-the-build-flow) 6 | * [Including JavaScript artifacts to the custom visual](#including-javascript-artifacts-to-the-custom-visual) 7 | * [Including CSS artifacts to the custom visual](#including-css-artifacts-to-the-custom-visual) 8 | 9 | ## Requirements 10 | To use the package you should have the following things: 11 | * [node.js](https://nodejs.org) (we recommend the latest LTS version) 12 | * [npm](https://www.npmjs.com/) (the minimal supported version is 3.0.0) 13 | * The custom visual created by [PowerBI-visuals-tools](https://github.com/Microsoft/PowerBI-visuals-tools) 14 | 15 | ## Installation 16 | To install the package you should run the following command in the directory with your current custom visual: 17 | 18 | ```bash 19 | npm install powerbi-visuals-utils-chartutils --save 20 | ``` 21 | 22 | This command installs the package and adds a package as a dependency to your ```package.json``` 23 | 24 | ## Including declarations to the build flow 25 | The package contains ```d.ts``` declarations file, it's necessary for TypeScript compiler and it helps to develop your visuals fast and confident. You should add the following files to the ```files``` property of ```tsconfig.json```: 26 | * ```typings/index.d.ts``` 27 | * ```node_modules/powerbi-visuals-utils-formattingutils/lib/index.d.ts``` 28 | * ```node_modules/powerbi-visuals-utils-svgutils/lib/index.d.ts``` 29 | * ```node_modules/powerbi-visuals-utils-typeutils/lib/index.d.ts``` 30 | * ```node_modules/powerbi-visuals-utils-chartutils/lib/index.d.ts``` 31 | 32 | As a result you will have the following file structure: 33 | ```json 34 | { 35 | "compilerOptions": {...}, 36 | "files": [ 37 | "typings/index.d.ts", 38 | "node_modules/powerbi-visuals-utils-formattingutils/lib/index.d.ts", 39 | "node_modules/powerbi-visuals-utils-svgutils/lib/index.d.ts", 40 | "node_modules/powerbi-visuals-utils-typeutils/lib/index.d.ts", 41 | "node_modules/powerbi-visuals-utils-chartutils/lib/index.d.ts" 42 | ] 43 | } 44 | ``` 45 | 46 | ## Including JavaScript artifacts to the custom visual 47 | To use the package with your custom visuals you should add the following files to the ```externalJS``` property of ```pbiviz.json``` : 48 | * ```node_modules/d3/d3.min.js``` 49 | * ```node_modules/globalize/lib/globalize.js``` 50 | * ```node_modules/globalize/lib/cultures/globalize.culture.en-US.js``` 51 | * ```node_modules/powerbi-visuals-utils-typeutils/lib/index.js``` 52 | * ```node_modules/powerbi-visuals-utils-svgutils/lib/index.js``` 53 | * ```node_modules/powerbi-visuals-utils-formattingutils/lib/index.js``` 54 | * ```node_modules/powerbi-visuals-utils-chartutils/lib/index.js``` 55 | 56 | As a result you will have the following file structure: 57 | ```json 58 | { 59 | "visual": {...}, 60 | "apiVersion": ..., 61 | "author": {...}, 62 | "assets": {...}, 63 | "externalJS": [ 64 | "node_modules/d3/d3.min.js", 65 | "node_modules/globalize/lib/globalize.js", 66 | "node_modules/globalize/lib/cultures/globalize.culture.en-US.js", 67 | "node_modules/powerbi-visuals-utils-typeutils/lib/index.js", 68 | "node_modules/powerbi-visuals-utils-svgutils/lib/index.js", 69 | "node_modules/powerbi-visuals-utils-formattingutils/lib/index.js", 70 | "node_modules/powerbi-visuals-utils-chartutils/lib/index.js" 71 | ], 72 | "style": ..., 73 | "capabilities": ... 74 | } 75 | ``` 76 | 77 | ## Including CSS artifacts to the custom visual 78 | To use the package with your custom visuals you should import the following CSS files to your ```.less``` file: 79 | 80 | * ```node_modules/powerbi-visuals-utils-chartutils/lib/index.css``` 81 | 82 | As a result you will have the following file structure: 83 | ```less 84 | @import (less) "node_modules/powerbi-visuals-utils-chartutils/lib/index.css"; 85 | ``` 86 | 87 | Please note, you should import ```.css``` files as ```.less``` files, because Power BI Visuals Tools wraps the external CSS rules. 88 | 89 | That's it. :rocket: :metal: That's a good time to read our [Usage Guide](./usage-guide.md). 90 | -------------------------------------------------------------------------------- /src/dataLabel/locationConverter.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 { shapesInterfaces, IRect } from "powerbi-visuals-utils-svgutils"; 28 | import ISize = shapesInterfaces.ISize; 29 | 30 | export function topInside(size: ISize, rect: IRect, offset: number): IRect { 31 | return { 32 | left: rect.left + rect.width / 2.0 - size.width / 2.0, 33 | top: rect.top + offset, 34 | width: size.width, 35 | height: size.height 36 | }; 37 | } 38 | 39 | export function bottomInside(size: ISize, rect: IRect, offset: number): IRect { 40 | return { 41 | left: rect.left + rect.width / 2.0 - size.width / 2.0, 42 | top: (rect.top + rect.height) - size.height - offset, 43 | width: size.width, 44 | height: size.height 45 | }; 46 | } 47 | 48 | export function rightInside(size: ISize, rect: IRect, offset: number): IRect { 49 | return { 50 | left: (rect.left + rect.width) - size.width - offset, 51 | top: rect.top + rect.height / 2.0 - size.height / 2.0, 52 | width: size.width, 53 | height: size.height 54 | }; 55 | } 56 | 57 | export function leftInside(size: ISize, rect: IRect, offset: number): IRect { 58 | return { 59 | left: rect.left + offset, 60 | top: rect.top + rect.height / 2.0 - size.height / 2.0, 61 | width: size.width, 62 | height: size.height 63 | }; 64 | } 65 | 66 | export function topOutside(size: ISize, rect: IRect, offset: number): IRect { 67 | return { 68 | left: rect.left + rect.width / 2.0 - size.width / 2.0, 69 | top: rect.top - size.height - offset, 70 | width: size.width, 71 | height: size.height 72 | }; 73 | } 74 | 75 | export function bottomOutside(size: ISize, rect: IRect, offset: number): IRect { 76 | return { 77 | left: rect.left + rect.width / 2.0 - size.width / 2.0, 78 | top: (rect.top + rect.height) + offset, 79 | width: size.width, 80 | height: size.height 81 | }; 82 | } 83 | 84 | export function rightOutside(size: ISize, rect: IRect, offset: number): IRect { 85 | return { 86 | left: (rect.left + rect.width) + offset, 87 | top: rect.top + rect.height / 2.0 - size.height / 2.0, 88 | width: size.width, 89 | height: size.height 90 | }; 91 | } 92 | 93 | export function leftOutside(size: ISize, rect: IRect, offset: number): IRect { 94 | return { 95 | left: rect.left - size.width - offset, 96 | top: rect.top + rect.height / 2.0 - size.height / 2.0, 97 | width: size.width, 98 | height: size.height 99 | }; 100 | } 101 | 102 | export function middleHorizontal(size: ISize, rect: IRect, offset: number): IRect { 103 | return { 104 | left: rect.left + rect.width / 2.0 - size.width / 2.0 + offset, 105 | top: rect.top + rect.height / 2.0 - size.height / 2.0, 106 | width: size.width, 107 | height: size.height 108 | }; 109 | } 110 | 111 | export function middleVertical(size: ISize, rect: IRect, offset: number): IRect { 112 | return { 113 | left: rect.left + rect.width / 2.0 - size.width / 2.0, 114 | top: rect.top + rect.height / 2.0 - size.height / 2.0 + offset, 115 | width: size.width, 116 | height: size.height 117 | }; 118 | } 119 | -------------------------------------------------------------------------------- /test/axis/helpers/axisTickLabelBuilder.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 | // powerbi.extensibility.utils.formatting 28 | import powerbi from "powerbi-visuals-api"; 29 | import IViewport = powerbi.IViewport; 30 | 31 | import * as axis from "./../../../src/axis/axis"; 32 | import * as axisInterfaces from "./../../../src/axis/axisInterfaces"; 33 | import IAxisProperties = axisInterfaces.IAxisProperties; 34 | 35 | import { textMeasurementService, interfaces } from "powerbi-visuals-utils-formattingutils"; 36 | import TextProperties = interfaces.TextProperties; 37 | import CartesianAxisProperties = axisInterfaces.CartesianAxisProperties; 38 | 39 | export class AxisTickLabelBuilder { 40 | private xAxisProperties: IAxisProperties; 41 | private y1AxisProperties: IAxisProperties; 42 | private y2AxisProperties: IAxisProperties; 43 | private axes: CartesianAxisProperties; 44 | 45 | private viewPort: IViewport = { 46 | width: 200, 47 | height: 125 48 | }; 49 | 50 | private textProperties: TextProperties = { 51 | fontFamily: "", 52 | fontSize: "16px" 53 | }; 54 | 55 | constructor(viewport?: IViewport, xValues?: any[]) { 56 | this.xAxisProperties = this.buildAxisOptions(xValues || ["Oregon", "Washington", "California", "Mississippi"]); 57 | this.y1AxisProperties = this.buildAxisOptions([20, 30, 50]); 58 | this.y2AxisProperties = this.buildAxisOptions([2000, 3000, 5000]); 59 | 60 | this.axes = { 61 | x: this.xAxisProperties, 62 | y1: this.y1AxisProperties, 63 | y2: this.y2AxisProperties, 64 | }; 65 | 66 | if (viewport) { 67 | this.viewPort = viewport; 68 | } 69 | } 70 | 71 | public getFontSize(): number { 72 | return parseInt(this.textProperties.fontSize, 10); 73 | } 74 | 75 | public buildAxisOptions(values: any[]): IAxisProperties { 76 | return { 77 | scale: undefined, 78 | values: values, 79 | axisLabel: "", 80 | isCategoryAxis: true, 81 | xLabelMaxWidth: 20, 82 | outerPadding: 10, 83 | categoryThickness: 25, 84 | } as IAxisProperties; 85 | } 86 | 87 | public buildTickLabelMargins( 88 | rotateX?: boolean, 89 | wordBreak?: boolean, 90 | showOnRight?: boolean, 91 | renderXAxis?: boolean, 92 | renderYAxes?: boolean, 93 | renderY2Axis?: boolean, 94 | categoryThickness?: number, 95 | outerPadding?: number, 96 | isScalar?: boolean 97 | ) { 98 | 99 | this.xAxisProperties.willLabelsFit = !rotateX; 100 | this.xAxisProperties.willLabelsWordBreak = wordBreak; 101 | let dataDomain = [0, 10]; 102 | this.xAxisProperties.dataDomain = dataDomain; 103 | this.xAxisProperties.scale = isScalar 104 | ? axis.createLinearScale(this.viewPort.width, dataDomain) 105 | : axis.createOrdinalScale(this.viewPort.width, dataDomain); 106 | 107 | if (categoryThickness != null) { 108 | this.xAxisProperties.categoryThickness = categoryThickness; 109 | this.xAxisProperties.xLabelMaxWidth = categoryThickness * 0.9; 110 | this.xAxisProperties.outerPadding = categoryThickness * 0.5; 111 | } 112 | 113 | // scalar line chart sets outer padding to zero since it isn't drawing rectangles 114 | if (outerPadding != null) 115 | this.xAxisProperties.outerPadding = outerPadding; 116 | 117 | let margins = axis.getTickLabelMargins({ 118 | viewport: this.viewPort, 119 | yMarginLimit: this.viewPort.width * 0.3, 120 | textWidthMeasurer: textMeasurementService.measureSvgTextWidth, 121 | textHeightMeasurer: textMeasurementService.estimateSvgTextHeight, 122 | axes: this.axes, 123 | bottomMarginLimit: this.viewPort.height * 0.2, 124 | properties: this.textProperties, 125 | showOnRight, 126 | renderXAxis, 127 | renderY1Axis: renderYAxes, 128 | renderY2Axis 129 | }); 130 | 131 | return margins; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/label/labelUtils.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 { Selection } from "d3-selection"; 28 | 29 | import { CssConstants } from "powerbi-visuals-utils-svgutils"; 30 | 31 | import { FontProperties } from "./fontProperties"; 32 | import { 33 | drawDefaultLabels as newDrawDefaultLabels, 34 | getDataLabelLayoutOptions as newGetDataLabelLayoutOptions, 35 | getNumberOfLabelsToRender as newGetNumberOfLabelsToRender } from "./newDataLabelUtils"; 36 | 37 | import { 38 | DataLabelLayoutOptions, 39 | Label, 40 | LabelOld, 41 | LabelOrientation, 42 | } from "./labelLayout"; 43 | 44 | export interface VisualDataLabelsSettings { 45 | show: boolean; 46 | fontProperties: FontProperties; 47 | showLabelPerSeries?: boolean; 48 | labelOrientation?: LabelOrientation; 49 | isSeriesExpanded?: boolean; 50 | displayUnits?: number; 51 | showCategory?: boolean; 52 | position?: any; 53 | precision?: number; 54 | percentagePrecision?: number; 55 | categoryLabelColor?: string; 56 | labelStyle?: any; 57 | minFontSize?: number; 58 | maxFontSize?: number; 59 | labelOverflow?: boolean; 60 | enableBackground?: boolean; 61 | backgroundColor?: string; 62 | backgroundTransparency?: number; 63 | } 64 | 65 | export interface LabelEnabledDataPoint { 66 | // for collistion detection use 67 | labelX?: number; 68 | labelY?: number; 69 | // for overriding color from label settings 70 | labelFill?: string; 71 | // for display units and precision 72 | labeltext?: string; 73 | // taken from column metadata 74 | labelFormatString?: string; 75 | isLabelInside?: boolean; 76 | labelFontSize?: number; 77 | } 78 | 79 | export const enum CartesianChartType { 80 | Line, 81 | Area, 82 | StackedArea, 83 | ClusteredColumn, 84 | StackedColumn, 85 | ClusteredBar, 86 | StackedBar, 87 | HundredPercentStackedBar, 88 | HundredPercentStackedColumn, 89 | RibbonChart, 90 | Scatter, 91 | ComboChart, 92 | DataDot, 93 | Waterfall, 94 | LineClusteredColumnCombo, 95 | LineStackedColumnCombo, 96 | DataDotClusteredColumnCombo, 97 | DataDotStackedColumnCombo, 98 | RealTimeLineChart, 99 | } 100 | 101 | 102 | export const DefaultFontSizeInPt = 9; 103 | 104 | export const horizontalLabelBackgroundPadding = 4; 105 | export const verticalLabelBackgroundPadding = 2; 106 | 107 | export const labelGraphicsContextClass: CssConstants.ClassAndSelector = CssConstants.createClassAndSelector("labelGraphicsContext"); 108 | export const labelBackgroundGraphicsContextClass: CssConstants.ClassAndSelector = CssConstants.createClassAndSelector("labelBackgroundGraphicsContext"); 109 | 110 | export function downgradeToOldLabels(labels: Label[]): LabelOld[] { 111 | if (!labels) return; 112 | return labels.map((label) => { 113 | const inheritedLabel: Label = { ...label }; 114 | inheritedLabel.fontProperties = null; 115 | const oldLabel: LabelOld = inheritedLabel; 116 | oldLabel.fill = label?.fontProperties?.color; 117 | oldLabel.fontSize = label?.fontProperties?.size?.pt; 118 | oldLabel.fontFamily = label?.fontProperties?.family; 119 | oldLabel.fontWeight = label?.fontProperties?.weight; 120 | oldLabel.fontStyle = label?.fontProperties?.style; 121 | oldLabel.textDecoration = label?.fontProperties?.decoration; 122 | return oldLabel; 123 | }); 124 | } 125 | 126 | export function drawDefaultLabels( 127 | context: Selection, 128 | dataLabels: Label[], 129 | numeric: boolean = false, 130 | hasTooltip: boolean = false 131 | ): Selection { 132 | return newDrawDefaultLabels(context, downgradeToOldLabels(dataLabels), numeric, hasTooltip); 133 | } 134 | 135 | export function getDataLabelLayoutOptions(chartType: CartesianChartType): DataLabelLayoutOptions { 136 | return newGetDataLabelLayoutOptions(chartType); 137 | } 138 | 139 | export function getNumberOfLabelsToRender(viewportWidth: number, labelDensity: number, minimumLabelsToRender: number, estimatedLabelWidth: number): number { 140 | return newGetNumberOfLabelsToRender(viewportWidth, labelDensity, minimumLabelsToRender, estimatedLabelWidth); 141 | } 142 | -------------------------------------------------------------------------------- /src/label/dataLabelPointPositioner.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 | shapesInterfaces, 29 | IRect, 30 | } from "powerbi-visuals-utils-svgutils"; 31 | 32 | import { 33 | NewPointLabelPosition, 34 | LabelParentPoint 35 | } from "./labelLayout"; 36 | 37 | export const cos45 = Math.cos(45); 38 | export const sin45 = Math.sin(45); 39 | 40 | export function getLabelRect(labelSize: shapesInterfaces.ISize, parentPoint: LabelParentPoint, position: NewPointLabelPosition, offset: number): IRect { 41 | const positionToFunctionMap = { 42 | [NewPointLabelPosition.Above]: above, 43 | [NewPointLabelPosition.Below]: below, 44 | [NewPointLabelPosition.Left]: left, 45 | [NewPointLabelPosition.Right]: right, 46 | [NewPointLabelPosition.BelowLeft]: belowLeft, 47 | [NewPointLabelPosition.BelowRight]: belowRight, 48 | [NewPointLabelPosition.AboveLeft]: aboveLeft, 49 | [NewPointLabelPosition.AboveRight]: aboveRight, 50 | [NewPointLabelPosition.Center]: center 51 | }; 52 | 53 | const positionFunction = positionToFunctionMap[position]; 54 | if (!positionFunction) { 55 | return null; 56 | } 57 | 58 | // Update the center position call to include the offset parameter 59 | return positionFunction(labelSize, parentPoint.point, parentPoint.radius + offset); 60 | } 61 | 62 | export function above(labelSize: shapesInterfaces.ISize, parentPoint: shapesInterfaces.IPoint, offset: number): IRect { 63 | return { 64 | left: parentPoint.x - (labelSize.width / 2), 65 | top: parentPoint.y - offset - labelSize.height, 66 | width: labelSize.width, 67 | height: labelSize.height 68 | }; 69 | } 70 | 71 | export function below(labelSize: shapesInterfaces.ISize, parentPoint: shapesInterfaces.IPoint, offset: number): IRect { 72 | return { 73 | left: parentPoint.x - (labelSize.width / 2), 74 | top: parentPoint.y + offset, 75 | width: labelSize.width, 76 | height: labelSize.height 77 | }; 78 | } 79 | 80 | export function left(labelSize: shapesInterfaces.ISize, parentPoint: shapesInterfaces.IPoint, offset: number): IRect { 81 | return { 82 | left: parentPoint.x - offset - labelSize.width, 83 | top: parentPoint.y - (labelSize.height / 2), 84 | width: labelSize.width, 85 | height: labelSize.height 86 | }; 87 | } 88 | 89 | export function right(labelSize: shapesInterfaces.ISize, parentPoint: shapesInterfaces.IPoint, offset: number): IRect { 90 | return { 91 | left: parentPoint.x + offset, 92 | top: parentPoint.y - (labelSize.height / 2), 93 | width: labelSize.width, 94 | height: labelSize.height 95 | }; 96 | } 97 | 98 | export function belowLeft(labelSize: shapesInterfaces.ISize, parentPoint: shapesInterfaces.IPoint, offset: number): IRect { 99 | return { 100 | left: parentPoint.x - (sin45 * offset) - labelSize.width, 101 | top: parentPoint.y + (cos45 * offset), 102 | width: labelSize.width, 103 | height: labelSize.height 104 | }; 105 | } 106 | 107 | export function belowRight(labelSize: shapesInterfaces.ISize, parentPoint: shapesInterfaces.IPoint, offset: number): IRect { 108 | return { 109 | left: parentPoint.x + (sin45 * offset), 110 | top: parentPoint.y + (cos45 * offset), 111 | width: labelSize.width, 112 | height: labelSize.height 113 | }; 114 | } 115 | 116 | export function aboveLeft(labelSize: shapesInterfaces.ISize, parentPoint: shapesInterfaces.IPoint, offset: number): IRect { 117 | return { 118 | left: parentPoint.x - (sin45 * offset) - labelSize.width, 119 | top: parentPoint.y - (cos45 * offset) - labelSize.height, 120 | width: labelSize.width, 121 | height: labelSize.height 122 | }; 123 | } 124 | 125 | export function aboveRight(labelSize: shapesInterfaces.ISize, parentPoint: shapesInterfaces.IPoint, offset: number): IRect { 126 | return { 127 | left: parentPoint.x + (sin45 * offset), 128 | top: parentPoint.y - (cos45 * offset) - labelSize.height, 129 | width: labelSize.width, 130 | height: labelSize.height 131 | }; 132 | } 133 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 134 | export function center(labelSize: shapesInterfaces.ISize, parentPoint: shapesInterfaces.IPoint, _: number): IRect { 135 | return { 136 | left: parentPoint.x - (labelSize.width / 2), 137 | top: parentPoint.y - (labelSize.height / 2), 138 | width: labelSize.width, 139 | height: labelSize.height 140 | }; 141 | } 142 | 143 | export function getLabelLeaderLineEndingPoint(boundingBox: IRect, position: NewPointLabelPosition, parentShape: LabelParentPoint): number[][] { 144 | let x = boundingBox.left; 145 | let y = boundingBox.top; 146 | switch (position) { 147 | case NewPointLabelPosition.Above: 148 | x += (boundingBox.width / 2); 149 | y += boundingBox.height; 150 | break; 151 | case NewPointLabelPosition.Below: 152 | x += (boundingBox.width / 2); 153 | break; 154 | case NewPointLabelPosition.Left: 155 | x += boundingBox.width; 156 | y += ((boundingBox.height * 2) / 3); 157 | break; 158 | case NewPointLabelPosition.Right: 159 | y += ((boundingBox.height * 2) / 3); 160 | break; 161 | case NewPointLabelPosition.BelowLeft: 162 | x += boundingBox.width; 163 | y += (boundingBox.height / 2); 164 | break; 165 | case NewPointLabelPosition.BelowRight: 166 | y += (boundingBox.height / 2); 167 | break; 168 | case NewPointLabelPosition.AboveLeft: 169 | x += boundingBox.width; 170 | y += boundingBox.height; 171 | break; 172 | case NewPointLabelPosition.AboveRight: 173 | y += boundingBox.height; 174 | break; 175 | } 176 | 177 | return [[parentShape.point.x, parentShape.point.y], [x, y]]; 178 | } 179 | 180 | -------------------------------------------------------------------------------- /src/label/newDataLabelUtils.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 { Selection } from "d3-selection"; 28 | import { interrupt } from "d3-transition"; 29 | 30 | import { pixelConverter } from "powerbi-visuals-utils-typeutils"; 31 | import { CssConstants } from "powerbi-visuals-utils-svgutils"; 32 | 33 | import { 34 | LabelOld, 35 | LabelOrientation, 36 | } from "./labelLayout"; 37 | 38 | import { CartesianChartType } from "./labelUtils"; 39 | 40 | export const DefaultLabelFontSizeInPt = 9; 41 | export const startingLabelOffset = 8; 42 | export const maxLabelOffset = 8; 43 | 44 | export const horizontalLabelBackgroundPadding = 4; 45 | export const verticalLabelBackgroundPadding = 2; 46 | 47 | const labelsClass: CssConstants.ClassAndSelector = CssConstants.createClassAndSelector("label"); 48 | 49 | function getLabelX(label: LabelOld) { 50 | const isVertical = label.labelOrientation === LabelOrientation.Vertical; 51 | const orientationOffset = isVertical ? label.boundingBox.width : (label.boundingBox.width / 2); 52 | const backgroundOffset = label.hasBackground && isVertical ? horizontalLabelBackgroundPadding : 0; 53 | return label.boundingBox.left + orientationOffset - backgroundOffset; 54 | } 55 | 56 | function getLabelY(label: LabelOld) { 57 | const isVertical = label.labelOrientation === LabelOrientation.Vertical; 58 | const orientationOffset = isVertical ? (label.boundingBox.height / 2) : label.boundingBox.height; 59 | const backgroundOffset = label.hasBackground && !isVertical ? verticalLabelBackgroundPadding : 0; 60 | return label.boundingBox.top + orientationOffset - backgroundOffset; 61 | } 62 | 63 | function labelKeyFunction(label: LabelOld, index: number): string | number { 64 | if (label.key) { 65 | return label.key; 66 | } 67 | return index; 68 | } 69 | 70 | export function drawDefaultLabels( 71 | context: Selection, 72 | dataLabels: LabelOld[], 73 | numeric: boolean = false, 74 | hasTooltip: boolean = false 75 | ): Selection { 76 | const labels: Selection = context 77 | .selectAll(labelsClass.selectorName) 78 | .data(dataLabels, labelKeyFunction); 79 | 80 | labels 81 | .exit() 82 | .remove(); 83 | 84 | 85 | const dy: string = numeric 86 | ? undefined 87 | : "-0.15em"; 88 | 89 | const mergedLabels = labels.enter() 90 | .append("text") 91 | .classed(labelsClass.className, true) 92 | .merge(labels); 93 | 94 | interrupt(mergedLabels.node()) 95 | 96 | mergedLabels 97 | .text((label: LabelOld) => label.text) 98 | .attr("transform", (d: LabelOld) => { 99 | const translate = "translate(" + getLabelX(d) + "," + getLabelY(d) + ")"; 100 | return (d.labelOrientation === LabelOrientation.Vertical) ? (translate + "rotate(-90)") : translate; 101 | }) 102 | .attr("dy", dy) 103 | .style("fill", (d: LabelOld) => d.fill) 104 | .style("font-size", (d: LabelOld) => pixelConverter.fromPoint(d.fontSize || DefaultLabelFontSizeInPt)) 105 | .style("font-family", (d: LabelOld) => d.fontFamily ? d.fontFamily : undefined) 106 | .style("text-anchor", (d: LabelOld) => d.textAnchor) 107 | .style("font-weight", (d: LabelOld) => d.fontWeight) 108 | .style("font-style", (d: LabelOld) => d.fontStyle) 109 | .style("text-decoration", (d: LabelOld) => d.textDecoration) 110 | 111 | if (hasTooltip) { 112 | labels.append("title").text((d: LabelOld) => d.tooltip); 113 | labels.style("pointer-events", "all"); 114 | } 115 | 116 | return mergedLabels; 117 | } 118 | 119 | export interface DataLabelLayoutOptions { 120 | /** The amount of offset to start with when the data label is not centered */ 121 | startingOffset: number; 122 | /** Maximum distance labels will be offset by */ 123 | maximumOffset: number; 124 | /** The amount to increase the offset each attempt while laying out labels */ 125 | offsetIterationDelta?: number; 126 | /** Horizontal padding used for checking whether a label is inside a parent shape */ 127 | horizontalPadding?: number; 128 | /** Vertical padding used for checking whether a label is inside a parent shape */ 129 | verticalPadding?: number; 130 | /** Should we draw reference lines in case the label offset is greater then the default */ 131 | allowLeaderLines?: boolean; 132 | /** Should the layout system attempt to move the label inside the viewport when it outside, but close */ 133 | attemptToMoveLabelsIntoViewport?: boolean; 134 | } 135 | 136 | export const dataLabelLayoutStartingOffset: number = 2; 137 | export const dataLabelLayoutOffsetIterationDelta: number = 6; 138 | export const dataLabelLayoutMaximumOffset: number = dataLabelLayoutStartingOffset + (2 * dataLabelLayoutOffsetIterationDelta); 139 | 140 | export function getDataLabelLayoutOptions(chartType: CartesianChartType): DataLabelLayoutOptions { 141 | switch (chartType) { 142 | case CartesianChartType.Scatter: 143 | return { 144 | maximumOffset: dataLabelLayoutMaximumOffset, 145 | startingOffset: dataLabelLayoutStartingOffset, 146 | offsetIterationDelta: dataLabelLayoutOffsetIterationDelta, 147 | allowLeaderLines: true, 148 | attemptToMoveLabelsIntoViewport: true, 149 | }; 150 | default: 151 | return { 152 | maximumOffset: maxLabelOffset, 153 | startingOffset: startingLabelOffset, 154 | attemptToMoveLabelsIntoViewport: true, 155 | }; 156 | } 157 | } 158 | 159 | export function getNumberOfLabelsToRender(viewportWidth: number, labelDensity: number, minimumLabelsToRender: number, estimatedLabelWidth: number): number { 160 | if (labelDensity == null || labelDensity === 0) { 161 | return minimumLabelsToRender; 162 | } 163 | const parsedAndNormalizedDensity = labelDensity / 100; 164 | const maxNumberForViewport = Math.ceil(viewportWidth / estimatedLabelWidth); 165 | if (parsedAndNormalizedDensity === 1) { 166 | return maxNumberForViewport; 167 | } 168 | return minimumLabelsToRender + Math.floor(parsedAndNormalizedDensity * (maxNumberForViewport - minimumLabelsToRender)); 169 | } 170 | -------------------------------------------------------------------------------- /test/dataLabel/dataLabelManagerTest.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 DataLabelManager from "./../../src/dataLabel/dataLabelManager"; 28 | import { IDataLabelSettings, ILabelLayout, IDataLabelInfo } from "./../../src/dataLabel/dataLabelInterfaces"; 29 | import { RectOrientation, ContentPositions, OutsidePlacement } from "./../../src/dataLabel/dataLabelInterfaces"; 30 | import powerbi from "powerbi-visuals-api"; 31 | 32 | describe("DataLabelManager", () => { 33 | describe("Default Settings", () => { 34 | it("Check default values are true", () => { 35 | let labelManager: DataLabelManager = new DataLabelManager(), 36 | defaultSettings: IDataLabelSettings = labelManager.defaultSettings; 37 | 38 | expect(defaultSettings.anchorMargin).toBe(0); 39 | expect(defaultSettings.anchorRectOrientation).toBe(RectOrientation.None); 40 | expect(defaultSettings.contentPosition).toBe(ContentPositions.BottomCenter); 41 | expect(defaultSettings.maximumMovingDistance).toBe(12); 42 | expect(defaultSettings.minimumMovingDistance).toBe(3); 43 | expect(defaultSettings.opacity).toBe(1); 44 | expect(defaultSettings.outsidePlacement).toBe(OutsidePlacement.Disallowed); 45 | expect(defaultSettings.validContentPositions).toBe(ContentPositions.BottomCenter); 46 | }); 47 | }); 48 | 49 | describe("Get Label info - One value provided", () => { 50 | let labelManager: DataLabelManager = new DataLabelManager(), 51 | defaultSettings: IDataLabelSettings = labelManager.defaultSettings; 52 | 53 | it("Get Label info", () => { 54 | let result: IDataLabelInfo = labelManager.getLabelInfo({ minimumMovingDistance: 10 }); 55 | 56 | expect(defaultSettings.minimumMovingDistance).toEqual(3); 57 | expect(result.minimumMovingDistance).toEqual(10); 58 | }); 59 | 60 | it("Get Label info - all values provided", () => { 61 | let result: IDataLabelInfo = labelManager.getLabelInfo({ maximumMovingDistance: 12 }); 62 | 63 | expect(defaultSettings.anchorMargin).toEqual(0); 64 | expect(result.maximumMovingDistance).toEqual(12); 65 | }); 66 | 67 | it("Get Label info - maximumMovingDistance with anchorMargin > 0", () => { 68 | let result: IDataLabelInfo = labelManager.getLabelInfo({ maximumMovingDistance: 16, anchorMargin: 4 }); 69 | 70 | expect(result.anchorMargin).toEqual(4); 71 | // maximumMovingDistance should be increased with anchorMargin. 72 | expect(result.maximumMovingDistance).toEqual(16 + 4); 73 | }); 74 | 75 | it("Get Label info - Default value should be taken", () => { 76 | let result: IDataLabelInfo = labelManager.getLabelInfo({}); 77 | 78 | expect(defaultSettings.anchorMargin).toEqual(0); 79 | expect(result.anchorMargin).toEqual(0); 80 | }); 81 | }); 82 | 83 | describe("It should hide collided labels", () => { 84 | it("No Input, No Output", () => { 85 | let labelManager: DataLabelManager = new DataLabelManager(), 86 | viewPort: powerbi.IViewport = { width: 500, height: 500 }, 87 | labelLayout: ILabelLayout = { 88 | filter: () => true, 89 | labelLayout: { x: () => 250, y: () => 250 }, 90 | labelText: () => "", 91 | style: null 92 | }, 93 | data: any[] = []; 94 | expect(labelManager.hideCollidedLabels(viewPort, data, labelLayout)).toEqual([]); 95 | }); 96 | 97 | it("One non colliding label should be present", () => { 98 | let labelManager: DataLabelManager = new DataLabelManager(), 99 | viewPort: powerbi.IViewport = { width: 500, height: 500 }, 100 | labelLayout: ILabelLayout = { 101 | filter: () => true, 102 | labelLayout: { 103 | x: (element) => element.x, 104 | y: (element) => element.y 105 | }, 106 | labelText: () => "My-label", 107 | style: null 108 | }, 109 | data: any[] = [ 110 | { x: 5, y: 100 } 111 | ]; 112 | let result = labelManager.hideCollidedLabels(viewPort, data, labelLayout); 113 | expect(result.length).toEqual(1); 114 | }); 115 | 116 | it("Two Identitical labels, 1 should be hidden", () => { 117 | let labelManager: DataLabelManager = new DataLabelManager(), 118 | viewPort: powerbi.IViewport = { width: 500, height: 500 }, 119 | labelLayout: ILabelLayout = { 120 | filter: () => true, 121 | labelLayout: { 122 | x: (element) => element.x, 123 | y: (element) => element.y 124 | }, 125 | labelText: () => "My-label", 126 | style: null 127 | }, 128 | data: any[] = [ 129 | { x: 5, y: 100 }, 130 | { x: 5, y: 100 } 131 | ]; 132 | let result = labelManager.hideCollidedLabels(viewPort, data, labelLayout, false, true); 133 | expect(result.length).toEqual(1); 134 | }); 135 | }); 136 | 137 | describe("Is Valid Rect", () => { 138 | it("Is Valid Rect - Return true", () => { 139 | expect(DataLabelManager.isValid({ 140 | left: 150, 141 | top: 130, 142 | width: 120, 143 | height: 110 144 | })).toBe(true); 145 | }); 146 | 147 | it("Is Valid Rect - Negative values", () => { 148 | expect(DataLabelManager.isValid({ 149 | left: -150, 150 | top: -130, 151 | width: -120, 152 | height: -110 153 | })).toBe(false); 154 | }); 155 | 156 | it("Is Valid Rect - Empty Rect", () => { 157 | expect(DataLabelManager.isValid({ 158 | left: 0, 159 | top: 0, 160 | width: 0, 161 | height: 0 162 | })).toBe(false); 163 | }); 164 | 165 | it("Is Valid Rect - null Rect", () => { 166 | expect(DataLabelManager.isValid(null)).toBe(false); 167 | }); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /src/dataLabel/dataLabelArrangeGrid.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 | // powerbi.extensibility.utils.svg.shapes 28 | import { shapesInterfaces, IRect, shapes } from "powerbi-visuals-utils-svgutils"; 29 | 30 | import ISize = shapesInterfaces.ISize; 31 | import IThickness = shapesInterfaces.IThickness; 32 | 33 | // powerbi.extensibility.utils.type 34 | import { prototype as Prototype } from "powerbi-visuals-utils-typeutils"; 35 | 36 | // powerbi.extensibility.utils.formatting 37 | import { textMeasurementService, 38 | interfaces } from "powerbi-visuals-utils-formattingutils"; 39 | import TextProperties = interfaces.TextProperties; 40 | 41 | import { IArrangeGridElementInfo, ILabelLayout, IDataLabelInfo, DataPointLabels } from "./dataLabelInterfaces"; 42 | 43 | import { LabelTextProperties } from "./dataLabelUtils"; 44 | /** 45 | * Utility class to speed up the conflict detection by collecting the arranged items in the DataLabelsPanel. 46 | */ 47 | export default class DataLabelArrangeGrid { 48 | 49 | private grid: Array>> = []; 50 | // size of a grid cell 51 | private cellSize: ISize; 52 | private rowCount: number; 53 | private colCount: number; 54 | 55 | private static ARRANGEGRID_MIN_COUNT = 1; 56 | private static ARRANGEGRID_MAX_COUNT = 100; 57 | 58 | /** 59 | * Creates new ArrangeGrid. 60 | * @param size The available size 61 | */ 62 | constructor(size: ISize, elements: DataPointLabels[], layout: ILabelLayout) { 63 | if (size.width === 0 || size.height === 0) { 64 | this.initializeEmptyGrid(size); 65 | return; 66 | } 67 | 68 | const baseProperties = this.createBaseTextProperties(); 69 | this.initializeCellSize(); 70 | this.calculateMaxLabelSizes(elements, layout, baseProperties); 71 | this.adjustEmptyCellSizes(size); 72 | this.calculateGridDimensions(size); 73 | this.initializeGrid(); 74 | } 75 | 76 | private initializeEmptyGrid(size: ISize): void { 77 | this.cellSize = size; 78 | this.rowCount = this.colCount = 0; 79 | } 80 | 81 | private createBaseTextProperties(): TextProperties { 82 | return { 83 | fontFamily: LabelTextProperties.fontFamily, 84 | fontSize: LabelTextProperties.fontSize, 85 | fontWeight: LabelTextProperties.fontWeight, 86 | }; 87 | } 88 | 89 | private initializeCellSize(): void { 90 | this.cellSize = { width: 0, height: 0 }; 91 | } 92 | 93 | private calculateMaxLabelSizes(elements: DataPointLabels[], layout: ILabelLayout, baseProperties: TextProperties): void { 94 | for (const child of elements) { 95 | child.labeltext = layout.labelText(child); 96 | 97 | const properties = Prototype.inherit(baseProperties); 98 | properties.text = child.labeltext; 99 | properties.fontSize = this.getFontSize(child); 100 | 101 | child.size = this.measureTextSize(properties); 102 | 103 | this.updateMaxCellSize(child.size); 104 | } 105 | } 106 | 107 | private getFontSize(child: DataPointLabels): string { 108 | if (child.data?.labelFontSize) { 109 | return child.data.labelFontSize.toString(); 110 | } 111 | if (child.labelFontSize) { 112 | return child.labelFontSize.toString(); 113 | } 114 | return LabelTextProperties.fontSize; 115 | } 116 | 117 | private measureTextSize(properties: TextProperties): ISize { 118 | return { 119 | width: textMeasurementService.measureSvgTextWidth(properties), 120 | height: textMeasurementService.estimateSvgTextHeight(properties) 121 | }; 122 | } 123 | 124 | private updateMaxCellSize(size: ISize): void { 125 | const width = size.width * 2; 126 | const height = size.height * 2; 127 | 128 | if (width > this.cellSize.width) { 129 | this.cellSize.width = width; 130 | } 131 | if (height > this.cellSize.height) { 132 | this.cellSize.height = height; 133 | } 134 | } 135 | 136 | private adjustEmptyCellSizes(size: ISize): void { 137 | if (this.cellSize.width === 0) { 138 | this.cellSize.width = size.width; 139 | } 140 | if (this.cellSize.height === 0) { 141 | this.cellSize.height = size.height; 142 | } 143 | } 144 | 145 | private calculateGridDimensions(size: ISize): void { 146 | this.colCount = this.getGridRowColCount( 147 | this.cellSize.width, 148 | size.width, 149 | DataLabelArrangeGrid.ARRANGEGRID_MIN_COUNT, 150 | DataLabelArrangeGrid.ARRANGEGRID_MAX_COUNT 151 | ); 152 | 153 | this.rowCount = this.getGridRowColCount( 154 | this.cellSize.height, 155 | size.height, 156 | DataLabelArrangeGrid.ARRANGEGRID_MIN_COUNT, 157 | DataLabelArrangeGrid.ARRANGEGRID_MAX_COUNT 158 | ); 159 | 160 | this.cellSize.width = size.width / this.colCount; 161 | this.cellSize.height = size.height / this.rowCount; 162 | } 163 | 164 | private initializeGrid(): void { 165 | for (let x = 0; x < this.colCount; x++) { 166 | this.grid[x] = []; 167 | for (let y = 0; y < this.rowCount; y++) { 168 | this.grid[x][y] = []; 169 | } 170 | } 171 | } 172 | 173 | /** 174 | * Register a new label element. 175 | * @param element The label element to register. 176 | * @param rect The label element position rectangle. 177 | */ 178 | public add(element: IDataLabelInfo, rect: IRect): void { 179 | const indexRect = this.getGridIndexRect(rect); 180 | const grid = this.grid; 181 | 182 | for (let x = indexRect.left; x < indexRect.right; x++) { 183 | const column = grid[x]; 184 | for (let y = indexRect.top; y < indexRect.bottom; y++) { 185 | column[y].push({ element, rect }); 186 | } 187 | } 188 | } 189 | 190 | /** 191 | * Checks for conflict of given rectangle in registered elements. 192 | * @param rect The rectengle to check. 193 | * @return True if conflict is detected. 194 | */ 195 | public hasConflict(rect: IRect): boolean { 196 | const indexRect = this.getGridIndexRect(rect); 197 | const grid = this.grid; 198 | 199 | for (let x = indexRect.left; x < indexRect.right; x++) { 200 | const column = grid[x]; 201 | for (let y = indexRect.top; y < indexRect.bottom; y++) { 202 | const cell = column[y]; 203 | if (cell.some(item => shapes.isIntersecting(item.rect, rect))) { 204 | return true; 205 | } 206 | } 207 | } 208 | 209 | return false; 210 | } 211 | 212 | /** 213 | * Calculates the number of rows or columns in a grid 214 | * @param step is the largest label size (width or height) 215 | * @param length is the grid size (width or height) 216 | * @param minCount is the minimum allowed size 217 | * @param maxCount is the maximum allowed size 218 | * @return the number of grid rows or columns 219 | */ 220 | private getGridRowColCount(step: number, length: number, minCount: number, maxCount: number): number { 221 | return Math.min(Math.max(Math.ceil(length / step), minCount), maxCount); 222 | } 223 | 224 | /** 225 | * Returns the grid index of a given recangle 226 | * @param rect The rectengle to check. 227 | * @return grid index as a thickness object. 228 | */ 229 | private getGridIndexRect(rect: IRect): IThickness { 230 | const restrict = (n, min, max) => Math.min(Math.max(n, min), max); 231 | 232 | return { 233 | left: restrict(Math.floor(rect.left / this.cellSize.width), 0, this.colCount), 234 | top: restrict(Math.floor(rect.top / this.cellSize.height), 0, this.rowCount), 235 | right: restrict(Math.ceil((rect.left + rect.width) / this.cellSize.width), 0, this.colCount), 236 | bottom: restrict(Math.ceil((rect.top + rect.height) / this.cellSize.height), 0, this.rowCount) 237 | }; 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/axis/labelLayoutStrategy.ts: -------------------------------------------------------------------------------- 1 | import { 2 | select, 3 | Selection 4 | } from "d3-selection"; 5 | 6 | import { 7 | textMeasurementService, 8 | interfaces, 9 | textUtil, 10 | wordBreaker 11 | } from "powerbi-visuals-utils-formattingutils"; 12 | 13 | import ITextAsSVGMeasurer = interfaces.ITextAsSVGMeasurer; 14 | import TextProperties = interfaces.TextProperties; 15 | 16 | import { 17 | IAxisProperties, 18 | IMargin 19 | } from "./axisInterfaces"; 20 | 21 | const LeftPadding = 10; 22 | 23 | export function willLabelsFit( 24 | axisProperties: IAxisProperties, 25 | availableWidth: number, 26 | textMeasurer: ITextAsSVGMeasurer, 27 | properties: TextProperties): boolean { 28 | 29 | const labels = axisProperties.values; 30 | if (labels.length === 0) 31 | return false; 32 | 33 | const labelMaxWidth = axisProperties.xLabelMaxWidth !== undefined 34 | ? axisProperties.xLabelMaxWidth 35 | : availableWidth / labels.length; 36 | 37 | return !labels.some(d => { 38 | properties.text = d; 39 | return textMeasurer(properties) > labelMaxWidth; 40 | }); 41 | } 42 | 43 | export function willLabelsWordBreak( 44 | axisProperties: IAxisProperties, 45 | margin: IMargin, 46 | availableWidth: number, 47 | textWidthMeasurer: ITextAsSVGMeasurer, 48 | textHeightMeasurer: ITextAsSVGMeasurer, 49 | textTruncator: (properties: TextProperties, maxWidth: number) => string, 50 | properties: TextProperties) { 51 | 52 | const labels = axisProperties.values; 53 | const labelMaxWidth = axisProperties.xLabelMaxWidth !== undefined 54 | ? axisProperties.xLabelMaxWidth 55 | : availableWidth / labels.length; 56 | const maxRotatedLength = margin.bottom / DefaultRotation.sine; 57 | const height = textHeightMeasurer(properties); 58 | const maxNumLines = Math.max(1, Math.floor(margin.bottom / height)); 59 | 60 | if (labels.length === 0) 61 | return false; 62 | 63 | // If no break character and exceeds max width, word breaking will not work, return false 64 | const mustRotate = labels.some(label => { 65 | // Detect must rotate and return immediately 66 | properties.text = label; 67 | return !wordBreaker.hasBreakers(label) && textWidthMeasurer(properties) > labelMaxWidth; 68 | }); 69 | 70 | if (mustRotate) { 71 | return false; 72 | } 73 | 74 | const moreWordBreakChars = labels.filter((label, index: number) => { 75 | // ...otherwise compare rotation versus word breaking 76 | const allowedLengthProjectedOnXAxis = 77 | // Left margin is the width of Y axis. 78 | margin.left 79 | // There could be a padding before the first category. 80 | + axisProperties.outerPadding 81 | // Align the rotated text's top right corner to the middle of the corresponding category first. 82 | + axisProperties.categoryThickness * (index + 0.5) 83 | // Subtracting the left padding space from the allowed length 84 | - LeftPadding; 85 | 86 | const allowedLength = allowedLengthProjectedOnXAxis / DefaultRotation.cosine; 87 | const rotatedLength = Math.min(allowedLength, maxRotatedLength); 88 | 89 | // Which shows more characters? Rotated or maxNumLines truncated to labelMaxWidth? 90 | const wordBreakChars = wordBreaker.splitByWidth( 91 | label, 92 | properties, 93 | textWidthMeasurer, 94 | labelMaxWidth, 95 | maxNumLines, 96 | textTruncator).join(" "); 97 | 98 | properties.text = label; 99 | const rotateChars = textTruncator(properties, rotatedLength); 100 | 101 | // prefer word break (>=) as it takes up less plot area 102 | return textUtil.removeEllipses(wordBreakChars).length >= textUtil.removeEllipses(rotateChars).length; 103 | }); 104 | 105 | // prefer word break (>=) as it takes up less plot area 106 | return moreWordBreakChars.length >= Math.floor(labels.length / 2); 107 | } 108 | 109 | export const DefaultRotation = { 110 | sine: Math.sin(Math.PI * (35 / 180)), 111 | cosine: Math.cos(Math.PI * (35 / 180)), 112 | tangent: Math.tan(Math.PI * (35 / 180)), 113 | transform: "rotate(-35)", 114 | dy: "-0.5em", 115 | }; 116 | 117 | export const DefaultRotationWithScrollbar = { 118 | sine: Math.sin(Math.PI * (90 / 180)), 119 | cosine: Math.cos(Math.PI * (90 / 180)), 120 | tangent: Math.tan(Math.PI * (90 / 180)), 121 | transform: "rotate(-90)", 122 | dy: "-0.8em", 123 | }; 124 | 125 | // NOTE: the above rotations are matched to D3 tickSize(6,0) and do not work with other tick sizes. 126 | // we hide these default ticks anyway (on category axes that require rotation), we should make this work 127 | // with any tick size. For now just hardcode a TickSizeZero structure 128 | export const DefaultRotationWithScrollbarTickSizeZero = { 129 | sine: Math.sin(Math.PI * (90 / 180)), 130 | cosine: Math.cos(Math.PI * (90 / 180)), 131 | tangent: Math.tan(Math.PI * (90 / 180)), 132 | transform: "rotate(-90)", 133 | dy: "-0.3em", 134 | }; 135 | 136 | /** 137 | * Perform rotation and/or truncation of axis tick labels (SVG text) with ellipsis 138 | */ 139 | export function rotate( 140 | labelSelection: Selection, 141 | maxBottomMargin: number, 142 | textTruncator: (properties: TextProperties, maxWidth: number) => string, 143 | textProperties: TextProperties, 144 | needRotate: boolean, 145 | needEllipsis: boolean, 146 | axisProperties: IAxisProperties, 147 | margin: IMargin, 148 | scrollbarVisible: boolean) { 149 | 150 | let rotatedLength; 151 | let defaultRotation: any; 152 | const tickSize = axisProperties.axis.tickSize(); 153 | 154 | if (scrollbarVisible) { 155 | if (!tickSize) // zero or undefined 156 | defaultRotation = DefaultRotationWithScrollbarTickSizeZero; 157 | else 158 | defaultRotation = DefaultRotationWithScrollbar; 159 | } 160 | else { 161 | defaultRotation = DefaultRotation; 162 | } 163 | 164 | if (needRotate) { 165 | rotatedLength = maxBottomMargin / defaultRotation.sine; 166 | } 167 | 168 | labelSelection.each(function (datum) { 169 | const axisLabel = select(this); 170 | let labelText = axisLabel.text(); 171 | textProperties.text = labelText; 172 | if (needRotate) { 173 | const textContentIndex = axisProperties.values.indexOf(this.textContent); 174 | let allowedLengthProjectedOnXAxis = 175 | // Left margin is the width of Y axis. 176 | margin.left 177 | // There could be a padding before the first category. 178 | + axisProperties.outerPadding 179 | // Align the rotated text's top right corner to the middle of the corresponding category first. 180 | + axisProperties.categoryThickness * (textContentIndex + 0.5); 181 | 182 | // Subtracting the left padding space from the allowed length. 183 | if (!scrollbarVisible) 184 | allowedLengthProjectedOnXAxis -= LeftPadding; 185 | 186 | // Truncate if scrollbar is visible or rotatedLength exceeds allowedLength 187 | const allowedLength = allowedLengthProjectedOnXAxis / defaultRotation.cosine; 188 | if (scrollbarVisible || needEllipsis || (allowedLength < rotatedLength)) { 189 | labelText = textTruncator(textProperties, Math.min(allowedLength, rotatedLength)); 190 | axisLabel.text(labelText); 191 | } 192 | 193 | // NOTE: see note above - rotation only lines up with default d3 tickSize(6,0) 194 | // TODO don't do these rotations if we already did them 195 | axisLabel.style("text-anchor", "end") 196 | .attr("dx", "-0.5em") 197 | .attr("dy", defaultRotation.dy) 198 | .attr("transform", defaultRotation.transform); 199 | } else { 200 | const maxLabelWidth = !arrayIsEmpty(axisProperties.xLabelMaxWidths) ? axisProperties.xLabelMaxWidths[datum] : axisProperties.xLabelMaxWidth; 201 | const newLabelText = textTruncator(textProperties, maxLabelWidth); 202 | if (newLabelText !== labelText) 203 | axisLabel.text(newLabelText); 204 | // TODO don't do these rotations if we already did them 205 | axisLabel.style("text-anchor", "middle") 206 | .attr("dx", "0em") 207 | .attr("dy", "1em") 208 | .attr("transform", "rotate(0)"); 209 | } 210 | }); 211 | } 212 | 213 | export function wordBreak( 214 | text: Selection, 215 | axisProperties: IAxisProperties, 216 | maxHeight: number) { 217 | 218 | const allowedLength = axisProperties.xLabelMaxWidth; 219 | 220 | text.each(function () { 221 | const node = select(this); 222 | 223 | // Reset style of text node 224 | node 225 | .style("text-anchor", "middle") 226 | .attr("dx", "0em") 227 | .attr("dy", "1em") 228 | .attr("transform", "rotate(0)"); 229 | 230 | textMeasurementService.wordBreak(this, allowedLength, maxHeight); 231 | }); 232 | } 233 | 234 | export function clip(text: Selection, availableWidth: number, svgEllipsis: (textElement: SVGTextElement, maxWidth: number) => void) { 235 | if (text.size() === 0) { 236 | return; 237 | } 238 | 239 | text.each(function () { 240 | const text = select(this); 241 | svgEllipsis(text.node() as any, availableWidth); 242 | }); 243 | } 244 | 245 | function arrayIsEmpty(array: any[]): boolean { 246 | return !(array && array.length); 247 | } -------------------------------------------------------------------------------- /test/axis/helpers/axisPropertiesBuilder.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 powerbi from "powerbi-visuals-api"; 28 | import DataViewMetadataColumn = powerbi.DataViewMetadataColumn; 29 | import DataViewObjectPropertyIdentifier = powerbi.DataViewObjectPropertyIdentifier; 30 | // powerbi.extensibility.utils.type 31 | import { valueType } from "powerbi-visuals-utils-typeutils"; 32 | import ValueType = valueType.ValueType; 33 | 34 | // powerbi.extensibility.utils.formatting 35 | 36 | import { valueFormatter as vf } from "powerbi-visuals-utils-formattingutils"; 37 | import valueFormatter = vf; 38 | 39 | // powerbi.extensibility.utils.chart 40 | import * as axis from "./../../../src/axis/axis"; 41 | import * as axisInterfaces from "./../../../src/axis/axisInterfaces"; 42 | import IAxisProperties = axisInterfaces.IAxisProperties; 43 | import CreateAxisOptions = axisInterfaces.CreateAxisOptions; 44 | 45 | const dataStrings = ["Sun", "Mon", "Holiday"]; 46 | 47 | export const dataNumbers = [47.5, 98.22, 127.3]; 48 | 49 | const domainOrdinal3 = [0, 1, 2]; 50 | 51 | const domainBoolIndex = [0, 1]; 52 | 53 | export const domainNaN = [NaN, NaN]; 54 | 55 | const displayName: string = "Column"; 56 | 57 | const pixelSpan: number = 100; 58 | 59 | export const dataTime = [ 60 | new Date("10/15/2014"), 61 | new Date("10/15/2015"), 62 | new Date("10/15/2016") 63 | ]; 64 | 65 | const metaDataColumnText: DataViewMetadataColumn = { 66 | displayName: displayName, 67 | type: ValueType.fromDescriptor({ text: true }) 68 | }; 69 | 70 | export const metaDataColumnNumeric: DataViewMetadataColumn = { 71 | displayName: displayName, 72 | type: ValueType.fromDescriptor({ numeric: true }) 73 | }; 74 | 75 | export const metaDataColumnCurrency: DataViewMetadataColumn = { 76 | displayName: displayName, 77 | type: ValueType.fromDescriptor({ numeric: true }), 78 | objects: { general: { formatString: "$0" } }, 79 | }; 80 | 81 | const metaDataColumnBool: DataViewMetadataColumn = { 82 | displayName: displayName, 83 | type: ValueType.fromDescriptor({ bool: true }) 84 | }; 85 | 86 | const metaDataColumnTime: DataViewMetadataColumn = { 87 | displayName: displayName, 88 | type: ValueType.fromDescriptor({ dateTime: true }), 89 | format: "yyyy/MM/dd", 90 | objects: { general: { formatString: "yyyy/MM/dd" } }, 91 | }; 92 | 93 | const formatStringProp: DataViewObjectPropertyIdentifier = { 94 | objectName: "general", 95 | propertyName: "formatString" 96 | }; 97 | 98 | function getValueFnStrings(index): string { 99 | return dataStrings[index]; 100 | } 101 | 102 | function getValueFnNumbers(index): number { 103 | return dataNumbers[index]; 104 | } 105 | 106 | function getValueFnBool(d): boolean { 107 | return d === 0; 108 | } 109 | 110 | function getValueFnTime(index): Date { 111 | return new Date(index); 112 | } 113 | 114 | function getValueFnTimeIndex(index): Date { 115 | return dataTime[index]; 116 | } 117 | 118 | function createAxisOptions( 119 | metaDataColumn: DataViewMetadataColumn, 120 | dataDomain: any[], 121 | getValueFn? 122 | ): CreateAxisOptions { 123 | let axisOptions: CreateAxisOptions = { 124 | pixelSpan: pixelSpan, 125 | dataDomain: dataDomain, 126 | metaDataColumn: metaDataColumn, 127 | formatString: valueFormatter.getFormatString(metaDataColumn, formatStringProp), 128 | outerPadding: 0.5, 129 | isScalar: false, 130 | isVertical: false, // callers will update this to true if necessary before calling createAxis 131 | getValueFn: getValueFn, 132 | }; 133 | 134 | return axisOptions; 135 | } 136 | 137 | function getAxisOptions( 138 | metaDataColumn: DataViewMetadataColumn, 139 | domain?: string[], 140 | getValueFn?: (idx: number) => string): CreateAxisOptions { 141 | let axisOptions = createAxisOptions( 142 | metaDataColumn, 143 | domain ? domain : 144 | metaDataColumn ? domainOrdinal3 : [], 145 | getValueFn ? getValueFn : getValueFnStrings); 146 | 147 | return axisOptions; 148 | } 149 | 150 | export function buildAxisProperties(dataDomain: any[], metadataColumn?: DataViewMetadataColumn): IAxisProperties { 151 | let axisOptions = createAxisOptions(metadataColumn ? metadataColumn : metaDataColumnNumeric, dataDomain); 152 | axisOptions.isScalar = true; 153 | axisOptions.useTickIntervalForDisplayUnits = true; 154 | 155 | return axis.createAxis(axisOptions); 156 | } 157 | 158 | export function buildAxisPropertiesString(): IAxisProperties { 159 | let axisOptions = getAxisOptions(metaDataColumnText); 160 | 161 | return axis.createAxis(axisOptions); 162 | } 163 | 164 | export function buildAxisPropertiesText( 165 | metaDataColumn: DataViewMetadataColumn, 166 | domain?: string[], 167 | getValueFn?: (idx: number) => string): IAxisProperties { 168 | let axisOptions = getAxisOptions(metaDataColumn, domain, getValueFn); 169 | 170 | return axis.createAxis(axisOptions); 171 | } 172 | 173 | export function buildAxisPropertiesNumber(): IAxisProperties { 174 | return axis.createAxis( 175 | createAxisOptions( 176 | metaDataColumnNumeric, 177 | domainOrdinal3, 178 | getValueFnNumbers)); 179 | } 180 | 181 | export function buildAxisPropertiesBool(): IAxisProperties { 182 | return axis.createAxis( 183 | createAxisOptions( 184 | metaDataColumnBool, 185 | domainBoolIndex, 186 | getValueFnBool)); 187 | } 188 | 189 | export function buildAxisPropertiesStringWithCategoryThickness( 190 | categoryThickness: number = 5): IAxisProperties { 191 | let axisOptions = createAxisOptions( 192 | metaDataColumnText, 193 | domainOrdinal3, 194 | getValueFnStrings); 195 | 196 | axisOptions.categoryThickness = categoryThickness; 197 | 198 | return axis.createAxis(axisOptions); 199 | } 200 | 201 | export function buildAxisPropertiesNumbers(): IAxisProperties { 202 | let axisOptions = createAxisOptions( 203 | metaDataColumnNumeric, 204 | [ 205 | dataNumbers[0], 206 | dataNumbers[2] 207 | ]); 208 | 209 | axisOptions.isScalar = true; 210 | 211 | return axis.createAxis(axisOptions); 212 | } 213 | 214 | export function buildAxisPropertiesNan(): IAxisProperties { 215 | let axisOptions = createAxisOptions( 216 | metaDataColumnNumeric, 217 | domainNaN); 218 | 219 | axisOptions.isVertical = true; 220 | axisOptions.isScalar = true; 221 | 222 | return axis.createAxis(axisOptions); 223 | } 224 | 225 | export function buildAxisPropertiesWithDefinedInnerPadding(): IAxisProperties { 226 | let axisOptions = getAxisOptions(metaDataColumnText); 227 | axisOptions.innerPadding = 0.5; 228 | return axis.createAxis(axisOptions); 229 | } 230 | 231 | export function buildAxisPropertiesWithRangePointsUsing(): IAxisProperties { 232 | let axisOptions = getAxisOptions(metaDataColumnText); 233 | axisOptions.innerPadding = 0.5; 234 | axisOptions.useRangePoints = true; 235 | return axis.createAxis(axisOptions); 236 | } 237 | 238 | export function buildAxisPropertiesNumeric( 239 | dataDomain: any[], 240 | categoryThickness?: number, 241 | pixelSpan?: number, 242 | isVertical: boolean = true, 243 | isScalar: boolean = true, 244 | getValueFn?: (idx: number) => string): IAxisProperties { 245 | 246 | let axisOptions = createAxisOptions( 247 | metaDataColumnNumeric, 248 | dataDomain, 249 | getValueFn); 250 | 251 | if (categoryThickness) { 252 | axisOptions.categoryThickness = categoryThickness; 253 | } 254 | 255 | if (pixelSpan) { 256 | axisOptions.pixelSpan = pixelSpan; 257 | } 258 | 259 | axisOptions.isVertical = isVertical; 260 | axisOptions.isScalar = isScalar; 261 | 262 | return axis.createAxis(axisOptions); 263 | } 264 | 265 | export function buildAxisPropertiesTime( 266 | dataDomain: any[], 267 | isScalar: boolean = true, 268 | maxTicks?: number 269 | ): IAxisProperties { 270 | let axisOptions = createAxisOptions( 271 | metaDataColumnTime, 272 | dataDomain, 273 | getValueFnTime 274 | ); 275 | 276 | axisOptions.isScalar = isScalar; 277 | 278 | if (maxTicks) 279 | axisOptions.maxTickCount = maxTicks; 280 | 281 | return axis.createAxis(axisOptions); 282 | } 283 | 284 | export function buildAxisPropertiesTimeIndex(): IAxisProperties { 285 | let axisOptions = createAxisOptions( 286 | metaDataColumnTime, 287 | domainOrdinal3, 288 | getValueFnTimeIndex); 289 | 290 | return axis.createAxis(axisOptions); 291 | } 292 | -------------------------------------------------------------------------------- /src/dataLabel/dataLabelInterfaces.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 | 28 | // powerbi.extensibility.utils.svg 29 | import { shapesInterfaces, IRect } from "powerbi-visuals-utils-svgutils"; 30 | import IPoint = shapesInterfaces.IPoint; 31 | import ISize = shapesInterfaces.ISize; 32 | 33 | // powerbi.extensibility.utils.formatting 34 | import { valueFormatter } from "powerbi-visuals-utils-formattingutils"; 35 | import IValueFormatter = valueFormatter.IValueFormatter; 36 | 37 | import { Selection } from "d3-selection"; 38 | 39 | /** Defines possible content positions. */ 40 | export enum ContentPositions { 41 | 42 | /** Content position is not defined. */ 43 | None = 0, 44 | 45 | /** Content aligned top left. */ 46 | TopLeft = 1, 47 | 48 | /** Content aligned top center. */ 49 | TopCenter = 2, 50 | 51 | /** Content aligned top right. */ 52 | TopRight = 4, 53 | 54 | /** Content aligned middle left. */ 55 | MiddleLeft = 8, 56 | 57 | /** Content aligned middle center. */ 58 | MiddleCenter = 16, 59 | 60 | /** Content aligned middle right. */ 61 | MiddleRight = 32, 62 | 63 | /** Content aligned bottom left. */ 64 | BottomLeft = 64, 65 | 66 | /** Content aligned bottom center. */ 67 | BottomCenter = 128, 68 | 69 | /** Content aligned bottom right. */ 70 | BottomRight = 256, 71 | 72 | /** Content is placed inside the bounding rectangle in the center. */ 73 | InsideCenter = 512, 74 | 75 | /** Content is placed inside the bounding rectangle at the base. */ 76 | InsideBase = 1024, 77 | 78 | /** Content is placed inside the bounding rectangle at the end. */ 79 | InsideEnd = 2048, 80 | 81 | /** Content is placed outside the bounding rectangle at the base. */ 82 | OutsideBase = 4096, 83 | 84 | /** Content is placed outside the bounding rectangle at the end. */ 85 | OutsideEnd = 8192, 86 | 87 | /** Content supports all possible positions. */ 88 | All = 89 | TopLeft | 90 | TopCenter | 91 | TopRight | 92 | MiddleLeft | 93 | MiddleCenter | 94 | MiddleRight | 95 | BottomLeft | 96 | BottomCenter | 97 | BottomRight | 98 | InsideCenter | 99 | InsideBase | 100 | InsideEnd | 101 | OutsideBase | 102 | OutsideEnd, 103 | } 104 | 105 | /** 106 | * Rectangle orientation. Rectangle orientation is used to define vertical or horizontal orientation 107 | * and starting/ending side of the rectangle. 108 | */ 109 | export enum RectOrientation { 110 | /** Rectangle with no specific orientation. */ 111 | None, 112 | 113 | /** Vertical rectangle with base at the bottom. */ 114 | VerticalBottomTop, 115 | 116 | /** Vertical rectangle with base at the top. */ 117 | VerticalTopBottom, 118 | 119 | /** Horizontal rectangle with base at the left. */ 120 | HorizontalLeftRight, 121 | 122 | /** Horizontal rectangle with base at the right. */ 123 | HorizontalRightLeft, 124 | } 125 | 126 | /** 127 | * Defines if panel elements are allowed to be positioned 128 | * outside of the panel boundaries. 129 | */ 130 | export enum OutsidePlacement { 131 | /** Elements can be positioned outside of the panel. */ 132 | Allowed, 133 | 134 | /** Elements can not be positioned outside of the panel. */ 135 | Disallowed, 136 | 137 | /** Elements can be partially outside of the panel. */ 138 | Partial 139 | } 140 | 141 | /** 142 | * Defines an interface for information needed for default label positioning. Used in DataLabelsPanel. 143 | * Note the question marks: none of the elements are mandatory. 144 | */ 145 | export interface IDataLabelSettings { 146 | /** Distance from the anchor point. */ 147 | anchorMargin?: number; 148 | 149 | /** Orientation of the anchor rectangle. */ 150 | anchorRectOrientation?: RectOrientation; 151 | 152 | /** Preferable position for the label. */ 153 | contentPosition?: ContentPositions; 154 | 155 | /** Defines the rules if the elements can be positioned outside panel bounds. */ 156 | outsidePlacement?: OutsidePlacement; 157 | 158 | /** Defines the valid positions if repositionOverlapped is true. */ 159 | validContentPositions?: ContentPositions; 160 | 161 | /** Defines maximum moving distance to reposition an element. */ 162 | minimumMovingDistance?: number; 163 | 164 | /** Defines minimum moving distance to reposition an element. */ 165 | maximumMovingDistance?: number; 166 | 167 | /** Opacity effect of the label. Use it for dimming. */ 168 | opacity?: number; 169 | } 170 | 171 | /** 172 | * Defines an interface for information needed for label positioning. 173 | * None of the elements are mandatory, but at least anchorPoint OR anchorRect is needed. 174 | */ 175 | export interface IDataLabelInfo extends IDataLabelSettings { 176 | 177 | /** The point to which label is anchored. */ 178 | anchorPoint?: IPoint; 179 | 180 | /** The rectangle to which label is anchored. */ 181 | anchorRect?: IRect; 182 | 183 | /** Disable label rendering and processing. */ 184 | hideLabel?: boolean; 185 | 186 | /** 187 | * Defines the visibility rank. This will not be processed by arrange phase, 188 | * but can be used for preprocessing the hideLabel value. 189 | */ 190 | visibilityRank?: number; 191 | 192 | /** Defines the starting offset from AnchorRect. */ 193 | offset?: number; 194 | 195 | /** Defines the callout line data. It is calculated and used during processing. */ 196 | callout?: { start: IPoint; end: IPoint; }; 197 | 198 | /** Source of the label. */ 199 | source?: any; 200 | 201 | size?: ISize; 202 | } 203 | 204 | /** Interface for label rendering. */ 205 | export interface IDataLabelRenderer { 206 | renderLabelArray(labels: IArrangeGridElementInfo[]): void; 207 | } 208 | 209 | /** Interface used in internal arrange structures. */ 210 | export interface IArrangeGridElementInfo { 211 | element: IDataLabelInfo; 212 | rect: IRect; 213 | } 214 | 215 | export enum PointLabelPosition { 216 | Above, 217 | Below, 218 | } 219 | 220 | export interface PointDataLabelsSettings extends VisualDataLabelsSettings { 221 | position: PointLabelPosition; 222 | } 223 | 224 | export interface LabelFormattedTextOptions { 225 | label: any; 226 | maxWidth?: number; 227 | format?: string; 228 | formatter?: IValueFormatter; 229 | fontSize?: number; 230 | } 231 | 232 | export interface VisualDataLabelsSettings { 233 | show: boolean; 234 | showLabelPerSeries?: boolean; 235 | isSeriesExpanded?: boolean; 236 | displayUnits?: number; 237 | showCategory?: boolean; 238 | position?: any; 239 | precision?: number; 240 | labelColor: string; 241 | categoryLabelColor?: string; 242 | fontSize?: number; 243 | labelStyle?: any; 244 | } 245 | 246 | /** 247 | * Options for setting the labels card on the property pane 248 | */ 249 | export interface VisualDataLabelsSettingsOptions { 250 | show: boolean; 251 | instances: powerbi.VisualObjectInstance[]; 252 | dataLabelsSettings: VisualDataLabelsSettings; 253 | displayUnits?: boolean; 254 | precision?: boolean; 255 | position?: boolean; 256 | positionObject?: string[]; 257 | selector?: powerbi.data.Selector; 258 | fontSize?: boolean; 259 | showAll?: boolean; 260 | labelDensity?: boolean; 261 | labelStyle?: boolean; 262 | } 263 | 264 | // for collistion detection use 265 | export interface LabelEnabledDataPoint { 266 | labelX?: number; 267 | labelY?: number; 268 | // for overriding color from label settings 269 | labelFill?: string; 270 | // for display units and precision 271 | labeltext?: string; 272 | // taken from column metadata 273 | labelFormatString?: string; 274 | isLabelInside?: boolean; 275 | labelFontSize?: number; 276 | } 277 | 278 | export interface IColumnFormatterCache { 279 | [column: string]: IValueFormatter; 280 | defaultFormatter?: IValueFormatter; 281 | } 282 | 283 | export interface IColumnFormatterCacheManager { 284 | cache: IColumnFormatterCache; 285 | getOrCreate: (formatString: string, labelSetting: VisualDataLabelsSettings, value2?: number) => IValueFormatter; 286 | } 287 | 288 | export interface LabelPosition { 289 | y: (d: any, i: number) => number; 290 | x: (d: any, i: number) => number; 291 | } 292 | 293 | export interface ILabelLayout { 294 | labelText: (d: any) => string; 295 | labelLayout: LabelPosition; 296 | filter: (d: any) => boolean; 297 | style: object; 298 | } 299 | 300 | export interface DataLabelObject extends powerbi.DataViewObject { 301 | show: boolean; 302 | color: powerbi.Fill; 303 | labelDisplayUnits: number; 304 | labelPrecision?: number; 305 | labelPosition: any; 306 | fontSize?: number; 307 | showAll?: boolean; 308 | showSeries?: boolean; 309 | labelDensity?: string; 310 | labelStyle?: any; 311 | } 312 | 313 | export interface DataPointLabels { 314 | size: ISize; 315 | labeltext?: string; 316 | labelFontSize?: number; 317 | labelX?: number; 318 | labelY?: number; 319 | data?: { 320 | labelFontSize?: number; 321 | }; 322 | } 323 | 324 | export interface DrawDefaultLabelsProps { 325 | data: any[], 326 | context: Selection, 327 | layout: ILabelLayout, 328 | viewport: powerbi.IViewport, 329 | isAnimator?: boolean, 330 | animationDuration?: number, 331 | hasSelection?: boolean, 332 | hideCollidedLabels?: boolean 333 | } -------------------------------------------------------------------------------- /src/axis/axisInterfaces.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 { Selection } from "d3-selection"; 27 | import { Axis } from "d3-axis"; 28 | import { ScaleLinear } from "d3-scale"; 29 | 30 | import powerbi from "powerbi-visuals-api"; 31 | import DataViewMetadataColumn = powerbi.DataViewMetadataColumn; 32 | 33 | import { valueFormatter, interfaces } from "powerbi-visuals-utils-formattingutils"; 34 | import IValueFormatter = valueFormatter.IValueFormatter; 35 | import ITextAsSVGMeasurer = interfaces.ITextAsSVGMeasurer; 36 | import TextProperties = interfaces.TextProperties; 37 | 38 | import { valueType } from "powerbi-visuals-utils-typeutils"; 39 | import ValueType = valueType.ValueType; 40 | import PrimitiveValue = valueType.PrimitiveType; 41 | 42 | export interface IMargin { 43 | top: number; 44 | bottom: number; 45 | left: number; 46 | right: number; 47 | } 48 | 49 | export interface CartesianAxisProperties { 50 | x: IAxisProperties; 51 | xStack?: IStackedAxisProperties[]; 52 | y1: IAxisProperties; 53 | y2?: IAxisProperties; 54 | } 55 | 56 | export interface AxisHelperCategoryDataPoint { 57 | categoryValue: any; 58 | value?: any; 59 | highlight?: boolean; 60 | categoryIndex?: number; 61 | } 62 | 63 | export interface AxisHelperSeries { 64 | data: AxisHelperCategoryDataPoint[]; 65 | } 66 | 67 | export interface IAxisProperties { 68 | /** 69 | * The D3 Scale object. 70 | */ 71 | scale: any; 72 | /** 73 | * The D3 Axis object. 74 | */ 75 | axis: Axis; 76 | /** 77 | * An array of the tick values to display for this axis. 78 | */ 79 | values: any[]; 80 | /** 81 | * The ValueType of the column used for this axis. 82 | */ 83 | axisType: ValueType; 84 | /** 85 | * A formatter with appropriate properties configured for this field. 86 | */ 87 | formatter: IValueFormatter; 88 | /** 89 | * The axis title label. 90 | */ 91 | axisLabel: string; 92 | /** 93 | * Cartesian axes are either a category or value axis. 94 | */ 95 | isCategoryAxis: boolean; 96 | /** 97 | * (optional) The max width for category tick label values. used for ellipsis truncation / label rotation. 98 | */ 99 | xLabelMaxWidth?: number; 100 | /** 101 | * (optional) The max width for each category tick label values. used for ellipsis truncation / label rotation. Used by hierarchy categories that have varying widths. 102 | */ 103 | xLabelMaxWidths?: number[]; 104 | /** 105 | * (optional) The thickness of each category on the axis. 106 | */ 107 | categoryThickness?: number; 108 | /** 109 | * (optional) The outer padding in pixels applied to the D3 scale. 110 | */ 111 | outerPadding?: number; 112 | /** 113 | * (optional) Whether we are using a default domain. 114 | */ 115 | usingDefaultDomain?: boolean; 116 | /** 117 | * (optional) do default d3 axis labels fit? 118 | */ 119 | willLabelsFit?: boolean; 120 | /** 121 | * (optional) word break axis labels 122 | */ 123 | willLabelsWordBreak?: boolean; 124 | /** 125 | * (optional) Whether log scale is possible on the current domain. 126 | */ 127 | isLogScaleAllowed?: boolean; 128 | /** 129 | * (optional) Whether domain contains zero value and log scale is enabled. 130 | */ 131 | hasDisallowedZeroInDomain?: boolean; 132 | /** 133 | *(optional) The original data domain. Linear scales use .nice() to round to cleaner edge values. Keep the original data domain for later. 134 | */ 135 | dataDomain?: number[]; 136 | /** 137 | * (optional) The D3 graphics context for this axis 138 | */ 139 | graphicsContext?: Selection; 140 | } 141 | 142 | export interface IStackedAxisLineStyleInfo { 143 | // levelIndex: number; 144 | x1: number; 145 | y1: number; 146 | x2: number; 147 | y2: number; 148 | stroke?: string; 149 | strokeWidth?: number; 150 | } 151 | 152 | export interface IStackedAxisPlaceholder { 153 | placeholder: boolean; 154 | } 155 | 156 | export type IStackedAxisValue = PrimitiveValue | IStackedAxisPlaceholder; 157 | 158 | export interface IStackedAxisProperties extends IAxisProperties { 159 | /** 160 | * level 0 is the "leaf" level, closest to the plot area. 161 | */ 162 | levelIndex: number; 163 | /** 164 | * levelSize: height for x-axis (column chart), width for y-axis (bar chart) 165 | */ 166 | levelSize: number; 167 | /** 168 | * arrays that we can use to bind to D3 using .enter.data(arr) for styling the axis ticks 169 | */ 170 | lineStyleInfo: IStackedAxisLineStyleInfo[][]; 171 | 172 | /** 173 | * Values for the stacked axis 174 | */ 175 | values: IStackedAxisValue[]; 176 | 177 | /** 178 | * Values adjusted to align with the current viewport 179 | */ 180 | adjustedValues: IStackedAxisValue[]; 181 | } 182 | 183 | export interface CreateAxisOptions { 184 | /** 185 | * The dimension length for the axis, in pixels. 186 | */ 187 | pixelSpan: number; 188 | /** 189 | * The data domain. [min, max] for a scalar axis, or [1...n] index array for ordinal. 190 | */ 191 | dataDomain: number[]; 192 | /** 193 | * If the scalar number domain is [0,0] use this one instead 194 | */ 195 | zeroScalarDomain?: number[]; 196 | /** 197 | * The DataViewMetadataColumn will be used for dataType and tick value formatting. 198 | */ 199 | metaDataColumn: DataViewMetadataColumn; // TODO: remove this, we should just be passing in the formatString and the ValueType, not this DataView-specific object 200 | /** 201 | * The format string. 202 | */ 203 | formatString: string; 204 | /** 205 | * outerPadding to be applied to the axis. 206 | */ 207 | outerPadding: number; 208 | /** 209 | * Indicates if this is the category axis. 210 | */ 211 | isCategoryAxis?: boolean; 212 | /** 213 | * If true and the dataType is numeric or dateTime, 214 | * create a linear axis, else create an ordinal axis. 215 | */ 216 | isScalar?: boolean; 217 | /** 218 | * (optional) The scale is inverted for a vertical axis, 219 | * and different optimizations are made for tick labels. 220 | */ 221 | isVertical?: boolean; 222 | /** 223 | * (optional) For visuals that do not need zero (e.g. column/bar) use tickInterval. 224 | */ 225 | useTickIntervalForDisplayUnits?: boolean; 226 | /** 227 | * (optional) Combo charts can override the tick count to 228 | * align y1 and y2 grid lines. 229 | */ 230 | forcedTickCount?: number; 231 | /** 232 | * (optional) For scalar axis with scalar keys, the number of ticks should never exceed the number of scalar keys, 233 | * or labeling will look wierd (i.e. level of detail is Year, but month labels are shown between years) 234 | */ 235 | maxTickCount?: number; 236 | /** 237 | * (optional) Callback for looking up actual values from indices, 238 | * used when formatting tick labels. 239 | */ 240 | getValueFn?: (index: number, dataType: ValueType) => any; 241 | /** 242 | * (optional) The width/height of each category on the axis. 243 | */ 244 | categoryThickness?: number; 245 | /** (optional) the scale type of the axis. e.g. log, linear */ 246 | scaleType?: string; 247 | /** (optional) user selected display units */ 248 | axisDisplayUnits?: number; 249 | /** (optional) user selected precision */ 250 | axisPrecision?: number; 251 | /** (optional) for 100 percent stacked charts, causes formatString override and minTickInterval adjustments */ 252 | is100Pct?: boolean; 253 | /** (optional) sets clamping on the D3 scale, useful for drawing column chart rectangles as it simplifies the math during layout */ 254 | shouldClamp?: boolean; 255 | /** (optional) Disable "niceing" for numeric axis. It means that if max value is 172 the axis will show 172 but not rounded to upper value 180 */ 256 | disableNice?: boolean; 257 | /** (optional) Disable "niceing" for numeric axis. Disabling nice will be applid only when creating scale obj (bestTickCount will be applied to 'ticks' method) */ 258 | disableNiceOnlyForScale?: boolean; 259 | /** (optional) InnerPadding to be applied to the axis.*/ 260 | innerPadding?: number; 261 | /** (optioanl) Apply for using of RangePoints function instead of RangeBands inside CreateOrdinal scale function.*/ 262 | useRangePoints?: boolean; 263 | } 264 | 265 | export enum AxisOrientation { 266 | // Names of these enums match the values passed into axis.orient([orientation]) 267 | top, 268 | bottom, 269 | left, 270 | right 271 | } 272 | 273 | export interface CreateStackedAxisOptions { 274 | axis: Axis; 275 | scale: ScaleLinear; 276 | innerTickSize?: number; 277 | outerTickSize?: number; 278 | orient?: AxisOrientation; 279 | tickFormat: (datumIndex: number) => any; 280 | } 281 | 282 | export interface CreateScaleResult { 283 | scale: ScaleLinear; 284 | bestTickCount: number; 285 | usingDefaultDomain?: boolean; 286 | } 287 | 288 | export interface GetTickLabelMarginsOptions { 289 | viewport: powerbi.IViewport; 290 | yMarginLimit: number; 291 | textWidthMeasurer: ITextAsSVGMeasurer; 292 | textHeightMeasurer: ITextAsSVGMeasurer; 293 | axes: CartesianAxisProperties; 294 | bottomMarginLimit: number; 295 | properties: TextProperties; 296 | scrollbarVisible?: boolean; 297 | showOnRight?: boolean; 298 | renderXAxis?: boolean; 299 | renderY1Axis?: boolean; 300 | renderY2Axis?: boolean; 301 | } 302 | 303 | export interface CreateFormatterOptions { 304 | scaleDomain: any[]; 305 | dataDomain: any[]; 306 | dataType?: powerbi.ValueTypeDescriptor; 307 | isScalar: boolean; 308 | formatString?: string; 309 | bestTickCount: number; 310 | tickValues: number[]; 311 | useTickIntervalForDisplayUnits?: boolean; 312 | axisDisplayUnits?: number; 313 | axisPrecision?: number; 314 | } 315 | 316 | export interface GetBestNumberOfTicksOptions { 317 | min: number; 318 | max: number; 319 | valuesMetadata: powerbi.DataViewMetadataColumn[]; 320 | maxTickCount: number; 321 | isDateTime?: boolean; 322 | } 323 | 324 | export interface CreateScaleOptions { 325 | pixelSpan: number; 326 | metaDataColumn: powerbi.DataViewMetadataColumn; 327 | outerPadding?: number; 328 | isScalar?: boolean; 329 | isVertical?: boolean; 330 | forcedTickCount?: number; 331 | categoryThickness?: number; 332 | shouldClamp?: boolean; 333 | maxTickCount?: number; 334 | disableNice?: boolean; 335 | disableNiceOnlyForScale?: boolean; 336 | innerPadding?: number; 337 | useRangePoints?: boolean; 338 | scaleType?: string; 339 | zeroScalarDomain?: any[]; 340 | dataDomain: any[]; 341 | } 342 | 343 | -------------------------------------------------------------------------------- /test/dataLabel/dataLabelUtilsTest.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 powerbi from "powerbi-visuals-api"; 28 | import DataViewPropertyValue = powerbi.DataViewPropertyValue; 29 | 30 | // powerbi.extensibility.utils.chart 31 | import { assertColorsMatch } from "./../helpers/helpers"; 32 | import * as dataLabelUtils from "./../../src/dataLabel/dataLabelUtils"; 33 | import { VisualDataLabelsSettings, VisualDataLabelsSettingsOptions } from "./../../src/dataLabel/dataLabelInterfaces"; 34 | 35 | describe("dataLabel.utils", () => { 36 | afterEach(() => { 37 | document.querySelectorAll(".data-labels").forEach((element) => { 38 | element.remove(); 39 | }); 40 | }); 41 | 42 | describe("dataLabel.utils tests", () => { 43 | 44 | it("display units formatting values : Auto", () => { 45 | let value: number = 2000000, 46 | labelSettings: VisualDataLabelsSettings = dataLabelUtils.getDefaultLabelSettings(); 47 | 48 | labelSettings.displayUnits = 0; 49 | labelSettings.precision = 0; 50 | 51 | let value2 = 1000000, 52 | formattersCache = dataLabelUtils.createColumnFormatterCacheManager(), 53 | formatter = formattersCache.getOrCreate(null, labelSettings, value2), 54 | formattedValue = formatter.format(value); 55 | 56 | expect(formattedValue).toBe("2M"); 57 | }); 58 | 59 | it("display units formatting values : None", () => { 60 | let value: number = 20000, 61 | labelSettings: VisualDataLabelsSettings = dataLabelUtils.getDefaultLabelSettings(); 62 | 63 | labelSettings.displayUnits = 10; 64 | labelSettings.precision = 0; 65 | 66 | let formattersCache = dataLabelUtils.createColumnFormatterCacheManager(), 67 | formatter = formattersCache.getOrCreate(null, labelSettings), 68 | formattedValue = formatter.format(value); 69 | 70 | expect(formattedValue).toBe("20000"); 71 | }); 72 | 73 | it("display units formatting values : K", () => { 74 | let value: number = 20000, 75 | labelSettings: VisualDataLabelsSettings = dataLabelUtils.getDefaultLabelSettings(); 76 | 77 | labelSettings.displayUnits = 10000; 78 | labelSettings.precision = 0; 79 | 80 | let formattersCache = dataLabelUtils.createColumnFormatterCacheManager(), 81 | formatter = formattersCache.getOrCreate(null, labelSettings), 82 | formattedValue = formatter.format(value); 83 | 84 | expect(formattedValue).toBe("20K"); 85 | }); 86 | 87 | it("display units formatting values : M", () => { 88 | let value: number = 200000, 89 | labelSettings: VisualDataLabelsSettings = dataLabelUtils.getDefaultLabelSettings(); 90 | 91 | labelSettings.displayUnits = 1000000; 92 | labelSettings.precision = 1; 93 | 94 | let formattersCache = dataLabelUtils.createColumnFormatterCacheManager(), 95 | formatter = formattersCache.getOrCreate(null, labelSettings), 96 | formattedValue = formatter.format(value); 97 | 98 | expect(formattedValue).toBe("0.2M"); 99 | }); 100 | 101 | it("display units formatting values : B", () => { 102 | let value: number = 200000000000, 103 | labelSettings: VisualDataLabelsSettings = dataLabelUtils.getDefaultLabelSettings(); 104 | 105 | labelSettings.displayUnits = 1000000000; 106 | labelSettings.precision = 0; 107 | 108 | let formattersCache = dataLabelUtils.createColumnFormatterCacheManager(), 109 | formatter = formattersCache.getOrCreate(null, labelSettings), 110 | formattedValue = formatter.format(value); 111 | 112 | expect(formattedValue).toBe("200bn"); 113 | }); 114 | 115 | it("display units formatting values : T", () => { 116 | let value: number = 200000000000, 117 | labelSettings: VisualDataLabelsSettings = dataLabelUtils.getDefaultLabelSettings(); 118 | 119 | labelSettings.displayUnits = 1000000000000; 120 | labelSettings.precision = 1; 121 | 122 | let formattersCache = dataLabelUtils.createColumnFormatterCacheManager(), 123 | formatter = formattersCache.getOrCreate(null, labelSettings), 124 | formattedValue = formatter.format(value); 125 | 126 | expect(formattedValue).toBe("0.2T"); 127 | }); 128 | 129 | it("precision formatting using format string #0", () => { 130 | let value: number = 2000, 131 | labelSettings: VisualDataLabelsSettings = dataLabelUtils.getDefaultLabelSettings(), 132 | formattersCache = dataLabelUtils.createColumnFormatterCacheManager(), 133 | formatter = formattersCache.getOrCreate("#0", labelSettings), 134 | formattedValue = formatter.format(value); 135 | 136 | expect(formattedValue).toBe("2000"); 137 | }); 138 | 139 | it("precision formatting using format string #0.00", () => { 140 | let value: number = 2000, 141 | labelSettings: VisualDataLabelsSettings = dataLabelUtils.getDefaultLabelSettings(), 142 | formattersCache = dataLabelUtils.createColumnFormatterCacheManager(), 143 | formatter = formattersCache.getOrCreate("#0.00", labelSettings), 144 | formattedValue = formatter.format(value); 145 | 146 | expect(formattedValue).toBe("2000.00"); 147 | }); 148 | 149 | it("precision formatting using format string 0.#### $;-0.#### $;0 $", () => { 150 | let value: number = -2000.123456, 151 | labelSettings: VisualDataLabelsSettings = dataLabelUtils.getDefaultLabelSettings(), 152 | formattersCache = dataLabelUtils.createColumnFormatterCacheManager(), 153 | formatter = formattersCache.getOrCreate("#.#### $;-#.#### $;0 $", labelSettings), 154 | formattedValue = formatter.format(value); 155 | 156 | expect(formattedValue).toBe("-2000.1235 $"); 157 | }); 158 | 159 | it("precision formatting using forced precision", () => { 160 | let value: number = 2000.123456, 161 | labelSettings: VisualDataLabelsSettings = dataLabelUtils.getDefaultLabelSettings(); 162 | 163 | labelSettings.precision = 2; 164 | 165 | let formattersCache = dataLabelUtils.createColumnFormatterCacheManager(), 166 | formatter = formattersCache.getOrCreate("0.0000", labelSettings), 167 | formattedValue = formatter.format(value); 168 | 169 | expect(formattedValue).toBe("2000.12"); 170 | }); 171 | 172 | it("label formatting - multiple formats", () => { 173 | let formatCol1 = "#,0.0", 174 | formatCol2 = "$#,0.0", 175 | value: number = 1545, 176 | labelSettings: VisualDataLabelsSettings = dataLabelUtils.getDefaultLabelSettings(); 177 | 178 | labelSettings.displayUnits = null; 179 | labelSettings.precision = 1; 180 | 181 | let formattersCache = dataLabelUtils.createColumnFormatterCacheManager(), 182 | formatter1 = formattersCache.getOrCreate(formatCol1, labelSettings), 183 | formattedValue = formatter1.format(value); 184 | 185 | expect(formattedValue).toBe("1,545.0"); 186 | 187 | let formatter2 = formattersCache.getOrCreate(formatCol2, labelSettings); 188 | formattedValue = formatter2.format(value); 189 | 190 | expect(formattedValue).toBe("$1,545.0"); 191 | }); 192 | }); 193 | 194 | describe("Test enumerateDataLabels", () => { 195 | it("showAll should always be the last property when exists", () => { 196 | const labelSettings: VisualDataLabelsSettings = dataLabelUtils.getDefaultLabelSettings(); 197 | const options: VisualDataLabelsSettingsOptions = { 198 | dataLabelsSettings: labelSettings, 199 | displayUnits: true, 200 | instances: [], 201 | fontSize: true, 202 | labelDensity: true, 203 | labelStyle: true, 204 | position: true, 205 | positionObject: [], 206 | precision: true, 207 | selector: null, 208 | show: true, 209 | showAll: true, 210 | }; 211 | 212 | dataLabelUtils.enumerateDataLabels(options); 213 | 214 | expect(options.instances.length).toBe(1); 215 | 216 | const properties: DataViewPropertyValue = options.instances[0].properties; 217 | const propArray: string[] = Object.keys(properties); 218 | expect(propArray[propArray.length - 1]).toBe("showAll"); 219 | }); 220 | }); 221 | 222 | describe("Test enumerateCategoryLabels", () => { 223 | function getEnumerationObject(): powerbi.VisualObjectInstanceEnumerationObject { 224 | return { instances: [] }; 225 | } 226 | 227 | it("test default values", () => { 228 | let labelSettings = dataLabelUtils.getDefaultPointLabelSettings(); 229 | 230 | let enumerationWithColor = getEnumerationObject(); 231 | dataLabelUtils.enumerateCategoryLabels(enumerationWithColor, labelSettings, true); 232 | let objectsWithColor = enumerationWithColor.instances; 233 | 234 | let enumerationNoColor = getEnumerationObject(); 235 | dataLabelUtils.enumerateCategoryLabels(enumerationNoColor, labelSettings, false); 236 | let objectsNoColor = enumerationNoColor.instances; 237 | 238 | labelSettings.showCategory = true; 239 | let enumerationCategoryLabels = getEnumerationObject(); 240 | dataLabelUtils.enumerateCategoryLabels(enumerationCategoryLabels, labelSettings, false, true); 241 | let objectsCategoryLabels = enumerationCategoryLabels.instances; 242 | 243 | expect(objectsWithColor[0].properties["show"]).toBe(false); 244 | expect(objectsNoColor[0].properties["show"]).toBe(false); 245 | expect(objectsCategoryLabels[0].properties["show"]).toBe(true); 246 | 247 | expect(objectsWithColor[0].properties["color"]).toBe(labelSettings.labelColor); 248 | expect(objectsNoColor[0].properties["color"]).toBeUndefined(); 249 | }); 250 | 251 | it("test custom values", () => { 252 | let labelSettings = dataLabelUtils.getDefaultPointLabelSettings(); 253 | labelSettings.show = true; 254 | labelSettings.labelColor = "#FF0000"; 255 | 256 | let enumerationWithColor = getEnumerationObject(); 257 | dataLabelUtils.enumerateCategoryLabels(enumerationWithColor, labelSettings, true); 258 | let objectsWithColor = enumerationWithColor.instances; 259 | 260 | expect(objectsWithColor[0].properties["show"]).toBe(true); 261 | assertColorsMatch( 262 | objectsWithColor[0].properties["color"], 263 | labelSettings.labelColor); 264 | 265 | labelSettings.categoryLabelColor = "#222222"; 266 | enumerationWithColor = getEnumerationObject(); 267 | dataLabelUtils.enumerateCategoryLabels(enumerationWithColor, labelSettings, true); 268 | objectsWithColor = enumerationWithColor.instances; 269 | 270 | assertColorsMatch(objectsWithColor[0].properties["color"], labelSettings.categoryLabelColor); 271 | }); 272 | 273 | it("test null values", () => { 274 | let labelSettings = dataLabelUtils.getDefaultPointLabelSettings(); 275 | 276 | let enumerationWithColor = getEnumerationObject(); 277 | dataLabelUtils.enumerateCategoryLabels(enumerationWithColor, null, true); 278 | let objectsWithColor = enumerationWithColor.instances; 279 | 280 | expect(objectsWithColor[0].properties["show"]).toBe(labelSettings.show); 281 | expect(objectsWithColor[0].properties["color"]).toBe(labelSettings.labelColor); 282 | }); 283 | }); 284 | }); 285 | -------------------------------------------------------------------------------- /docs/api/axis-helper.md: -------------------------------------------------------------------------------- 1 | # Axis Helper 2 | > The ```Axis Helper``` provides functions in order to simplify manipulations with axis. 3 | 4 | The ```powerbi.extensibility.utils.chart.axis``` module provides the following functions: 5 | 6 | * [getRecommendedNumberOfTicksForXAxis](#getrecommendednumberofticksforxaxis) 7 | * [getRecommendedNumberOfTicksForYAxis](#getrecommendednumberofticksforyaxis) 8 | * [getBestNumberOfTicks](#getbestnumberofticks) 9 | * [getTickLabelMargins](#getticklabelmargins) 10 | * [isOrdinal](#isordinal) 11 | * [isDateTime](#isdatetime) 12 | * [getCategoryThickness](#getcategorythickness) 13 | * [invertOrdinalScale](#invertordinalscale) 14 | * [findClosestXAxisIndex](#findclosestxaxisindex) 15 | * [diffScaled](#diffscaled) 16 | * [createDomain](#createdomain) 17 | * [getCategoryValueType](#getcategoryvaluetype) 18 | * [createAxis](#createaxis) 19 | * [createFormatter](#createformatter) 20 | * [applyCustomizedDomain](#applycustomizeddomain) 21 | * [combineDomain](#combinedomain) 22 | * [powerOfTen](#poweroften) 23 | 24 | ## getRecommendedNumberOfTicksForXAxis 25 | 26 | This function returns recommended amount of ticks according to width of chart. 27 | 28 | ```typescript 29 | function getRecommendedNumberOfTicksForXAxis(availableWidth: number): number; 30 | ``` 31 | 32 | ### Example 33 | 34 | ```typescript 35 | import axisHelper = powerbi.extensibility.utils.chart.axis; 36 | 37 | axisHelper.getRecommendedNumberOfTicksForXAxis(1024); 38 | 39 | // returns: 8 40 | ``` 41 | 42 | ## getRecommendedNumberOfTicksForYAxis 43 | 44 | This function returns recommended amount of ticks according to height of chart. 45 | 46 | ```typescript 47 | function getRecommendedNumberOfTicksForYAxis(availableWidth: number) 48 | ``` 49 | 50 | ### Example 51 | 52 | ```typescript 53 | import axisHelper = powerbi.extensibility.utils.chart.axis; 54 | 55 | axisHelper.getRecommendedNumberOfTicksForYAxis(100); 56 | 57 | // returns: 3 58 | ``` 59 | 60 | ## getBestNumberOfTicks 61 | 62 | Gets the optimal number of ticks based on minimum value, maximum value, measure metadata and max tick count; 63 | 64 | ```typescript 65 | function getBestNumberOfTicks(min: number, max: number, valuesMetadata: DataViewMetadataColumn[], maxTickCount: number, isDateTime?: boolean): number; 66 | ``` 67 | 68 | ### Example 69 | 70 | ```typescript 71 | import axisHelper = powerbi.extensibility.utils.chart.axis; 72 | var dataViewMetadataColumnWithIntegersOnly: powerbi.DataViewMetadataColumn[] = [ 73 | { 74 | displayName: "col1", 75 | isMeasure: true, 76 | type: ValueType.fromDescriptor({ integer: true }) 77 | }, 78 | { 79 | displayName: "col2", 80 | isMeasure: true, 81 | type: ValueType.fromDescriptor({ integer: true }) 82 | } 83 | ]; 84 | var actual = AxisHelper.getBestNumberOfTicks(0, 3, dataViewMetadataColumnWithIntegersOnly, 6); 85 | // returns: 4 86 | ``` 87 | 88 | ## contains 89 | 90 | This function checks if a string contains a specified substring. 91 | 92 | ```typescript 93 | function contains(source: string, substring: string): boolean; 94 | ``` 95 | 96 | ### Example 97 | 98 | ```typescript 99 | import axisHelper = powerbi.extensibility.utils.chart.axis; 100 | 101 | axisHelper.contains("Microsoft Power BI Visuals", "Power BI"); 102 | 103 | // returns: true 104 | ``` 105 | 106 | ## getTickLabelMargins 107 | 108 | This function returns the margins for tick labels. 109 | 110 | ```typescript 111 | function getTickLabelMargins( 112 | viewport: IViewport, 113 | yMarginLimit: number, 114 | textWidthMeasurer: ITextAsSVGMeasurer, 115 | textHeightMeasurer: ITextAsSVGMeasurer, 116 | axes: CartesianAxisProperties, 117 | bottomMarginLimit: number, 118 | properties: TextProperties, 119 | scrollbarVisible?: boolean, 120 | showOnRight?: boolean, 121 | renderXAxis?: boolean, 122 | renderY1Axis?: boolean, 123 | renderY2Axis?: boolean): TickLabelMargins; 124 | ``` 125 | 126 | ### Example 127 | 128 | ```typescript 129 | import axisHelper = powerbi.extensibility.utils.chart.axis; 130 | 131 | axisHelper.getTickLabelMargins( 132 | plotArea, 133 | marginLimits.left, 134 | TextMeasurementService.measureSvgTextWidth, 135 | TextMeasurementService.estimateSvgTextHeight, 136 | axes, 137 | marginLimits.bottom, 138 | textProperties, 139 | /*scrolling*/ false, 140 | showY1OnRight, 141 | renderXAxis, 142 | renderY1Axis, 143 | renderY2Axis); 144 | 145 | // returns: xMax, 146 | yLeft, 147 | yRight, 148 | stackHeigh 149 | ``` 150 | 151 | ## isOrdinal 152 | 153 | Checks if a string is null or undefined or empty. 154 | 155 | ```typescript 156 | function isOrdinal(type: ValueTypeDescriptor): boolean; 157 | ``` 158 | 159 | ### Example 160 | 161 | ```typescript 162 | import axisHelper = powerbi.extensibility.utils.chart.axis; 163 | let type = ValueType.fromDescriptor({ misc: { barcode: true } }); 164 | axisHelper.isOrdinal(type); 165 | 166 | // returns: true 167 | ``` 168 | 169 | ## isDateTime 170 | 171 | Checks if value is of DateTime type. 172 | 173 | ```typescript 174 | function isDateTime(type: ValueTypeDescriptor): boolean; 175 | ``` 176 | 177 | ### Example 178 | 179 | ```typescript 180 | import axisHelper = powerbi.extensibility.utils.chart.axis; 181 | 182 | axisHelper.isDateTime(ValueType.fromDescriptor({ dateTime: true })) 183 | 184 | // returns: true 185 | ``` 186 | 187 | ## getCategoryThickness 188 | 189 | Uses the D3 scale to get the actual category thickness. 190 | 191 | ```typescript 192 | function getCategoryThickness(scale: any): number; 193 | ``` 194 | 195 | ### Example 196 | 197 | ```typescript 198 | import axisHelper = powerbi.extensibility.utils.chart.axis; 199 | 200 | let range = [0, 100]; 201 | let domain = [0, 10]; 202 | let scale = d3.scale.linear().domain(domain).range(range); 203 | let actualThickness = AxisHelper.getCategoryThickness(scale); 204 | ``` 205 | 206 | ## invertOrdinalScale 207 | 208 | This function inverts the ordinal scale. If x < scale.range()[0], then scale.domain()[0] is returned. 209 | Otherwise, it returns the greatest item in scale.domain() that's <= x. 210 | 211 | ```typescript 212 | function invertOrdinalScale(scale: d3.scale.Ordinal, x: number) ; 213 | ``` 214 | 215 | ### Example 216 | 217 | ```typescript 218 | import axisHelper = powerbi.extensibility.utils.chart.axis; 219 | 220 | let domain: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 221 | pixelSpan: number = 100, 222 | ordinalScale: d3.scale.ordinal = axisHelper.createOrdinalScale(pixelSpan, domain, 0.4); 223 | 224 | axisHelper.invertOrdinalScale(ordinalScale, 49); 225 | 226 | // returns: 4 227 | ``` 228 | 229 | ## findClosestXAxisIndex 230 | 231 | This function finds and returns the closest x-axis index. 232 | 233 | ```typescript 234 | function findClosestXAxisIndex(categoryValue: number, categoryAxisValues: AxisHelperCategoryDataPoint[]): number; 235 | ``` 236 | 237 | ### Example 238 | 239 | ```typescript 240 | import axisHelper = powerbi.extensibility.utils.chart.axis; 241 | 242 | /** 243 | * Finds the index of the category of the given x coordinate given. 244 | * pointX is in non-scaled screen-space, and offsetX is in render-space. 245 | * offsetX does not need any scaling adjustment. 246 | * @param {number} pointX The mouse coordinate in screen-space, without scaling applied 247 | * @param {number} offsetX Any left offset in d3.scale render-space 248 | * @return {number} 249 | */ 250 | private findIndex(pointX: number, offsetX?: number): number { 251 | // we are using mouse coordinates that do not know about any potential CSS transform scale 252 | let xScale = this.scaleDetector.getScale().x; 253 | if (!Double.equalWithPrecision(xScale, 1.0, 0.00001)) { 254 | pointX = pointX / xScale; 255 | } 256 | if (offsetX) { 257 | pointX += offsetX; 258 | } 259 | 260 | let index = axisHelper.invertScale(this.xAxisProperties.scale, pointX); 261 | if (this.data.isScalar) { 262 | // When we have scalar data the inverted scale produces a category value, so we need to search for the closest index. 263 | index = axisHelper.findClosestXAxisIndex(index, this.data.categoryData); 264 | } 265 | 266 | return index; 267 | } 268 | ``` 269 | 270 | ## diffScaled 271 | 272 | This function computes and returns a diff of values in the scale. 273 | 274 | ```typescript 275 | function diffScaled(scale: d3.scale.Linear, value1: any, value2: any): number; 276 | ``` 277 | 278 | ### Example 279 | 280 | ```typescript 281 | import axisHelper = powerbi.extensibility.utils.chart.axis; 282 | 283 | var scale: d3.scale.Linear, 284 | range = [0, 999], 285 | domain = [0, 1, 2, 3, 4, 5, 6, 7, 8, 999]; 286 | 287 | scale = d3.scale.linear() 288 | .range(range) 289 | .domain(domain); 290 | 291 | return axisHelper.diffScaled(scale, 0, 0)); 292 | 293 | // returns: 0 294 | ``` 295 | 296 | ## createDomain 297 | 298 | This function creates a domain of values for axis. 299 | 300 | ```typescript 301 | function createDomain(data: any[], axisType: ValueTypeDescriptor, isScalar: boolean, forcedScalarDomain: any[], ensureDomain?: NumberRange): number[]; 302 | ``` 303 | 304 | ### Example 305 | 306 | ```typescript 307 | import axisHelper = powerbi.extensibility.utils.chart.axis; 308 | 309 | var cartesianSeries = [ 310 | { 311 | data: [{ categoryValue: 7, value: 11, categoryIndex: 0, seriesIndex: 0, }, { 312 | categoryValue: 9, value: 9, categoryIndex: 1, seriesIndex: 0, 313 | }, { 314 | categoryValue: 15, value: 6, categoryIndex: 2, seriesIndex: 0, 315 | }, { categoryValue: 22, value: 7, categoryIndex: 3, seriesIndex: 0, }] 316 | }, 317 | ]; 318 | 319 | var domain = axisHelper.createDomain(cartesianSeries, ValueType.fromDescriptor({ text: true }), false, []); 320 | 321 | // returns: [0, 1, 2, 3] 322 | ``` 323 | 324 | ## getCategoryValueType 325 | 326 | This function gets the ValueType of a category column, defaults to Text if the type is not present. 327 | 328 | ```typescript 329 | function getCategoryValueType(data: any[], axisType: ValueTypeDescriptor, isScalar: boolean, forcedScalarDomain: any[], ensureDomain?: NumberRange): number[]; 330 | ``` 331 | 332 | ### Example 333 | 334 | ```typescript 335 | import axisHelper = powerbi.extensibility.utils.chart.axis; 336 | 337 | var cartesianSeries = [ 338 | { 339 | data: [{ categoryValue: 7, value: 11, categoryIndex: 0, seriesIndex: 0, }, { 340 | categoryValue: 9, value: 9, categoryIndex: 1, seriesIndex: 0, 341 | }, { 342 | categoryValue: 15, value: 6, categoryIndex: 2, seriesIndex: 0, 343 | }, { categoryValue: 22, value: 7, categoryIndex: 3, seriesIndex: 0, }] 344 | }, 345 | ]; 346 | 347 | axisHelper.getCategoryValueType(cartesianSeries, ValueType.fromDescriptor({ text: true }), false, []); 348 | 349 | // returns: [0, 1, 2, 3] 350 | ``` 351 | 352 | ## createAxis 353 | 354 | This function creates a D3 axis including scale. Can be vertical or horizontal, and either datetime, numeric, or text. 355 | 356 | ```typescript 357 | function createAxis(options: CreateAxisOptions): IAxisProperties; 358 | ``` 359 | ### Example 360 | 361 | ```typescript 362 | import axisHelper = powerbi.extensibility.utils.chart.axis; 363 | import valueFormatter = powerbi.visuals.valueFormatter; 364 | 365 | var dataPercent = [0.0, 0.33, 0.49]; 366 | 367 | var formatStringProp: powerbi.DataViewObjectPropertyIdentifier = { 368 | objectName: 'general', 369 | propertyName: 'formatString', 370 | }; 371 | let metaDataColumnPercent: powerbi.DataViewMetadataColumn = { 372 | displayName: 'Column', 373 | type: ValueType.fromDescriptor({ numeric: true }), 374 | objects: { 375 | general: { 376 | formatString: '0 %', 377 | } 378 | } 379 | }; 380 | 381 | var os = AxisHelper.createAxis({ 382 | pixelSpan: 100, 383 | dataDomain: [dataPercent[0], dataPercent[2]], 384 | metaDataColumn: metaDataColumnPercent, 385 | formatString: valueFormatter.getFormatString(metaDataColumnPercent, formatStringProp), 386 | outerPadding: 0.5, 387 | isScalar: true, 388 | isVertical: true, 389 | }); 390 | ``` 391 | 392 | ## applyCustomizedDomain 393 | 394 | This function sets customized domain, but don't change when nothing is set. 395 | 396 | ```typescript 397 | function applyCustomizedDomain(customizedDomain, forcedDomain: any[]): any[]; 398 | ``` 399 | 400 | ### Example 401 | 402 | ```typescript 403 | import axisHelper = powerbi.extensibility.utils.chart.axis; 404 | 405 | let customizedDomain = [undefined, 20], 406 | existingDomain = [0, 10]; 407 | 408 | axisHelper.applyCustomizedDomain(customizedDomain, existingDomain); 409 | 410 | // returns: {0:0, 1:20} 411 | ``` 412 | 413 | ## combineDomain 414 | 415 | This function combines the forced domain with the actual domain if one of the values was set. 416 | The forcedDomain is in 1st priority. Extends the domain if the any reference point requires it. 417 | 418 | ```typescript 419 | function combineDomain(forcedDomain: any[], domain: any[], ensureDomain?: NumberRange): any[]; 420 | ``` 421 | 422 | ### Example 423 | 424 | ```typescript 425 | import axisHelper = powerbi.extensibility.utils.chart.axis; 426 | 427 | let forcedYDomain = this.valueAxisProperties 428 | ? [this.valueAxisProperties['secStart'], this.valueAxisProperties['secEnd']] 429 | : null; 430 | 431 | let xDomain = [minX, maxX]; 432 | 433 | AxisHelper.combineDomain(forcedYDomain, xDomain, ensureXDomain); 434 | ``` 435 | 436 | ## powerOfTen 437 | 438 | This function indicates whether the number is power of 10. 439 | 440 | ```typescript 441 | function powerOfTen(d: any): boolean; 442 | ``` 443 | 444 | ### Example 445 | 446 | ```typescript 447 | import axis = powerbi.extensibility.utils.chart.axis; 448 | 449 | axis.powerOfTen(10); 450 | 451 | // returns: true 452 | ``` 453 | -------------------------------------------------------------------------------- /src/label/dataLabelRectPositioner.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 | shapes, 29 | shapesInterfaces, 30 | IRect, 31 | } from "powerbi-visuals-utils-svgutils"; 32 | 33 | import { double } from "powerbi-visuals-utils-typeutils"; 34 | 35 | import { 36 | LabelDataPointLayoutInfo, 37 | RectLabelPosition, 38 | LabelParentRect, 39 | NewRectOrientation, 40 | LabelDataPoint 41 | } from "./labelLayout"; 42 | 43 | /** 44 | * (Private) Contains methods for calculating the bounding box of a data label 45 | */ 46 | 47 | export interface OverflowingConstants { 48 | equalityPrecision: number; 49 | minIntersectionRatio: number; 50 | parentToLabelOverflowRatioThreshold: number; 51 | } 52 | 53 | export const LabelOverflowingConsts: OverflowingConstants = { 54 | equalityPrecision: 0.09, 55 | minIntersectionRatio: 0.2, 56 | parentToLabelOverflowRatioThreshold: 1 / 50 57 | }; 58 | 59 | interface LabelContainment { 60 | isContainedHorizontally: boolean; 61 | isContainedVertically: boolean; 62 | } 63 | 64 | export function getLabelRect(labelDataPointLayoutInfo: LabelDataPointLayoutInfo, position: RectLabelPosition, offset: number): IRect { 65 | const labelDataPoint = labelDataPointLayoutInfo.labelDataPoint; 66 | const parentRect: LabelParentRect = labelDataPoint.parentShape; 67 | if (parentRect != null) { 68 | const defaultProps: [shapesInterfaces.ISize, IRect, number] = [labelDataPointLayoutInfo.labelSize, parentRect.rect, offset]; 69 | // Each combination of position and orientation results in a different actual positioning, which is then called. 70 | switch (position) { 71 | case RectLabelPosition.InsideCenter: 72 | case RectLabelPosition.OverflowInsideCenter: 73 | switch (parentRect.orientation) { 74 | case NewRectOrientation.VerticalBottomBased: 75 | case NewRectOrientation.VerticalTopBased: 76 | return middleVertical(...defaultProps); 77 | case NewRectOrientation.HorizontalLeftBased: 78 | case NewRectOrientation.HorizontalRightBased: 79 | return middleHorizontal(...defaultProps); 80 | case NewRectOrientation.None: 81 | // TODO: which of the above cases should we default to for rects with no orientation? 82 | } 83 | break; 84 | case RectLabelPosition.InsideBase: 85 | case RectLabelPosition.OverflowInsideBase: 86 | switch (parentRect.orientation) { 87 | case NewRectOrientation.VerticalBottomBased: 88 | return bottomInside(...defaultProps); 89 | case NewRectOrientation.VerticalTopBased: 90 | return topInside(...defaultProps); 91 | case NewRectOrientation.HorizontalLeftBased: 92 | return leftInside(...defaultProps); 93 | case NewRectOrientation.HorizontalRightBased: 94 | return rightInside(...defaultProps); 95 | case NewRectOrientation.None: 96 | // TODO: which of the above cases should we default to for rects with no orientation? 97 | } 98 | break; 99 | case RectLabelPosition.InsideEnd: 100 | case RectLabelPosition.OverflowInsideEnd: 101 | switch (parentRect.orientation) { 102 | case NewRectOrientation.VerticalBottomBased: 103 | return topInside(...defaultProps); 104 | case NewRectOrientation.VerticalTopBased: 105 | return bottomInside(...defaultProps); 106 | case NewRectOrientation.HorizontalLeftBased: 107 | return rightInside(...defaultProps); 108 | case NewRectOrientation.HorizontalRightBased: 109 | return leftInside(...defaultProps); 110 | case NewRectOrientation.None: 111 | // TODO: which of the above cases should we default to for rects with no orientation? 112 | } 113 | break; 114 | case RectLabelPosition.OutsideBase: 115 | switch (parentRect.orientation) { 116 | case NewRectOrientation.VerticalBottomBased: 117 | return bottomOutside(...defaultProps); 118 | case NewRectOrientation.VerticalTopBased: 119 | return topOutside(...defaultProps); 120 | case NewRectOrientation.HorizontalLeftBased: 121 | return leftOutside(...defaultProps); 122 | case NewRectOrientation.HorizontalRightBased: 123 | return rightOutside(...defaultProps); 124 | case NewRectOrientation.None: 125 | // TODO: which of the above cases should we default to for rects with no orientation? 126 | } 127 | break; 128 | case RectLabelPosition.OutsideEnd: 129 | switch (parentRect.orientation) { 130 | case NewRectOrientation.VerticalBottomBased: 131 | return topOutside(...defaultProps); 132 | case NewRectOrientation.VerticalTopBased: 133 | return bottomOutside(...defaultProps); 134 | case NewRectOrientation.HorizontalLeftBased: 135 | return rightOutside(...defaultProps); 136 | case NewRectOrientation.HorizontalRightBased: 137 | return leftOutside(...defaultProps); 138 | case NewRectOrientation.None: 139 | // TODO: which of the above cases should we default to for rects with no orientation? 140 | } 141 | } 142 | } 143 | else { 144 | // TODO: Data labels for non-rectangular visuals (line chart) 145 | } 146 | return null; 147 | } 148 | 149 | export function canFitWithinParent({labelDataPoint, labelSize}: LabelDataPointLayoutInfo, horizontalPadding: number, verticalPadding: number): boolean { 150 | const parentRect = (labelDataPoint.parentShape).rect; 151 | const horizontalPaddingWithLabel = 2 * horizontalPadding + labelSize.width; 152 | const verticalPaddingWithLabel = 2 * verticalPadding + labelSize.height; 153 | return (horizontalPaddingWithLabel < parentRect.width) || (verticalPaddingWithLabel < parentRect.height); 154 | } 155 | 156 | export function isLabelWithinParent(labelRect: IRect, labelPoint: LabelDataPoint, horizontalPadding: number, verticalPadding: number): boolean { 157 | const parentRect = (labelPoint.parentShape).rect; 158 | const { left, top, width, height } = shapes.inflate( 159 | labelRect, 160 | { left: horizontalPadding, right: horizontalPadding, top: verticalPadding, bottom: verticalPadding } 161 | ); 162 | return shapes.containsPoint(parentRect, { 163 | x: left, 164 | y: top 165 | }) && shapes.containsPoint(parentRect, { 166 | x: left + width, 167 | y: top + height 168 | }); 169 | } 170 | 171 | export function isValidLabelOverflowing(labelRect: IRect, labelPoint: LabelDataPoint, hasMultipleDataSeries: boolean): boolean { 172 | const parentRect = (labelPoint.parentShape).rect; 173 | 174 | if (!shapes.isIntersecting(labelRect, parentRect)) { 175 | return false; // label isn't overflowing from within parent 176 | } 177 | 178 | const intersection = shapes.intersect(labelRect, parentRect); 179 | const precision = LabelOverflowingConsts.equalityPrecision; 180 | 181 | const labelContainment = getLabelContainment(labelRect, intersection, precision); 182 | const parentOrientation = (labelPoint.parentShape).orientation; 183 | const isParentOrientVertically = isVerticalOrientation(parentOrientation); 184 | 185 | if (!isValidContainment(labelContainment, hasMultipleDataSeries, isParentOrientVertically)) { 186 | // Our overflowing definition require that at least one label's rectangle dimension (width / height) to be contained in parent rectangle. 187 | // Furthermore, if we have multiple data series the contained dimention should be respective with the parent's orientation. 188 | // To avoid data labels collisions from one series to another which appears inside the same bar. 189 | return false; 190 | } 191 | 192 | if (labelContainment.isContainedHorizontally && labelContainment.isContainedVertically) { 193 | return true; // null-overflowing, label is fully contained. 194 | } 195 | // Our overflowing definition require that the label and parent "will touch each other enough", this is defined by the ratio of their intersection 196 | // (touching) against each of them, we look at the maximal intersection ratio which means that at least one of them is 'touched' enought by the other. 197 | const labelAndParentIntersectEnough = maximalIntersectionRatio(labelRect, parentRect) >= 198 | LabelOverflowingConsts.minIntersectionRatio; 199 | 200 | // Our overflowing definition require that the overflowing dimensions will not be too big in comparison to the same dimension of parent. 201 | // this is done to avoid situationion where the parent is barely visible or that label text is very long. 202 | return labelAndParentIntersectEnough && 203 | isValidOverflowRatio(labelRect, parentRect, labelContainment); 204 | } 205 | 206 | function getLabelContainment(labelRect: IRect, intersection: IRect, precision: number): LabelContainment { 207 | return { 208 | isContainedHorizontally: 209 | double.equalWithPrecision(intersection.left, labelRect.left, precision) && 210 | double.equalWithPrecision(intersection.width, labelRect.width, precision), 211 | isContainedVertically: 212 | double.equalWithPrecision(intersection.top, labelRect.top, precision) && 213 | double.equalWithPrecision(intersection.height, labelRect.height, precision) 214 | }; 215 | } 216 | 217 | function isVerticalOrientation(orientation: NewRectOrientation): boolean { 218 | return [ 219 | NewRectOrientation.VerticalBottomBased, 220 | NewRectOrientation.VerticalTopBased 221 | ].includes(orientation); 222 | } 223 | 224 | function isValidContainment( 225 | containment: LabelContainment, 226 | hasMultipleDataSeries: boolean, 227 | isParentOrientVertically: boolean 228 | ): boolean { 229 | if (!containment.isContainedHorizontally && !containment.isContainedVertically) { 230 | return false; 231 | } 232 | 233 | if (hasMultipleDataSeries) { 234 | if (isParentOrientVertically && !containment.isContainedVertically) { 235 | return false; 236 | } 237 | if (!isParentOrientVertically && !containment.isContainedHorizontally) { 238 | return false; 239 | } 240 | } 241 | 242 | return true; 243 | } 244 | 245 | function isValidOverflowRatio( 246 | labelRect: IRect, 247 | parentRect: IRect, 248 | containment: LabelContainment 249 | ): boolean { 250 | const threshold = LabelOverflowingConsts.parentToLabelOverflowRatioThreshold; 251 | 252 | if (containment.isContainedVertically) { 253 | return parentRect.width === 0 || (parentRect.width / labelRect.width) > threshold; 254 | } 255 | 256 | if (containment.isContainedHorizontally) { 257 | return parentRect.height === 0 || (parentRect.height / labelRect.height) > threshold; 258 | } 259 | 260 | return false; 261 | } 262 | 263 | export function maximalIntersectionRatio(labelRect: IRect, parentRect: IRect): number { 264 | const getArea = (rect: IRect) => rect.width * rect.height; 265 | 266 | const parentArea = getArea(parentRect); 267 | const labelArea = getArea(labelRect); 268 | 269 | const maxArea = Math.max(parentArea, labelArea); 270 | if (maxArea === 0) { 271 | return 0; 272 | } 273 | 274 | const intersectionArea = getArea(shapes.intersect(parentRect, labelRect)); 275 | const divisor = labelArea === 0 ? parentArea : Math.min(parentArea, labelArea); 276 | 277 | return intersectionArea / divisor; 278 | } 279 | 280 | export function topInside(labelSize: shapesInterfaces.ISize, parentRect: IRect, offset: number): IRect { 281 | return { 282 | left: parentRect.left + parentRect.width / 2.0 - labelSize.width / 2.0, 283 | top: parentRect.top + offset, 284 | width: labelSize.width, 285 | height: labelSize.height 286 | }; 287 | } 288 | 289 | export function bottomInside(labelSize: shapesInterfaces.ISize, parentRect: IRect, offset: number): IRect { 290 | return { 291 | left: parentRect.left + parentRect.width / 2.0 - labelSize.width / 2.0, 292 | top: (parentRect.top + parentRect.height) - offset - labelSize.height, 293 | width: labelSize.width, 294 | height: labelSize.height 295 | }; 296 | } 297 | 298 | export function rightInside(labelSize: shapesInterfaces.ISize, parentRect: IRect, offset: number): IRect { 299 | return { 300 | left: (parentRect.left + parentRect.width) - labelSize.width - offset, 301 | top: parentRect.top + parentRect.height / 2.0 - labelSize.height / 2.0, 302 | width: labelSize.width, 303 | height: labelSize.height 304 | }; 305 | } 306 | 307 | export function leftInside(labelSize: shapesInterfaces.ISize, parentRect: IRect, offset: number): IRect { 308 | return { 309 | left: parentRect.left + offset, 310 | top: parentRect.top + parentRect.height / 2.0 - labelSize.height / 2.0, 311 | width: labelSize.width, 312 | height: labelSize.height 313 | }; 314 | } 315 | 316 | export function topOutside(labelSize: shapesInterfaces.ISize, parentRect: IRect, offset: number): IRect { 317 | return { 318 | left: parentRect.left + parentRect.width / 2.0 - labelSize.width / 2.0, 319 | top: parentRect.top - labelSize.height - offset, 320 | width: labelSize.width, 321 | height: labelSize.height 322 | }; 323 | } 324 | 325 | export function bottomOutside(labelSize: shapesInterfaces.ISize, parentRect: IRect, offset: number): IRect { 326 | return { 327 | left: parentRect.left + parentRect.width / 2.0 - labelSize.width / 2.0, 328 | top: (parentRect.top + parentRect.height) + offset, 329 | width: labelSize.width, 330 | height: labelSize.height 331 | }; 332 | } 333 | 334 | export function rightOutside(labelSize: shapesInterfaces.ISize, parentRect: IRect, offset: number): IRect { 335 | return { 336 | left: (parentRect.left + parentRect.width) + offset, 337 | top: parentRect.top + parentRect.height / 2.0 - labelSize.height / 2.0, 338 | width: labelSize.width, 339 | height: labelSize.height 340 | }; 341 | } 342 | 343 | export function leftOutside(labelSize: shapesInterfaces.ISize, parentRect: IRect, offset: number): IRect { 344 | return { 345 | left: parentRect.left - labelSize.width - offset, 346 | top: parentRect.top + parentRect.height / 2.0 - labelSize.height / 2.0, 347 | width: labelSize.width, 348 | height: labelSize.height 349 | }; 350 | } 351 | 352 | export function middleHorizontal(labelSize: shapesInterfaces.ISize, parentRect: IRect, offset: number): IRect { 353 | return { 354 | left: parentRect.left + parentRect.width / 2.0 - labelSize.width / 2.0 + offset, 355 | top: parentRect.top + parentRect.height / 2.0 - labelSize.height / 2.0, 356 | width: labelSize.width, 357 | height: labelSize.height 358 | }; 359 | } 360 | 361 | export function middleVertical(labelSize: shapesInterfaces.ISize, parentRect: IRect, offset: number): IRect { 362 | return { 363 | left: parentRect.left + parentRect.width / 2.0 - labelSize.width / 2.0, 364 | top: parentRect.top + parentRect.height / 2.0 - labelSize.height / 2.0 + offset, 365 | width: labelSize.width, 366 | height: labelSize.height 367 | }; 368 | } 369 | -------------------------------------------------------------------------------- /src/dataLabel/dataLabelUtils.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 | // powerbi.visuals 27 | import powerbi from "powerbi-visuals-api"; 28 | import ISelectionId = powerbi.visuals.ISelectionId; 29 | 30 | // powerbi.extensibility.utils.type 31 | import { pixelConverter as PixelConverter } from "powerbi-visuals-utils-typeutils"; 32 | 33 | // powerbi.extensibility.utils.formatting 34 | import * as formatting from "powerbi-visuals-utils-formattingutils"; 35 | 36 | // powerbi.extensibility.utils.formatting 37 | import TextProperties = formatting.interfaces.TextProperties; 38 | import DisplayUnitSystemType = formatting.displayUnitSystemType.DisplayUnitSystemType; 39 | import ValueFormatterOptions = formatting.valueFormatter.ValueFormatterOptions; 40 | import font = formatting.font; 41 | import numberFormat = formatting.formattingService.numberFormat; 42 | import formattingService = formatting.formattingService.formattingService; 43 | import textMeasurementService = formatting.textMeasurementService; 44 | import valueFormatter = formatting.valueFormatter; 45 | 46 | // powerbi.extensibility.utils.svg 47 | import * as svg from "powerbi-visuals-utils-svgutils"; 48 | import ClassAndSelector = svg.CssConstants.ClassAndSelector; 49 | import createClassAndSelector = svg.CssConstants.createClassAndSelector; 50 | 51 | import { Selection, BaseType } from "d3-selection"; 52 | 53 | import { 54 | LabelFormattedTextOptions, 55 | LabelEnabledDataPoint, 56 | VisualDataLabelsSettings, 57 | DrawDefaultLabelsProps, 58 | DataLabelObject, 59 | PointLabelPosition, 60 | PointDataLabelsSettings, 61 | VisualDataLabelsSettingsOptions, 62 | IColumnFormatterCacheManager 63 | } from "./dataLabelInterfaces"; 64 | 65 | import DataLabelManager from "./dataLabelManager"; 66 | 67 | export const maxLabelWidth: number = 50; 68 | export const defaultLabelDensity: string = "50"; 69 | export const DefaultDy: string = "-0.15em"; 70 | export const DefaultFontSizeInPt = 9; 71 | 72 | export const StandardFontFamily = font.Family.regular.css; 73 | export const LabelTextProperties: TextProperties = { 74 | fontFamily: font.Family.regularSecondary.css, 75 | fontSize: PixelConverter.fromPoint(DefaultFontSizeInPt), 76 | fontWeight: "normal", 77 | }; 78 | 79 | export const defaultLabelColor = "#777777"; 80 | export const defaultInsideLabelColor = "#ffffff"; 81 | export const hundredPercentFormat = "0.00 %;-0.00 %;0.00 %"; 82 | 83 | export const defaultLabelPrecision: number = undefined; 84 | const defaultCountLabelPrecision: number = 0; 85 | 86 | const labelGraphicsContextClass: ClassAndSelector = createClassAndSelector("labels"); 87 | const linesGraphicsContextClass: ClassAndSelector = createClassAndSelector("lines"); 88 | const labelsClass: ClassAndSelector = createClassAndSelector("data-labels"); 89 | const lineClass: ClassAndSelector = createClassAndSelector("line-label"); 90 | 91 | const DimmedOpacity = 0.4; 92 | const DefaultOpacity = 1.0; 93 | 94 | interface SelectLabelsProps { 95 | filteredData: LabelEnabledDataPoint[]; 96 | context: Selection; 97 | isDonut?: boolean; 98 | hasAnimation?: boolean; 99 | animationDuration?: number; 100 | } 101 | 102 | function getFillOpacity(selected: boolean, highlight: boolean, hasSelection: boolean, hasPartialHighlights: boolean): number { 103 | if ((hasPartialHighlights && !highlight) || (hasSelection && !selected)) { 104 | return DimmedOpacity; 105 | } 106 | 107 | return DefaultOpacity; 108 | } 109 | 110 | export function updateLabelSettingsFromLabelsObject(labelsObj: DataLabelObject, labelSettings: VisualDataLabelsSettings): void { 111 | if (!labelsObj) { 112 | return; 113 | } 114 | if (labelsObj.show !== undefined) { 115 | labelSettings.show = labelsObj.show; 116 | } 117 | 118 | if (labelsObj.showSeries !== undefined) { 119 | labelSettings.show = labelsObj.showSeries; 120 | } 121 | 122 | if (labelsObj.color !== undefined) { 123 | labelSettings.labelColor = labelsObj.color.solid.color; 124 | } 125 | 126 | if (labelsObj.labelDisplayUnits !== undefined) { 127 | labelSettings.displayUnits = labelsObj.labelDisplayUnits; 128 | } 129 | 130 | if (labelsObj.labelPrecision !== undefined) { 131 | labelSettings.precision = (labelsObj.labelPrecision >= 0) 132 | ? labelsObj.labelPrecision 133 | : defaultLabelPrecision; 134 | } 135 | 136 | if (labelsObj.fontSize !== undefined) { 137 | labelSettings.fontSize = labelsObj.fontSize; 138 | } 139 | 140 | if (labelsObj.showAll !== undefined) { 141 | labelSettings.showLabelPerSeries = labelsObj.showAll; 142 | } 143 | 144 | if (labelsObj.labelStyle !== undefined) { 145 | labelSettings.labelStyle = labelsObj.labelStyle; 146 | } 147 | 148 | if (labelsObj.labelPosition) { 149 | labelSettings.position = labelsObj.labelPosition; 150 | } 151 | } 152 | 153 | export function getDefaultLabelSettings(show: boolean = false, labelColor?: string, fontSize?: number): VisualDataLabelsSettings { 154 | return { 155 | show: show, 156 | position: PointLabelPosition.Above, 157 | displayUnits: 0, 158 | precision: defaultLabelPrecision, 159 | labelColor: labelColor || defaultLabelColor, 160 | fontSize: fontSize || DefaultFontSizeInPt, 161 | }; 162 | } 163 | 164 | /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ 165 | export function getDefaultColumnLabelSettings(isLabelPositionInside: boolean): VisualDataLabelsSettings { 166 | const labelSettings = getDefaultLabelSettings(false, undefined); 167 | 168 | labelSettings.position = null; 169 | labelSettings.labelColor = undefined; 170 | 171 | return labelSettings; 172 | } 173 | 174 | export function getDefaultPointLabelSettings(): PointDataLabelsSettings { 175 | return { 176 | show: false, 177 | position: PointLabelPosition.Above, 178 | displayUnits: 0, 179 | precision: defaultLabelPrecision, 180 | labelColor: defaultLabelColor, 181 | fontSize: DefaultFontSizeInPt, 182 | }; 183 | } 184 | 185 | export function getLabelPrecision(precision: number, format: string): number { 186 | if (precision !== defaultLabelPrecision) { 187 | return precision; 188 | } 189 | 190 | if (format === "g" || format === "G") { 191 | return; 192 | } 193 | 194 | if (format) { 195 | // Calculate precision from positive format by default 196 | const positiveFormat = numberFormat.getComponents(format).positive, 197 | formatMetadata = numberFormat.getCustomFormatMetadata(positiveFormat, true /*calculatePrecision*/); 198 | 199 | if (formatMetadata.hasDots) { 200 | return formatMetadata.precision; 201 | } 202 | } 203 | 204 | // For count fields we do not want a precision by default 205 | return defaultCountLabelPrecision; 206 | } 207 | 208 | export function drawDefaultLabelsForDataPointChart({ 209 | data, 210 | context, 211 | layout, 212 | viewport, 213 | isAnimator, 214 | animationDuration, 215 | hasSelection, 216 | hideCollidedLabels = true 217 | }: DrawDefaultLabelsProps ): Selection { 218 | 219 | // Hide and reposition labels that overlap 220 | const dataLabelManager = new DataLabelManager(); 221 | const filteredData = dataLabelManager.hideCollidedLabels(viewport, data, layout, false, hideCollidedLabels); 222 | const hasAnimation: boolean = isAnimator && !!animationDuration; 223 | const selectedLabels: Selection = selectLabels({filteredData, context, hasAnimation, animationDuration}); 224 | if (!selectedLabels) { 225 | return; 226 | } 227 | 228 | // Draw default labels 229 | selectedLabels 230 | .text((d: LabelEnabledDataPoint) => d.labeltext) 231 | .attr("x", (d: LabelEnabledDataPoint) => d.labelX) 232 | .attr("y", (d: LabelEnabledDataPoint) => d.labelY) 233 | 234 | if (hasAnimation) { 235 | // Add opacity animation 236 | selectedLabels 237 | .transition("") 238 | .duration(animationDuration) 239 | .style("opacity", hasSelection ? (d => getFillOpacity(d.selected, false, hasSelection, false))() : 1) 240 | } else { 241 | // Set opacity to default 242 | selectedLabels 243 | .style(layout.style.toString()); 244 | } 245 | if (layout?.style) { 246 | Object.keys(layout.style).forEach(style => selectedLabels.style(style, layout.style[style])); 247 | } 248 | 249 | return selectedLabels; 250 | } 251 | 252 | function selectLabels({ 253 | filteredData, 254 | context, 255 | isDonut, 256 | hasAnimation, 257 | animationDuration 258 | }: SelectLabelsProps): Selection { 259 | // Guard for a case where resizing leaves no labels - then we need to remove the labels "g" 260 | if (!filteredData.length) { 261 | cleanDataLabels(context, true); 262 | return null; 263 | } 264 | 265 | if (context.select(labelGraphicsContextClass.selectorName).empty()) { 266 | context.append("g").classed(labelGraphicsContextClass.className, true); 267 | } 268 | 269 | // line chart ViewModel has a special "key" property for point identification since the "identity" field is set to the series identity 270 | const hasKey: boolean = (filteredData)[0].key != null; 271 | const hasDataPointIdentity: boolean = (filteredData)[0].identity != null; 272 | let getIdentifier; 273 | switch (true) { 274 | case hasKey: 275 | getIdentifier = (d: any) => d.key; 276 | break; 277 | case hasDataPointIdentity: 278 | getIdentifier = d => (d.identity as ISelectionId).getKey(); 279 | break; 280 | case isDonut: 281 | getIdentifier = d => d.data.identity.getKey(); 282 | break; 283 | } 284 | 285 | const labels: Selection = context 286 | .select(labelGraphicsContextClass.selectorName) 287 | .selectAll(labelsClass.selectorName) 288 | .data(filteredData, getIdentifier) 289 | 290 | if (hasAnimation) { 291 | labels 292 | .exit() 293 | .transition() 294 | .duration(animationDuration) 295 | .style("opacity", 0) // fade out labels that are removed 296 | .remove(); 297 | } else { 298 | labels.exit().remove(); 299 | } 300 | 301 | const allLabels = labels.enter() 302 | .append("text") 303 | .classed(labelsClass.className, true) 304 | .merge(labels); 305 | 306 | if (hasAnimation) { 307 | allLabels.style("opacity", 0); 308 | } 309 | 310 | return allLabels; 311 | } 312 | 313 | export function cleanDataLabels( 314 | context: Selection, 315 | removeLines: boolean = false 316 | ): void { 317 | const emptyData = [] 318 | const labels = context.selectAll(labelsClass.selectorName).data(emptyData); 319 | 320 | labels 321 | .exit() 322 | .remove(); 323 | 324 | context 325 | .selectAll(labelGraphicsContextClass.selectorName) 326 | .remove(); 327 | 328 | if (removeLines) { 329 | const lines = context 330 | .selectAll(lineClass.selectorName) 331 | .data(emptyData); 332 | 333 | lines 334 | .exit() 335 | .remove(); 336 | 337 | context 338 | .selectAll(linesGraphicsContextClass.selectorName) 339 | .remove(); 340 | } 341 | } 342 | 343 | export function setHighlightedLabelsOpacity(context: Selection, hasSelection: boolean, hasHighlights: boolean) { 344 | context 345 | .selectAll(labelsClass.selectorName) 346 | .style("fill-opacity", (d: any) => { 347 | const fillOpacity =getFillOpacity( 348 | d.selected, 349 | d.highlight, 350 | !d.highlight && hasSelection, 351 | !d.selected && hasHighlights 352 | ) 353 | const labelOpacity = fillOpacity < 1 ? 0 : 1; 354 | 355 | return labelOpacity; 356 | }); 357 | } 358 | 359 | export function getLabelFormattedText(options: LabelFormattedTextOptions): string { 360 | const properties: TextProperties = { 361 | text: options.formatter?.format(options.label) || formattingService.formatValue(options.label, options.format), 362 | fontFamily: LabelTextProperties.fontFamily, 363 | fontSize: PixelConverter.fromPoint(options.fontSize), 364 | fontWeight: LabelTextProperties.fontWeight, 365 | }; 366 | 367 | return textMeasurementService.getTailoredTextOrDefault( 368 | properties, 369 | options.maxWidth ?? maxLabelWidth 370 | ); 371 | } 372 | 373 | export function enumerateDataLabels( 374 | options: VisualDataLabelsSettingsOptions): powerbi.VisualObjectInstance { 375 | 376 | if (!options.dataLabelsSettings) { 377 | return; 378 | } 379 | 380 | const instance: powerbi.VisualObjectInstance = { 381 | objectName: "labels", 382 | selector: options.selector, 383 | properties: {}, 384 | }; 385 | 386 | if (options.show && options.selector) { 387 | instance.properties["showSeries"] = options.dataLabelsSettings.show; 388 | } else if (options.show) { 389 | instance.properties["show"] = options.dataLabelsSettings.show; 390 | } 391 | 392 | instance.properties["color"] = options.dataLabelsSettings.labelColor || defaultLabelColor; 393 | 394 | if (options.displayUnits) { 395 | instance.properties["labelDisplayUnits"] = options.dataLabelsSettings.displayUnits; 396 | } 397 | 398 | if (options.precision) { 399 | const precision = options.dataLabelsSettings.precision; 400 | instance.properties["labelPrecision"] = precision === defaultLabelPrecision ? null : precision; 401 | } 402 | 403 | if (options.position) { 404 | instance.properties["labelPosition"] = options.dataLabelsSettings.position; 405 | if (options.positionObject) { 406 | 407 | instance.validValues = { "labelPosition": options.positionObject }; 408 | } 409 | } 410 | if (options.labelStyle) { 411 | instance.properties["labelStyle"] = options.dataLabelsSettings.labelStyle; 412 | } 413 | 414 | if (options.fontSize) { 415 | instance.properties["fontSize"] = options.dataLabelsSettings.fontSize; 416 | } 417 | 418 | if (options.labelDensity) { 419 | const lineChartSettings = options.dataLabelsSettings; 420 | 421 | if (lineChartSettings) { 422 | instance.properties["labelDensity"] = lineChartSettings.labelDensity; 423 | } 424 | } 425 | 426 | // Keep show all as the last property of the instance. 427 | if (options.showAll) { 428 | instance.properties["showAll"] = options.dataLabelsSettings.showLabelPerSeries; 429 | } 430 | 431 | options.instances.push(instance); 432 | 433 | return instance; 434 | } 435 | 436 | export function enumerateCategoryLabels( 437 | enumeration: powerbi.VisualObjectInstanceEnumerationObject, 438 | dataLabelsSettings: VisualDataLabelsSettings, 439 | withFill: boolean, 440 | isShowCategory: boolean = false, 441 | fontSize?: number 442 | ): void { 443 | 444 | const labelSettings = (dataLabelsSettings) 445 | ? dataLabelsSettings 446 | : getDefaultPointLabelSettings(); 447 | 448 | const instance: powerbi.VisualObjectInstance = { 449 | objectName: "categoryLabels", 450 | selector: null, 451 | properties: { 452 | show: isShowCategory 453 | ? labelSettings.showCategory 454 | : labelSettings.show, 455 | fontSize: dataLabelsSettings ? dataLabelsSettings.fontSize : DefaultFontSizeInPt, 456 | }, 457 | }; 458 | 459 | if (withFill) { 460 | instance.properties["color"] = labelSettings.categoryLabelColor 461 | ? labelSettings.categoryLabelColor 462 | : labelSettings.labelColor; 463 | } 464 | 465 | if (fontSize) { 466 | instance.properties["fontSize"] = fontSize; 467 | } 468 | 469 | enumeration.instances.push(instance); 470 | } 471 | 472 | export function createColumnFormatterCacheManager(): IColumnFormatterCacheManager { 473 | return { 474 | cache: { defaultFormatter: null }, 475 | getOrCreate(formatString: string, labelSetting: VisualDataLabelsSettings, value2?: number) { 476 | if (formatString) { 477 | const cacheKeyObject = { 478 | formatString: formatString, 479 | displayUnits: labelSetting.displayUnits, 480 | precision: getLabelPrecision(labelSetting.precision, formatString), 481 | value2: value2 482 | }; 483 | 484 | const cacheKey = JSON.stringify(cacheKeyObject); 485 | 486 | if (!this.cache[cacheKey]) { 487 | this.cache[cacheKey] = valueFormatter.create(getOptionsForLabelFormatter( 488 | labelSetting, 489 | formatString, 490 | value2, 491 | cacheKeyObject.precision)); 492 | } 493 | 494 | return this.cache[cacheKey]; 495 | } 496 | 497 | if (!this.cache.defaultFormatter) { 498 | this.cache.defaultFormatter = valueFormatter.create(getOptionsForLabelFormatter( 499 | labelSetting, 500 | formatString, 501 | value2, 502 | labelSetting.precision)); 503 | } 504 | 505 | return this.cache.defaultFormatter; 506 | } 507 | }; 508 | } 509 | 510 | export function getOptionsForLabelFormatter( 511 | labelSetting: VisualDataLabelsSettings, 512 | formatString: string, 513 | value2?: number, 514 | precision?: number 515 | ): ValueFormatterOptions { 516 | 517 | return { 518 | displayUnitSystemType: DisplayUnitSystemType.DataLabels, 519 | format: formatString, 520 | precision: precision, 521 | value: labelSetting.displayUnits, 522 | value2: value2, 523 | allowFormatBeautification: true, 524 | }; 525 | } 526 | 527 | export function isTextWidthOverflows(textWidth, maxTextWidth): boolean { 528 | return textWidth > maxTextWidth; 529 | } 530 | 531 | export function isTextHeightOverflows(textHeight, innerChordLength): boolean { 532 | return textHeight > innerChordLength; 533 | } 534 | --------------------------------------------------------------------------------