├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── build.yml │ └── codeql-analysis.yml ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── README.md ├── SECURITY.md ├── Tutorial ├── ColorPalette.md ├── ConditionalFormatting.md ├── DataBinding.md ├── DataBoundObjects.md ├── ExtensibilityUtils.md ├── ExternalLibraries.md ├── HighContrastSupport.md ├── LaunchURL.md ├── Locale.md ├── OnObject.md ├── ReportPageTooltips.md ├── Selection.md ├── StaticObjects.md ├── StaticVisual.md ├── Typings.md └── images │ ├── AddTypings.png │ ├── ApplyReportPageTooltip.png │ ├── CondFormatSupport.png │ ├── ConditionalFormattingEntry.png │ ├── DataBinding.png │ ├── ExternalLibraries.png │ ├── HC_sampleBarChart_dark2.png │ ├── HC_sampleBarChart_standard.png │ ├── HC_sampleBarChart_white.png │ ├── InstallTypings.png │ ├── LocaleInSampleBarChart.png │ ├── MakeExternalsDirectory.png │ ├── ObjectDataBoundProperty.png │ ├── ObjectShowProperty.png │ ├── PropertyPane.png │ ├── ReportPageTooltip.png │ ├── SampleBarChart.png │ ├── UsingTypings.png │ └── launchURLtoggle.png ├── assets └── icon.png ├── azure-pipelines.yml ├── capabilities.json ├── package-lock.json ├── package.json ├── pbiviz.json ├── src ├── barChart.ts └── barChartSettingsModel.ts ├── stringResources ├── ar-SA │ └── resources.resjson ├── bg-BG │ └── resources.resjson ├── ca-ES │ └── resources.resjson ├── cs-CZ │ └── resources.resjson ├── da-DK │ └── resources.resjson ├── de-DE │ └── resources.resjson ├── el-GR │ └── resources.resjson ├── en-US │ └── resources.resjson ├── es-ES │ └── resources.resjson ├── et-EE │ └── resources.resjson ├── eu-ES │ └── resources.resjson ├── fi-FI │ └── resources.resjson ├── fr-FR │ └── resources.resjson ├── gl-ES │ └── resources.resjson ├── he-IL │ └── resources.resjson ├── hi-IN │ └── resources.resjson ├── hr-HR │ └── resources.resjson ├── hu-HU │ └── resources.resjson ├── id-ID │ └── resources.resjson ├── it-IT │ └── resources.resjson ├── ja-JP │ └── resources.resjson ├── kk-KZ │ └── resources.resjson ├── ko-KR │ └── resources.resjson ├── lt-LT │ └── resources.resjson ├── lv-LV │ └── resources.resjson ├── ms-MY │ └── resources.resjson ├── nb-NO │ └── resources.resjson ├── nl-NL │ └── resources.resjson ├── pl-PL │ └── resources.resjson ├── pt-BR │ └── resources.resjson ├── pt-PT │ └── resources.resjson ├── ro-RO │ └── resources.resjson ├── ru-RU │ └── resources.resjson ├── sk-SK │ └── resources.resjson ├── sl-SI │ └── resources.resjson ├── sr-Cyrl-RS │ └── resources.resjson ├── sr-Latn-RS │ └── resources.resjson ├── sv-SE │ └── resources.resjson ├── th-TH │ └── resources.resjson ├── tr-TR │ └── resources.resjson ├── uk-UA │ └── resources.resjson ├── vi-VN │ └── resources.resjson ├── zh-CN │ └── resources.resjson └── zh-TW │ └── resources.resjson ├── style └── visual.less └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | test 5 | .eslintrc.js 6 | karma.conf.ts 7 | test.webpack.config.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | "browser": true, 4 | "es6": true, 5 | "es2017": true 6 | }, 7 | root: true, 8 | parser: "@typescript-eslint/parser", 9 | parserOptions: { 10 | project: "tsconfig.json", 11 | tsconfigRootDir: ".", 12 | }, 13 | plugins: [ 14 | "powerbi-visuals" 15 | ], 16 | extends: [ 17 | "plugin:powerbi-visuals/recommended" 18 | ], 19 | rules: {} 20 | }; -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install 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-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18.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 ci 28 | - run: npm run lint 29 | - run: npm test 30 | env: 31 | CI: true 32 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '0 0 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | timeout-minutes: 60 28 | permissions: 29 | actions: read 30 | contents: read 31 | security-events: write 32 | 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | language: [ 'typescript' ] 37 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 38 | # Learn more: 39 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v4 44 | with: 45 | fetch-depth: 2 46 | 47 | - name: Use Node.js 18 48 | uses: actions/setup-node@v2 49 | with: 50 | node-version: 18.x 51 | 52 | - name: Install Dependencies 53 | run: npm ci 54 | 55 | # Initializes the CodeQL tools for scanning. 56 | - name: Initialize CodeQL 57 | uses: github/codeql-action/init@v3 58 | with: 59 | languages: ${{ matrix.language }} 60 | # If you wish to specify custom queries, you can do so here or in a config file. 61 | # By default, queries listed here will override any specified in a config file. 62 | # Prefix the list here with "+" to use these queries and those in the config file. 63 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 64 | 65 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 66 | # If this step fails, then you should remove it and run the build manually (see below) 67 | - name: Autobuild 68 | uses: github/codeql-action/autobuild@v3 69 | 70 | # ℹ️ Command-line programs to run using the OS shell. 71 | # 📚 https://git.io/JvXDl 72 | 73 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 74 | # and modify them (or add more) to build your code if your project 75 | # uses a compiled language 76 | 77 | #- run: | 78 | # make bootstrap 79 | # make release 80 | 81 | - name: Perform CodeQL Analysis 82 | uses: github/codeql-action/analyze@v3 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | typings 2 | node_modules 3 | .DS_Store 4 | .tmp 5 | dist 6 | .api 7 | 8 | webpack.statistics.*.html -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | sudo: required 4 | dist: trusty 5 | language: node_js 6 | node_js: 7 | - "10" 8 | install: 9 | - npm install 10 | script: 11 | - npm run lint 12 | - npm run test 13 | after_success: 14 | - node node_modules/coveralls/bin/coveralls.js < coverage/lcov.info 15 | notifications: 16 | email: false 17 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "configurations": [ 4 | { 5 | "name": "Debugger", 6 | "type": "chrome", 7 | "request": "attach", 8 | "port": 9222, 9 | "sourceMaps": true, 10 | "webRoot": "${cwd}/" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 4, 3 | "editor.insertSpaces": true, 4 | "files.eol": "\n", 5 | "files.watcherExclude": { 6 | "**/.git/objects/**": true, 7 | "**/node_modules/**": true, 8 | ".tmp": true 9 | }, 10 | "files.exclude": { 11 | ".tmp": true 12 | }, 13 | "search.exclude": { 14 | ".tmp": true, 15 | "typings": true 16 | }, 17 | "json.schemas": [ 18 | { 19 | "fileMatch": [ 20 | "/pbiviz.json" 21 | ], 22 | "url": "./.api/v2.5.0/schema.pbiviz.json" 23 | }, 24 | { 25 | "fileMatch": [ 26 | "/capabilities.json" 27 | ], 28 | "url": "./.api/v2.5.0/schema.capabilities.json" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - Sample Bar Chart 2 | 3 | This page contains information about changes to the 'Sample bar chart' Power BI visual. 4 | 5 | ## 4.1.1.0 6 | 7 | * Added localizationManager 8 | * Replaced objectEnumerationUtility with dataviewUtils 9 | 10 | ## 4.1.0.0 11 | 12 | * Added onObject support 13 | * Added formatting model 14 | 15 | ## 4.0.0 16 | 17 | * Added *dynamic format* feature support 18 | * Migrated to ESlint 19 | * Updated dependencies 20 | 21 | ## 3.2.0 22 | 23 | * Added *modern tooltip* feature 24 | * Updated dependencies 25 | 26 | ## 3.0.0 27 | 28 | * Webpack integration 29 | * Azure Pipelines integration 30 | * API 2.6.0 31 | * updated powerbi-visuals-utils, powerbi-visuals-tools 3.x.x 32 | * d3 v5 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerBI-visuals-sampleBarChart 2 | [![Build Status](https://travis-ci.org/Microsoft/PowerBI-visuals-samplebarchart.svg?branch=master)](https://travis-ci.org/Microsoft/PowerBI-visuals-samplebarchart) 3 | Bar Chart Custom Visual sample. 4 | 5 | 6 | ### Setting Up Environment 7 | 8 | Before starting creating your first custom visual follow by [this](https://github.com/Microsoft/PowerBI-visuals/blob/master/Readme.md#setting-up-environment) 9 | setting up environment instruction. 10 | 11 | 12 | ### Install dev dependencies: 13 | 14 | Once you have cloned this example, run these commands to install dependencies and to connect the visual into powerbi. 15 | 16 | ``` 17 | npm install # This command will install all necessary modules 18 | ``` 19 | 20 | ### Start dev app 21 | ``` 22 | pbiviz start 23 | ``` 24 | 25 | ### Building Bar Chart 26 | 1. [Building a Visual with Static Data](Tutorial/StaticVisual.md) 27 | 2. [Adding Databinding to the Bar Chart](Tutorial/DataBinding.md) 28 | 3. [Adding Color to the Bar Chart](Tutorial/ColorPalette.md) 29 | 4. [Adding Selection and Interaction with Other Visuals](Tutorial/Selection.md) 30 | 5. [Adding Static Objects to Property Pane](Tutorial/StaticObjects.md) 31 | 6. [Adding Databound Objects to Property Pane](Tutorial/DataBoundObjects.md) 32 | 7. [Adding Powerbi Extensibility Utils](Tutorial/ExtensibilityUtils.md) 33 | 8. [Adding URL Launcher element to the Bar Chart](Tutorial/LaunchURL.md) 34 | 9. [Adding Report Page tooltips support to the Bar Chart](Tutorial/ReportPageTooltips.md) 35 | 10. [Adding Conditional Formatting support to the Bar Chart](Tutorial/ConditionalFormatting.md) 36 | 11. [Accessibility: Adding High-Contrast Mode Support](Tutorial/HighContrastSupport.md) 37 | 12. [Finally Package for Distribution ... Done](https://github.com/Microsoft/PowerBI-visuals/blob/master/tools/usage.md#packaging-your-visual-for-distribution) 38 | 39 | 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Tutorial/ColorPalette.md: -------------------------------------------------------------------------------- 1 | # Adding Color to your Visual 2 | Color is exposed as one of the services available on `IVisualHost`. 3 | 4 | See [commit](https://github.com/Microsoft/PowerBI-visuals-sampleBarChart/commit/a521bc6b9930f630861dc08e27330030766ae057) for what was added at this step. 5 | 6 | ## Add Color to Data Points 7 | Each data point will be represented by a different color. Add color to the BarChartDataPoint interface. 8 | 9 | ```typescript 10 | /** 11 | * Interface for BarChart data points. 12 | * 13 | * @interface 14 | * @property {number} value - Data value for point. 15 | * @property {string} category - Corresponding category of data value. 16 | * @property {string} color - Color corresponding to data point. 17 | */ 18 | interface BarChartDataPoint { 19 | value: number; 20 | category: string; 21 | color: string; 22 | }; 23 | ``` 24 | 25 | ## Color Palette 26 | `colorPalette` is a service that manages the colors used on your visual. An instance of it is available on `IVisualHost`. 27 | 28 | ## Assigning Color to Data Points 29 | We defined `createSelectorDataPoints` as a construct to convert options `dataView` to Bar Chart data points that will be used in visual view. 30 | Since we iterate through the data points in `createSelectorDataPoints` it is also the ideal place to assign colors. 31 | 32 | ```typescript 33 | 34 | function createSelectorDataPoints(options: VisualUpdateOptions, host: IVisualHost): BarChartDataPoint[] { 35 | let barChartDataPoints: BarChartDataPoint[] = [] 36 | const dataViews = options.dataViews; 37 | if (!dataViews 38 | || !dataViews[0] 39 | || !dataViews[0].categorical 40 | || !dataViews[0].categorical.categories 41 | || !dataViews[0].categorical.categories[0].source 42 | || !dataViews[0].categorical.values 43 | ) { 44 | return barChartDataPoints; 45 | } 46 | 47 | const categorical = dataViews[0].categorical; 48 | const category = categorical.categories[0]; 49 | const dataValue = categorical.values[0]; 50 | 51 | const colorPalette: ISandboxExtendedColorPalette = host.colorPalette; 52 | const strokeColor: string = getColumnStrokeColor(colorPalette); 53 | const strokeWidth: number = getColumnStrokeWidth(colorPalette.isHighContrast); 54 | 55 | for (let i = 0, len = Math.max(category.values.length, dataValue.values.length); i < len; i++) { 56 | const color: string = getColumnColorByIndex(category, i, colorPalette); 57 | 58 | const selectionId: ISelectionId = host.createSelectionIdBuilder() 59 | .withCategory(category, i) 60 | .createSelectionId(); 61 | 62 | barChartDataPoints.push({ 63 | color, 64 | strokeColor, 65 | strokeWidth, 66 | selectionId, 67 | value: dataValue.values[i], 68 | category: `${category.values[i]}`, 69 | }); 70 | } 71 | return barChartDataPoints; 72 | } 73 | ``` -------------------------------------------------------------------------------- /Tutorial/ConditionalFormatting.md: -------------------------------------------------------------------------------- 1 | # Adding conditional formatting to your Visual 2 | [Conditional formatting](https://docs.microsoft.com/en-us/power-bi/visuals/service-tips-and-tricks-for-color-formatting#conditional-formatting-for-visualizations) of custom formatting properties is supported by setting formatting property `instanceKind` in `getFormattingModel` method. 3 | For more info on conditional formatting click [here](https://learn.microsoft.com/en-us/power-bi/developer/visuals/conditional-format?tabs=getFormattingModel) 4 | 5 | Conditional formatting can only be applied to the following property types: 6 | * Color 7 | * Text 8 | * Icon 9 | * Web URL 10 | 11 | ## Add a conditional color formatting entry in the format pane 12 | To add the conditional color formatting button in the format pane for the desired object, under the `getFormattingModel` method, make the following change: 13 | 14 | Define `instanceKind` property of required formatting property `descriptor` with the appropriate value. 15 | Use `VisualEnumerationInstanceKinds` enum to declare the type of the desired format (constant, rule or both). 16 | 17 | ```typescript 18 | // List your conditional formatting properties 19 | instanceKind: powerbi.VisualEnumerationInstanceKinds.ConstantOrRule 20 | ``` 21 | ![](images/ConditionalFormattingEntry.png) 22 | 23 | ## Define how conditional formatting behaves 24 | Using `createDataViewWildcardSelector` declared under `powerbi-visuals-utils-dataviewutils`, specify whether conditional formatting will be applied to instances, totals, or both. For more information, see [DataViewWildcard](https://docs.microsoft.com/en-us/power-bi/developer/visuals/utils-dataview#dataviewwildcard). 25 | 26 | In `BarChartFormattingSettingsModel`, make the following changes to the formatting properties you want to apply conditional formatting to: 27 | 28 | * Replace the formatting property `descriptor`'s `selector` value with a `dataViewWildcard.createDataViewWildcardSelector()` call. Specify the desired option from `DataViewWildcardMatchingOption` enum to define whether conditional formatting is applied to instances, totals, or both. 29 | 30 | * Add the `altConstantValueSelector` property having the value previously defined for the `selector` property. 31 | 32 | ```typescript 33 | // Define whether the conditional formatting will apply to instances, totals, or both 34 | selector: dataViewWildcard.createDataViewWildcardSelector(dataViewWildcard.DataViewWildcardMatchingOption.InstancesAndTotals), 35 | 36 | // Add this property with the value previously defined for the selector property 37 | altConstantValueSelector: barDataPoint.selectionId.getSelector() 38 | ``` 39 | See [commit](https://github.com/Microsoft/PowerBI-visuals-sampleBarChart) for how conditional formatting was applied to sample bar chart. 40 | 41 | ![](images/CondFormatSupport.png) -------------------------------------------------------------------------------- /Tutorial/DataBinding.md: -------------------------------------------------------------------------------- 1 | # Adding Databinding to Bar Chart 2 | Databinding can be done by defining your visual capabilities. 3 | See [commit](https://github.com/Microsoft/PowerBI-visuals-sampleBarChart/commit/3c6e8186436b63bf0cf97d2cdd5dde8aa8d08709) for what was added at this step. 4 | 5 | To add databinding, all changes will be located in `capabilities.json`. A sample schema is already in place for you. 6 | 7 | Databinding can be done on the field well. 8 | 9 | ![](images/DataBinding.png) 10 | 11 | ## Adding Data Roles 12 | Currently, data roles are added for you, but customizations can still be made. 13 | 14 | `displayName` is the name shown on the field well. 15 | `name` is the internal name used to reference this data role. 16 | 17 | `0` for the kind property refers to the grouping or category. Groupings resemble a discrete number of values. 18 | `1` for the kind property refers to the values for each of the groupings. 19 | 20 | ```json 21 | "dataRoles": [ 22 | { 23 | "displayName": "Category Data", 24 | "name": "category", 25 | "kind": 0 26 | }, 27 | { 28 | "displayName": "Measure Data", 29 | "name": "measure", 30 | "kind": 1 31 | } 32 | ], 33 | ``` 34 | 35 | For more information, see the section about [Data Roles](https://github.com/Microsoft/PowerBI-visuals/blob/master/Capabilities/Capabilities.md#define-the-data-fields-your-visual-expects---dataroles). 36 | 37 | ## Adding Conditions to DataViewMapping 38 | Define conditions within your dataViewMappings to determine how many fields can be bound for each field well. 39 | Use the internal `name` defined in your dataRoles to reference each field. 40 | 41 | ```json 42 | "dataViewMappings": [ 43 | { 44 | "conditions": [ 45 | { 46 | "category": { 47 | "max": 1 48 | }, 49 | "measure": { 50 | "max": 1 51 | } 52 | } 53 | ], 54 | } 55 | ] 56 | ``` 57 | 58 | For more information, see the section about [Data View Mapping](https://github.com/Microsoft/PowerBI-visuals/blob/master/Capabilities/DataViewMappings.md). 59 | 60 | ## Defining and Using `createSelectorDataPoints` 61 | DataView is the structure that PowerBI provides to your visual and it contains the queried data to be visualized. 62 | However, DataView provides your data in different forms such as categorical and table forms. In this instance we're building a categorical visual and we will only need the use the categorical property on the DataView. 63 | 64 | Defining `createSelectorDataPoints` will allow you to convert options dataView into bar chart data points your visual will use. 65 | IVisualHost is required because when defining individual data points, you will want to assign colors and selection to them. 66 | 67 | ```typescript 68 | /** 69 | * Function that converts queried data into bar chart data points that will be used by the visual 70 | * 71 | * @function 72 | * @param {VisualUpdateOptions} options - Contains references to the size of the container 73 | * and the dataView which contains all the data 74 | * the visual had queried. 75 | * @param {IVisualHost} host - Contains references to the host which contains services 76 | */ 77 | function createSelectorDataPoints(options: VisualUpdateOptions, host: IVisualHost): BarChartDataPoint[] { 78 | /*Convert dataView to bar chart data points*/ 79 | } 80 | 81 | ``` -------------------------------------------------------------------------------- /Tutorial/DataBoundObjects.md: -------------------------------------------------------------------------------- 1 | # Databound Objects 2 | Databound objects are similar to static objects, however they typically deal with data selection. 3 | We will be changing the color associated with the data point. 4 | 5 | ![](images/ObjectDataBoundProperty.png) 6 | 7 | See [commit](https://github.com/Microsoft/PowerBI-visuals-sampleBarChart/commit/3018a4ef020ee5de8a87be5f29f008bd5cf8fe63) for what was added at this step. 8 | 9 | ## Define Object in Capabilities 10 | Similar to static objects, we will define another object in the capabilities 11 | `colorSelector` is the internal name that will be referenced in the `dataView`. 12 | 13 | `fill` is a `StructuralObjectValue` and is not associated with a primitive type. 14 | 15 | ```typescript 16 | "colorSelector": { 17 | "properties": { 18 | "fill": { 19 | "type": { 20 | "fill": { 21 | "solid": { 22 | "color": true 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | For more information, see the section about using [Objects](../Capabilities/Objects.md). 32 | 33 | ## Using Object Enumeration Utility 34 | Similarly with static objects, we will need to retrieve object details from the `dataView`. However, instead of the object values being within metadata, the object values are associated with each category. 35 | 36 | ```typescript 37 | /** 38 | * Gets property value for a particular object in a category. 39 | * 40 | * @function 41 | * @param {DataViewCategoryColumn} category - List of category objects. 42 | * @param {number} index - Index of category object. 43 | * @param {string} objectName - Name of desired object. 44 | * @param {string} propertyName - Name of desired property. 45 | * @param {T} defaultValue - Default value of desired property. 46 | */ 47 | export function getCategoricalObjectValue(category: DataViewCategoryColumn, index: number, objectName: string, propertyName: string, defaultValue: T): T { 48 | let categoryObjects = category.objects; 49 | 50 | if(categoryObjects) { 51 | let categoryObject: DataViewObject = categoryObjects[index]; 52 | if(categoryObject) { 53 | let object = categoryObject[objectName]; 54 | if(object) { 55 | let property: T = object[propertyName]; 56 | if(property !== undefined) { 57 | return property; 58 | } 59 | } 60 | } 61 | } 62 | return defaultValue; 63 | } 64 | ``` 65 | 66 | See [objectEnumerationUtility.ts](https://github.com/Microsoft/PowerBI-visuals-sampleBarChart/blob/master/src/objectEnumerationUtility.ts) for source code. 67 | 68 | ## Defining Default Color and Retrieving Categorical Object from DataView 69 | Each color is now associated with each category inside options dataView. We will set each data point to its corresponding color. 70 | 71 | ```typescript 72 | const strokeWidth: number = getColumnStrokeWidth(colorPalette.isHighContrast); 73 | 74 | for (let i = 0, len = Math.max(category.values.length, dataValue.values.length); i < len; i++) { 75 | const color: string = getColumnColorByIndex(category, i, colorPalette); 76 | 77 | const selectionId: ISelectionId = host.createSelectionIdBuilder() 78 | .withCategory(category, i) 79 | .createSelectionId(); 80 | 81 | barChartDataPoints.push({ 82 | color, 83 | strokeColor, 84 | strokeWidth, 85 | selectionId, 86 | value: dataValue.values[i], 87 | category: `${category.values[i]}`, 88 | }); 89 | } 90 | ``` 91 | 92 | ## Populate Property Pane with `getFormattingModel` 93 | `getFormattingModel` is used to populate the property pane with objects. 94 | For more information [here](https://learn.microsoft.com/en-us/power-bi/developer/visuals/format-pane) 95 | 96 | For this instance, we would like a color picker per category we have. Each category be rendered on the property pane. 97 | We will do this by adding a populate method `populateColorSelector` to create corresponding bar chart data points color selector in format pane after building the data points in `update` method. This `populateColorSelector` method iterate through each data point with the associated color. 98 | 99 | Selection is required to associate the color with a data point. 100 | In visual class: 101 | ```typescript 102 | public update(options: VisualUpdateOptions) { 103 | this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(BarChartSettingsModel, options.dataViews); 104 | this.barDataPoints = createSelectorDataPoints(options, this.host); 105 | this.formattingSettings.populateColorSelector(this.barDataPoints); 106 | 107 | // ... 108 | } 109 | ``` 110 | 111 | In formatting settings model: 112 | ```typescript 113 | /** 114 | * populate colorSelector object categories formatting properties 115 | * @param dataPoints 116 | */ 117 | populateColorSelector(dataPoints: BarChartDataPoint[]) { 118 | const slices: formattingSettings.ColorPicker[] = this.colorSelector.slices; 119 | if (dataPoints) { 120 | dataPoints.forEach(dataPoint => { 121 | slices.push(new formattingSettings.ColorPicker({ 122 | name: "fill", 123 | displayName: dataPoint.category, 124 | value: { value: dataPoint.color }, 125 | selector: dataPoint.selectionId.getSelector(), 126 | })); 127 | }); 128 | } 129 | } 130 | ``` -------------------------------------------------------------------------------- /Tutorial/ExtensibilityUtils.md: -------------------------------------------------------------------------------- 1 | # Powerbi Extensibility Utils 2 | PowerBI provides several tools that help to cover the main needs to build your own visual. 3 | 4 | ### NPM packages 5 | 1. [DataViewUtils](https://www.npmjs.com/package/powerbi-visuals-utils-dataviewutils) is a set of functions and classes in order to simplify parsing of the DataView objects for PowerBI custom visuals. 6 | 2. [ChartUtils](https://www.npmjs.com/package/powerbi-visuals-utils-chartutils) helps to simplify development of axes, labels and legend for PowerBI custom visuals. 7 | 3. [ColorUtils](https://www.npmjs.com/package/powerbi-visuals-utils-colorutils) is a tool to manage color manipulations for PowerBI custom visuals 8 | 4. [TypeUtils](https://www.npmjs.com/package/powerbi-visuals-utils-tooltiputils) helps to use the Tooltip API for PowerBI custom visuals and extends the basic types for PowerBI custom visuals. 9 | 5. [InteractivityUtils](https://www.npmjs.com/package/powerbi-visuals-utils-interactivityutils) is a set of functions and classes for implementation of cross-selection and cross-filtering for PowerBI custom visuals. 10 | 6. [FormattingUtils](https://www.npmjs.com/package/powerbi-visuals-utils-formattingutils) are interfaces for creating PowerBI custom visuals. 11 | 7. [SVGUtils](https://www.npmjs.com/package/powerbi-visuals-utils-svgutils) is a tool for SVG manipulations for PowerBI custom visuals. 12 | 8. [FormattingModelUtils](https://github.com/microsoft/powerbi-visuals-utils-formattingmodel) is a set of classes, interfaces and method help building format pane easily. 13 | 9. [OnObjectUtils](https://github.com/microsoft/powerbi-visuals-utils-onobjectutils) provides an easy way for your Power BI custom visual to emit subselections to Power BI, get and render outlines. 14 | 15 | ### How to install 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-svgutils --save 20 | ``` 21 | This command installs the package and adds a package as a dependency to your ```package.json``` 22 | 23 | ### Including package dependencies 24 | After installation of the package you should include all necessary js dependencies into your project. 25 | You can find more information on each package github page. 26 | 27 | For example here is the installation guide for [SVGUtils](https://github.com/Microsoft/powerbi-visuals-utils-svgutils/blob/dev/documentation/docs/usage/installation-guide.md#how-to-instal) package. 28 | 29 | ### How to use 30 | Having installed the package with all dependencies, you can use these utils in your project. 31 | Let's change the following instruction by using SVGUtils package: 32 | 33 | ```typescript 34 | this.xAxis 35 | .attr('transform', 'translate(0, ' + height + ')') 36 | .call(xAxis); 37 | ``` 38 | 39 | At first, import SVGUtils module in top of your typescript file: 40 | 41 | ```typescript 42 | import SVGUtils = powerbi.extensibility.utils.svg; 43 | ``` 44 | 45 | After that user can use all available module methods 46 | ```typescript 47 | this.xAxis 48 | .attr('transform', SVGUtils.translate(0, height)) 49 | .call(xAxis); 50 | ``` 51 | 52 | To get more information about SVGItils package, please check the following [documentation](https://github.com/Microsoft/powerbi-visuals-utils-svgutils/) -------------------------------------------------------------------------------- /Tutorial/ExternalLibraries.md: -------------------------------------------------------------------------------- 1 | # Adding External Libraries 2 | 3 | PowerBI encourages you to use libraries of your choice. 4 | 5 | **NOTE:** We currently provide you with JQuery, d3, and lodash preloaded. However, we plan to remove these libraries in the future version. 6 | 7 | **Supplied Library Versions** 8 | 9 | * JQuery - 2.2.0 10 | * d3 - 3.5.5 11 | * lodash - 3.6.0 12 | 13 | ## Adding External Libraries 14 | Download the external library of your choice. 15 | 16 | Create an external folder in the root of your visual. 17 | 18 | ![](images/MakeExternalsDirectory.png) 19 | 20 | Copy your library into the external folder. 21 | 22 | ![](images/ExternalLibraries.png) 23 | 24 | Add the library to your `tsconfig.json` file. 25 | ```json 26 | { 27 | "compilerOptions": { 28 | "allowJs": true, 29 | "emitDecoratorMetadata": true, 30 | "experimentalDecorators": true, 31 | "target": "ES5", 32 | "sourceMap": true, 33 | "out": "./.tmp/build/visual.js" 34 | }, 35 | "files": [ 36 | ".api/v1.1.0/PowerBI-visuals.d.ts", 37 | "external/easeljs-0.8.2.min.js", 38 | "src/visual.ts" 39 | ] 40 | } 41 | ``` 42 | 43 | Refer to [this](Typings.md) if you'd like to add typings for your JS file to get intellisense and compile time safety on them. -------------------------------------------------------------------------------- /Tutorial/HighContrastSupport.md: -------------------------------------------------------------------------------- 1 | # Accessibility: Adding High-Contrast Mode Support 2 | Windows *High-Contrast* setting makes text and apps easier to see by using more distinct colors. 3 | Read more about [high-contrast support in Power BI](https://powerbi.microsoft.com/en-us/blog/power-bi-desktop-june-2018-feature-summary/#highContrast). 4 | 5 | Adding high-contrast support to your visual requires the following: 6 | 1. On init: Detect whether Power BI is in high-contrast mode and if so, get current high-contrast colors. 7 | 2. Every update: Change the way the visual renders to make it easier to see. 8 | 9 | See this [commit](https://github.com/Microsoft/PowerBI-visuals-sampleBarChart/commit/61011c82b66ca0d3321868f1d089c65101ca42e6) to learn how high-contrast was implemented in Sample Bar Chart, the files [src/barChart.ts](https://github.com/Microsoft/PowerBI-visuals-sampleBarChart/commit/61011c82b66ca0d3321868f1d089c65101ca42e6#diff-433142f7814fee940a0ffc98dc75bfcb) and [capabilities.json](https://github.com/Microsoft/PowerBI-visuals-sampleBarChart/commit/61011c82b66ca0d3321868f1d089c65101ca42e6#diff-290828b604cfa62f1cb310f2e90c52fd) contain the main changes. 10 | 11 | ## On Init 12 | The [colorPalette](./ColorPalette.md) member of `options.host` has several properties for high-contrast mode. Use these properties to determine whether high-contrast mode is active, and if so, what colors to use. 13 | 14 | ### Detect that Power BI is in high-contrast mode 15 | If `host.colorPalette.isHighContrast` is `true`, high-contrast mode is active and the visual should draw itself accordingly. 16 | 17 | ### Get high-contrast colors 18 | In high-contrast mode, your visual should limit itself to the following colors: 19 | * **Foreground** color is used to draw any lines, icons, text and outline or fill of shapes. 20 | * **Background** color is used for background, and as the fill color of outlined shapes. 21 | * **Foreground - selected** color is used to indicate a selected or active element. 22 | * **Hyperlink** color is used only for hyperlink text. 23 | 24 | Note: If a secondary color is needed, foreground color may be used with some opacity (Power BI native visuals use 40% opacity). Use this sparingly to keep the visual details easy to see. 25 | 26 | You can store these values during initialization: 27 | 28 | ```typescript 29 | private isHighContrast: boolean; 30 | 31 | private foregroundColor: string; 32 | private backgroundColor: string; 33 | private foregroundSelectedColor: string; 34 | private hyperlinkColor: string; 35 | //... 36 | 37 | constructor(options: VisualConstructorOptions) { 38 | this.host = options.host; 39 | let colorPalette: ISandboxExtendedColorPalette = host.colorPalette; 40 | //... 41 | this.isHighContrast = colorPalette.isHighContrast; 42 | if (this.isHighContrast) { 43 | this.foregroundColor = colorPalette.foreground.value; 44 | this.backgroundColor = colorPalette.background.value; 45 | this.foregroundSelectedColor = colorPalette.foregroundSelected.value; 46 | this.hyperlinkColor = colorPalette.hyperlink.value; 47 | } 48 | ``` 49 | Alternatively, you can store the `host` object during initialization and access the relevant `colorPalette` properties during update. 50 | 51 | ## On Update 52 | The specific implementation of high-contrast support vary from visual to visual and depend on the details of the graphic design. Typically, high-contrast mode requires a slightly different design than the default, in order to keep the important details easy to distinguish with the limited colors. 53 | Here are some guidelines followed by Power BI native visuals: 54 | * All data points use the same color (foreground). 55 | * All text, axes, arrows, lines etc. use foreground color. 56 | * Thick shapes are drawn as outlines, with thick strokes (at least 2 pixels) and background color fill. 57 | * When relevant, data points are distinguished by different marker shapes, data lines are distinguished by different dashing. 58 | * When a data element is highlighted, all other elements change their opacity to 40%. 59 | * For slicers, active filter elements use foreground-selected color. 60 | 61 | In Sample Bar Chart, for example, all bars are drawn with 2 pixels thick foreground outline and background fill. Compare the way it looks with default colors and with a couple of high-contrast themes: 62 | 63 | ![Sample Bar Chart using standard colors](images/HC_sampleBarChart_standard.png) 64 | 65 | ![Sample Bar Chart using *Dark #2* color theme](images/HC_sampleBarChart_dark2.png) 66 | ![Sample Bar Chart using *White* color theme](images/HC_sampleBarChart_white.png) 67 | 68 | Here is one place in the `createSelectorDataPoints` function that was changed to support high-contrast, it is called as part of rendering during `update`: 69 | 70 | **before** 71 | ```typescript 72 | for (let i = 0, len = Math.max(category.values.length, dataValue.values.length); i < len; i++) { 73 | let defaultColor: Fill = { 74 | solid: { 75 | color: colorPalette.getColor(category.values[i] + '').value 76 | } 77 | }; 78 | 79 | barChartDataPoints.push({ 80 | category: category.values[i] + '', 81 | value: dataValue.values[i], 82 | color: getCategoricalObjectValue(category, i, 'colorSelector', 'fill', defaultColor).solid.color, 83 | selectionId: host.createSelectionIdBuilder() 84 | .withCategory(category, i) 85 | .createSelectionId() 86 | }); 87 | } 88 | ``` 89 | 90 | **after** 91 | ```typescript 92 | 93 | const colorPalette: ISandboxExtendedColorPalette = host.colorPalette; 94 | const strokeColor: string = getColumnStrokeColor(colorPalette); 95 | const strokeWidth: number = getColumnStrokeWidth(colorPalette.isHighContrast); 96 | 97 | for (let i = 0, len = Math.max(category.values.length, dataValue.values.length); i < len; i++) { 98 | const color: string = getColumnColorByIndex(category, i, colorPalette); 99 | 100 | const selectionId: ISelectionId = host.createSelectionIdBuilder() 101 | .withCategory(category, i) 102 | .createSelectionId(); 103 | 104 | barChartDataPoints.push({ 105 | color, 106 | strokeColor, 107 | strokeWidth, 108 | selectionId, 109 | value: dataValue.values[i], 110 | category: `${category.values[i]}`, 111 | }); 112 | } 113 | 114 | //... 115 | 116 | function getColumnColorByIndex( 117 | category: DataViewCategoryColumn, 118 | index: number, 119 | colorPalette: ISandboxExtendedColorPalette, 120 | ): string { 121 | if (colorPalette.isHighContrast) { 122 | return colorPalette.background.value; 123 | } 124 | 125 | const defaultColor: Fill = { 126 | solid: { 127 | color: colorPalette.getColor(`${category.values[index]}`).value, 128 | } 129 | }; 130 | 131 | return getCategoricalObjectValue(category, index, 'colorSelector', 'fill', defaultColor).solid.color; 132 | } 133 | ``` -------------------------------------------------------------------------------- /Tutorial/LaunchURL.md: -------------------------------------------------------------------------------- 1 | # Opening a URL in a new tab/window 2 | Launch URL allows opening a new browser tab (or window), by delegating the actual work to Power BI. 3 | 4 | Note: custom visuals are hosted in Power BI inside sandboxed iframes, this prevents opening a new browser tab (or window) in "the usual way", e.g. using `window.open('http://some.link.net','_blank')`. 5 | 6 | ## Usage 7 | Use the `host.launchUrl()` API call, passing your destination URL as a string argument: 8 | 9 | ```typescript 10 | this.host.launchUrl('http://some.link.net'); 11 | ``` 12 | 13 | ## Restrictions 14 | * Use only absolute paths, not relative ones. `http://some.link.net/subfolder/page.html` is fine, `/page.html` won't be opened. 15 | * Currently only `http` and `https` protocols are supported. Avoid `ftp`, `mailto` etc. 16 | 17 | ## Best practices 18 | 1. For most cases, it is best to only open a link as a response to a user's explicit action. Make it easy for the user to understand that clicking the link or button will result in opening a new tab. Triggering a `launchUrl()` call without a user's action, or as a side effect of a different action can be confusing or frustrating for the user. 19 | 2. If the link is not crucial for the proper functioning of the visual, it is recommended to provide the report's author a way to disable and hide the link. This is especially relevant for special Power BI use-cases, such as embedding a report in a 3rd party application or publishing it to the web. 20 | 3. Avoid Triggering a `launchUrl()` call from inside a loop, the visual's `update` function, or any other frequently recurring code. 21 | 22 | ## Step by step example 23 | ### Adding a link launching element 24 | The following lines were added to the visual's `constructor` function: 25 | ```typescript 26 | this.helpLinkElement = this.createHelpLinkElement(); 27 | options.element.appendChild(this.helpLinkElement); 28 | ``` 29 | And, a private function creating and attaching the anchor element was added: 30 | ```typescript 31 | private createHelpLinkElement(): Element { 32 | let linkElement = document.createElement("a"); 33 | linkElement.textContent = "?"; 34 | linkElement.setAttribute("title", "Open documentation"); 35 | linkElement.setAttribute("class", "helpLink"); 36 | linkElement.addEventListener("click", () => { 37 | this.host.launchUrl("https://github.com/Microsoft/PowerBI-visuals/blob/master/Readme.md#developing-your-first-powerbi-visual"); 38 | }); 39 | return linkElement; 40 | }; 41 | ``` 42 | Finally, an entry in the visual.less file defines the style for the link element ([see here](https://github.com/Microsoft/PowerBI-visuals-sampleBarChart/commit/2ecc5cf74b9bc6fbf5c03f84c3ab24841b489d4e#diff-96b5545ad582c6d540c60ebff2c9f806)) 43 | 44 | ### Adding a toggling mechanism 45 | This requires adding a static object (see [static object tutorial](https://github.com/Microsoft/PowerBI-visuals-sampleBarChart/blob/master/Tutorial/StaticObjects.md)), so that the report's author can toggle the visibility of the link element (default is set to hidden). 46 | A `showHelpLink` boolean static object was added to `capabilities.json` objects entry: 47 | 48 | ```typescript 49 | "objects": { 50 | //... 51 | "generalView": { 52 | "properties": 53 | //... 54 | "showHelpLink": { 55 | "type": { 56 | "bool": true 57 | } 58 | } 59 | } 60 | } 61 | } 62 | ``` 63 | 64 | ![](images/launchURLtoggle.png) 65 | 66 | And, in the visual's `update` function, the following lines were added: 67 | ```typescript 68 | this.helpLinkElement 69 | .classed("hidden", !this.formattingSettings.generalView.showHelpLink.value) 70 | .style("border-color", this.formattingSettings.generalView.helpLinkColor) 71 | .style("color", this.formattingSettings.generalView.helpLinkColor); 72 | ``` 73 | 74 | The `hidden` class is defined in visual.less to control the display of the element. -------------------------------------------------------------------------------- /Tutorial/Locale.md: -------------------------------------------------------------------------------- 1 | # Localizing your Custom Visuals 2 | 3 | Visuals can now know PowerBI's locale, so they can display localized information 4 | (read more about [Supported languages and countries/regions for Power BI](https://powerbi.microsoft.com/en-us/documentation/powerbi-supported-languages/)).
5 | The `locale` string is passed on `IVisualHost`. 6 | 7 | See [commit](https://github.com/Microsoft/PowerBI-visuals-sampleBarChart/commit/388670c71a873bf7412e771164ea3cbb8522a63e) for what was added at this step. 8 | 9 | ## Localizing the tooltips 10 | 11 | In the sample we display the current locale in the tooltip. 12 | 13 | ![Sample BarChart with Locale](./images/LocaleInSampleBarChart.png) 14 | 15 | Each of these bar charts was created under different locale (English, Basque and Hindi). 16 | 17 | The BarChart constructor now has a `locale` member which is instantiated in the constructor with the host `locale` instance. 18 | 19 | ```typescript 20 | private locale: string; 21 | ... 22 | this.locale = options.host.locale; 23 | ``` 24 | 25 | A `LocalizationResources` interface was added, which helps in localizing strings. It defines the required string for each locale, and also the 'defaultValue', which will be displayed if the visual wasn't adapted to this locale.
26 | `myResources` is an instance of this interface, which holds the localized strings: 27 | 28 | ```typescript 29 | module powerbi.extensibility.visual { 30 | 31 | export var myResources: Resources = {}; 32 | myResources["LanguageKey"] = { 33 | defaultValue: "English(English)", 34 | localization: { 35 | "ar-SA": "العربية (Arabic)", 36 | "bg-BG": "български (Bulgarian)", 37 | ..., 38 | "zh-CN": "中国 (Chinese-Simplified)", 39 | "zh-TW": "中國 (Chinese-Tranditional)" 40 | } 41 | }; 42 | 43 | } 44 | ``` 45 | Getting a localized string is easy using `getLocalizedString`. 46 | ```typescript 47 | /** 48 | * Returns the localized string in the locale transferred using the key that was given to search the resources 49 | * 50 | * @param {string} locale - the locale in which PowerBI is currently running 51 | * @param {object} key - specify a key for the string you want localized in your visual 52 | */ 53 | export function getLocalizedString(locale: string, key: string): string { 54 | return myResources && key && myResources[key] && (((myResources[key]).localization[locale])|| (myResources[key]).defaultValue); 55 | } 56 | ``` 57 | 58 | The data for the tooltip is than derived from the current `locale`: 59 | 60 | ```typescript 61 | private getTooltipData(value: any): VisualTooltipDataItem[] { 62 | let language = getLocalizedString(this.locale,"LanguageKey"); 63 | return [{ 64 | displayName: value.category, 65 | value: value.value.toString(), 66 | color: value.color, 67 | header: language && "displayed language " + language 68 | }]; 69 | } 70 | ``` 71 | 72 | ## Format Pane Localization 73 | For more info on localization [here](https://learn.microsoft.com/power-bi/developer/visuals/localization) -------------------------------------------------------------------------------- /Tutorial/OnObject.md: -------------------------------------------------------------------------------- 1 | # Adding OnObject formatting to your Visual 2 | The `IVisual` interface expose `VisualOnObjectFormatting` which we will implement in this tutorial using the `HtmlSubSelectionHelper` and `subSelectionService` exposed on `IVisualHost`. 3 | 4 | ```typescript 5 | interface VisualOnObjectFormatting { 6 | getSubSelectionStyles(subSelections: powerbi.visuals.CustomVisualSubSelection[]): powerbi.visuals. SubSelectionStyles | undefined; 7 | getSubSelectionShortcuts(subSelections: powerbi.visuals.CustomVisualSubSelection[], filter: powerbi.visuals.SubSelectionShortcutsKey | undefined): powerbi.visuals.VisualSubSelectionShortcuts | undefined; 8 | getSubSelectables?(filter?: powerbi.visuals.SubSelectionStylesType): powerbi.visuals.CustomVisualSubSelection[] | undefined; 9 | } 10 | ``` 11 | 12 | import the `HtmlSubSelectionHelper` and its attributes: 13 | ```typescript 14 | import { 15 | HtmlSubSelectableClass, HtmlSubSelectionHelper, SubSelectableDirectEdit as SubSelectableDirectEditAttr, 16 | SubSelectableDisplayNameAttribute, SubSelectableObjectNameAttribute, SubSelectableTypeAttribute 17 | } from 'powerbi-visuals-utils-onobjectutils'; 18 | ``` 19 | 20 | To make it easy to use the visual object names let's create an enum containing the visual objects: 21 | ```typescript 22 | const enum BarChartObjectNames { 23 | ArcElement = 'arcElement', 24 | ColorSelector = 'colorSelector', 25 | EnableAxis = 'enableAxis', 26 | DirectEdit = 'directEdit' 27 | } 28 | ``` 29 | 30 | create a reference interface for the visual objects properties FormattingIds, this will help when implementing the `VisualOnObjectFormatting` methods 31 | ```typescript 32 | interface References { 33 | cardUid?: string; 34 | groupUid?: string; 35 | fill?: FormattingId; 36 | font?: FormattingId; 37 | fontColor?: FormattingId; 38 | show?: FormattingId; 39 | fontFamily?: FormattingId; 40 | bold?: FormattingId; 41 | italic?: FormattingId; 42 | underline?: FormattingId; 43 | fontSize?: FormattingId; 44 | position?: FormattingId; 45 | textProperty?: FormattingId; 46 | } 47 | ``` 48 | 49 | Create a reference for each visual object you need. 50 | 51 | ```typescript 52 | const colorSelectorReferences: References = { 53 | cardUid: 'Visual-colorSelector-card', 54 | groupUid: 'colorSelector-group', 55 | fill: { 56 | objectName: BarChartObjectNames.ColorSelector, 57 | propertyName: 'fill' 58 | } 59 | }; 60 | 61 | const enableAxisReferences: References = { 62 | cardUid: 'Visual-enableAxis-card', 63 | groupUid: 'enableAxis-group', 64 | fill: { 65 | objectName: BarChartObjectNames.EnableAxis, 66 | propertyName: 'fill' 67 | }, 68 | show: { 69 | objectName: BarChartObjectNames.EnableAxis, 70 | propertyName: 'show' 71 | } 72 | }; 73 | 74 | const directEditReferences: References = { 75 | cardUid: 'Visual-directEdit-card', 76 | groupUid: 'directEdit-group', 77 | fontFamily: { 78 | objectName: BarChartObjectNames.DirectEdit, 79 | propertyName: 'fontFamily' 80 | }, 81 | bold: { 82 | objectName: BarChartObjectNames.DirectEdit, 83 | propertyName: 'bold' 84 | }, 85 | italic: { 86 | objectName: BarChartObjectNames.DirectEdit, 87 | propertyName: 'italic' 88 | }, 89 | underline: { 90 | objectName: BarChartObjectNames.DirectEdit, 91 | propertyName: 'underline' 92 | }, 93 | fontSize: { 94 | objectName: BarChartObjectNames.DirectEdit, 95 | propertyName: 'fontSize' 96 | }, 97 | fontColor: { 98 | objectName: BarChartObjectNames.DirectEdit, 99 | propertyName: 'fontColor' 100 | }, 101 | show: { 102 | objectName: BarChartObjectNames.DirectEdit, 103 | propertyName: 'show' 104 | }, 105 | position: { 106 | objectName: BarChartObjectNames.DirectEdit, 107 | propertyName: 'position' 108 | }, 109 | textProperty: { 110 | objectName: BarChartObjectNames.DirectEdit, 111 | propertyName: 'textProperty' 112 | } 113 | }; 114 | ``` 115 | 116 | implement the `VisualOnObjectFormatting` methods 117 | Note: When providing a selector, make sure that it is the same selector as provided for the formatting model. 118 | ```typescript 119 | private getSubSelectionStyles(subSelections: CustomVisualSubSelection[]): powerbi.visuals.SubSelectionStyles | undefined { 120 | const visualObject = subSelections[0]?.customVisualObjects[0]; 121 | if (visualObject) { 122 | switch (visualObject.objectName) { 123 | case BarChartObjectNames.ColorSelector: 124 | return this.getColorSelectorStyles(subSelections); 125 | case BarChartObjectNames.EnableAxis: 126 | return this.getEnableAxisStyles(); 127 | case BarChartObjectNames.DirectEdit: 128 | return this.getDirectEditStyles(); 129 | } 130 | } 131 | } 132 | private getSubSelectionShortcuts(subSelections: CustomVisualSubSelection[]): VisualSubSelectionShortcuts | undefined { 133 | const visualObject = subSelections[0]?.customVisualObjects[0]; 134 | if (visualObject) { 135 | switch (visualObject.objectName) { 136 | case BarChartObjectNames.ColorSelector: 137 | return this.getColorSelectorShortcuts(subSelections); 138 | case BarChartObjectNames.EnableAxis: 139 | return this.getEnableAxisShortcuts(); 140 | case BarChartObjectNames.DirectEdit: 141 | return this.getDirectEditShortcuts(); 142 | } 143 | } 144 | } 145 | private getSubSelectables?(filter?: powerbi.visuals.SubSelectionStylesType): CustomVisualSubSelection[] | undefined { 146 | return this.subSelectionHelper.getAllSubSelectables(filter); 147 | } 148 | 149 | private getColorSelectorShortcuts(subSelections: CustomVisualSubSelection[]): VisualSubSelectionShortcuts { 150 | const selector = subSelections[0].customVisualObjects[0].selectionId?.getSelector(); 151 | return [ 152 | { 153 | type: VisualShortcutType.Reset, 154 | relatedResetFormattingIds: [{ 155 | ...colorSelectorReferences.fill, 156 | selector 157 | }], 158 | }, 159 | { 160 | type: VisualShortcutType.Navigate, 161 | destinationInfo: { cardUid: colorSelectorReferences.cardUid }, 162 | label: 'Color' 163 | } 164 | ]; 165 | } 166 | 167 | private getColorSelectorStyles(subSelections: CustomVisualSubSelection[]): SubSelectionStyles { 168 | const selector = subSelections[0].customVisualObjects[0].selectionId?.getSelector(); 169 | return { 170 | type: SubSelectionStylesType.Shape, 171 | fill: { 172 | label: 'Fill', 173 | reference: { 174 | ...colorSelectorReferences.fill, 175 | selector 176 | }, 177 | }, 178 | }; 179 | } 180 | 181 | private getEnableAxisStyles(): SubSelectionStyles { 182 | return { 183 | type: SubSelectionStylesType.Shape, 184 | fill: { 185 | reference: { 186 | ...enableAxisReferences.fill 187 | }, 188 | label: 'Enable Axis' 189 | } 190 | } 191 | } 192 | 193 | private getEnableAxisShortcuts(): VisualSubSelectionShortcuts { 194 | return [ 195 | { 196 | type: VisualShortcutType.Reset, 197 | relatedResetFormattingIds: [{ 198 | ...enableAxisReferences.fill, 199 | }], 200 | excludedResetFormattingIds: [{ 201 | ...enableAxisReferences.show, 202 | }] 203 | }, 204 | { 205 | type: VisualShortcutType.Toggle, 206 | relatedToggledFormattingIds: [{ 207 | ...enableAxisReferences.show 208 | }], 209 | ...enableAxisReferences.show, 210 | disabledLabel: 'Delete', 211 | enabledLabel: 'Delete' 212 | }, 213 | { 214 | type: VisualShortcutType.Navigate, 215 | destinationInfo: { cardUid: enableAxisReferences.cardUid }, 216 | label: 'EnableAxis' 217 | } 218 | ]; 219 | } 220 | 221 | private getDirectEditShortcuts(): VisualSubSelectionShortcuts { 222 | return [ 223 | { 224 | type: VisualShortcutType.Reset, 225 | relatedResetFormattingIds: [ 226 | directEditReferences.bold, 227 | directEditReferences.fontFamily, 228 | directEditReferences.fontSize, 229 | directEditReferences.italic, 230 | directEditReferences.underline, 231 | directEditReferences.fontColor, 232 | directEditReferences.textProperty 233 | ] 234 | }, 235 | { 236 | type: VisualShortcutType.Toggle, 237 | relatedToggledFormattingIds: [{ 238 | ...directEditReferences.show, 239 | }], 240 | ...directEditReferences.show, 241 | disabledLabel: 'Delete', 242 | 243 | }, 244 | { 245 | type: VisualShortcutType.Picker, 246 | ...directEditReferences.position, 247 | label: 'Position' 248 | }, 249 | { 250 | type: VisualShortcutType.Navigate, 251 | destinationInfo: { cardUid: directEditReferences.cardUid }, 252 | label: 'Direct edit' 253 | } 254 | ]; 255 | } 256 | 257 | private getDirectEditStyles(): SubSelectionStyles { 258 | return { 259 | type: powerbi.visuals.SubSelectionStylesType.Text, 260 | fontFamily: { 261 | reference: { 262 | ...directEditReferences.fontFamily 263 | }, 264 | label: 'font' 265 | }, 266 | bold: { 267 | reference: { 268 | ...directEditReferences.bold 269 | }, 270 | label: 'font' 271 | }, 272 | italic: { 273 | reference: { 274 | ...directEditReferences.italic 275 | }, 276 | label: 'font' 277 | }, 278 | underline: { 279 | reference: { 280 | ...directEditReferences.underline 281 | }, 282 | label: 'font' 283 | }, 284 | fontSize: { 285 | reference: { 286 | ...directEditReferences.fontSize 287 | }, 288 | label: 'font' 289 | }, 290 | fontColor: { 291 | reference: { 292 | ...directEditReferences.fontColor 293 | }, 294 | label: 'fontColor' 295 | }, 296 | background: { 297 | reference: { 298 | objectName: 'directEdit', 299 | propertyName: 'background' 300 | }, 301 | label: 'background' 302 | } 303 | }; 304 | } 305 | ``` 306 | 307 | in the constuctor create the `HtmlSubSelectionHelper` and provide the get methods in the `visualOnObjectFormatting` 308 | ```typescript 309 | this.subSelectionHelper = HtmlSubSelectionHelper.createHtmlSubselectionHelper({ 310 | hostElement: options.element, 311 | subSelectionService: options.host.subSelectionService, 312 | selectionIdCallback: (e) => this.selectionIdCallback(e), 313 | }); 314 | 315 | this.visualOnObjectFormatting = { 316 | getSubSelectionStyles: (subSelections) => this.getSubSelectionStyles(subSelections), 317 | getSubSelectionShortcuts: (subSelections) => this.getSubSelectionShortcuts(subSelections), 318 | getSubSelectables: (filter) => this.getSubSelectables(filter) 319 | }; 320 | ``` 321 | 322 | In the visual update: 323 | Add `HtmlSubSelectionHelper` attributes to the relevant element and set their format in the visual update, for example for the `colorSelector` 324 | ```typescript 325 | barSelectionMerged 326 | .attr(SubSelectableObjectNameAttribute, 'colorSelector') 327 | .attr(SubSelectableDisplayNameAttribute, (dataPoint: BarChartDataPoint) => this.formattingSettings.colorSelector.slices[dataPoint.index].displayName) 328 | .attr(SubSelectableTypeAttribute, powerbi.visuals.SubSelectionStylesType.Shape) 329 | .classed(HtmlSubSelectableClass, options.formatMode) 330 | .attr("width", xScale.bandwidth()) 331 | .attr("height", d => height - yScale(d.value)) 332 | .attr("y", d => yScale(d.value)) 333 | .attr("x", d => xScale(d.category)) 334 | .style("fill-opacity", opacity) 335 | .style("stroke-opacity", opacity) 336 | .style("fill", (dataPoint: BarChartDataPoint) => dataPoint.color) 337 | .style("stroke", (dataPoint: BarChartDataPoint) => dataPoint.strokeColor) 338 | .style("stroke-width", (dataPoint: BarChartDataPoint) => `${dataPoint.strokeWidth}px`); 339 | ``` 340 | 341 | set the formatMode for the `HtmlSubSelectionHelper` 342 | ```typescript 343 | this.subSelectionHelper.setFormatMode(options.formatMode); 344 | ``` 345 | 346 | 347 | disable data interactivity when the visual in formatMode 348 | ```typescript 349 | if (this.formatMode) { 350 | this.removeEventHandlers(barSelectionMerged); 351 | } else { 352 | this.addEventHandlers(barSelectionMerged); 353 | } 354 | ``` 355 | 356 | When a subselection is sub-selected we will get it in the update, use `HtmlSubSelectionHelper` to render the subselection outlines 357 | ```typescript 358 | const shouldUpdateSubSelection = options.type & (powerbi.VisualUpdateType.Data 359 | | powerbi.VisualUpdateType.Resize 360 | | powerbi.VisualUpdateType.FormattingSubSelectionChange); 361 | if (this.formatMode && shouldUpdateSubSelection) { 362 | this.subSelectionHelper.updateOutlinesFromSubSelections(options.subSelections, true); 363 | } 364 | ``` -------------------------------------------------------------------------------- /Tutorial/ReportPageTooltips.md: -------------------------------------------------------------------------------- 1 | # Adding Report Page Tooltips support to Bar Chart 2 | Report page tooltips support can be done by updating your visual capabilities. 3 | See [commit](https://github.com/Microsoft/PowerBI-visuals-sampleBarChart/commit/42aa6b6d69dea0b5e75bb6a78779b524efde9800) for what was added at this step. 4 | 5 | To add report page tooltips support, most changes will be located in `capabilities.json`. A sample schema is already in place for you. 6 | 7 | Report page tooltips definition can be done on the Format pane. 8 | 9 | ![](images/ReportPageTooltip.png) 10 | 11 | ## Support Canvas Tooltips 12 | To support displaying report page tooltips, add "tooltips" definition to capabilities.json as follows 13 | 14 | ```json 15 | "tooltips": { 16 | "supportedTypes": { 17 | "default": true, 18 | "canvas": true 19 | }, 20 | "roles": [ 21 | "tooltips" 22 | ] 23 | } 24 | ``` 25 | 26 | `supportedTypes` is the tooltips configuration supported by the visual and reflected on the field well. 27 | `default` specifies whether the "automatic" tooltips binding via data field is supported. 28 | `canvas` specifies whether the report page tooltips are supported. 29 | 30 | `roles` optional. Once defined, instructs what data roles will be bound to the selected tooltip option in fields well. 31 | 32 | 33 | For more information, see the Report Page Tooltips usage guidelines [Report Page Tooltips](https://powerbi.microsoft.com/en-us/blog/power-bi-desktop-march-2018-feature-summary/#tooltips). 34 | 35 | ## Applying report page tooltips 36 | For displaying the report page tooltip, upon calling ITooltipService.Show(options: TooltipShowOptions) or ITooltipService.Move(options: TooltipMoveOptions), the PowerBI host will consume the selectionId ('identities' property of 'options' argument above). 37 | Therefore, the SelectionId should represent the selected data (category, series, etc) of the item you hovered above to be retrieved by the tooltip. 38 | 39 | See more on building SelectionId under [Adding Selection and Interactions with Other Visuals](https://github.com/Microsoft/PowerBI-visuals/blob/master/Tutorial/Selection.md) 40 | 41 | Example of sending the selectionId to tooltip display calls: 42 | 43 | ![](images/ApplyReportPageTooltip.png) -------------------------------------------------------------------------------- /Tutorial/Selection.md: -------------------------------------------------------------------------------- 1 | # Adding Selection and Interactions with Other Visuals 2 | Selection provides the ability for the user to interact with your visual and also interact with other visuals. 3 | 4 | See [commit](https://github.com/Microsoft/PowerBI-visuals-sampleBarChart/commit/b765940e9b9a14b3360cded30b329224ab572475) for what was added at this step. 5 | 6 | ## Adding Selection to Each Data Point 7 | Since each data point is unique, selection must be added to each data point. Add a property for selection on BarChartDataPoint interface. 8 | 9 | ```typescript 10 | /** 11 | * Interface for BarChart data points. 12 | * 13 | * @interface 14 | * @property {PrimitiveValue} value - Data value for point. 15 | * @property {string} category - Corresponding category of data value. 16 | * @property {string} color - Color corresponding to data point. 17 | * @property {string} strokeColor - Stroke color for data point column. 18 | * @property {number} strokeWidth - Stroke width for data point column. 19 | * @property {ISelectionId} selectionId - Id assigned to data point for cross filtering 20 | * and visual interaction. 21 | */ 22 | export interface BarChartDataPoint { 23 | value: PrimitiveValue; 24 | category: string; 25 | color: string; 26 | strokeColor: string; 27 | strokeWidth: number; 28 | selectionId: ISelectionId; 29 | } 30 | ``` 31 | 32 | ## Assigning Selection Ids to Each Data Point 33 | Since we iterate through the data points in `createSelectorDataPoints` it is also the ideal place to create your selection ids. 34 | The host variable is a `IVisualHost`, which contains services that the visual may use such as color and selection builder. 35 | 36 | Use the selection builder factory method on `IVisualHost` to create a new selection id. 37 | Since we're making selection only based on the category, we only need to define selection `withCategory`. 38 | 39 | **NOTE**: A new selection builder must be created per data point. 40 | 41 | ```typescript 42 | 43 | for (let i = 0, len = Math.max(category.values.length, dataValue.values.length); i < len; i++) { 44 | const color: string = getColumnColorByIndex(category, i, colorPalette); 45 | 46 | const selectionId: ISelectionId = host.createSelectionIdBuilder() 47 | .withCategory(category, i) 48 | .createSelectionId(); 49 | 50 | barChartDataPoints.push({ 51 | color, 52 | strokeColor, 53 | strokeWidth, 54 | selectionId, 55 | value: dataValue.values[i], 56 | category: `${category.values[i]}`, 57 | }); 58 | } 59 | ``` 60 | 61 | For more information, see the section about using [Selection Id Builder](https://github.com/Microsoft/PowerBI-visuals/blob/master/Visual/Selection.md#creating-selection-ids-selectionidbuilder). 62 | 63 | ## Interacting with your Data Points 64 | Each bar on the bar chart can be interacted with once a selection id is assigned to the data point. 65 | The bar chart will listen to click events. 66 | 67 | Use the selection manager factory method on `IVisualHost` to create selection manager. This allow for cross filtering and clearing selections. 68 | Call `syncSelectionState` using selectionManager selectionIds and barSelection: 69 | 70 | ```typescript 71 | this.selectionManager = options.host.createSelectionManager(); 72 | this.selectionManager.registerOnSelectCallback(() => { 73 | this.syncSelectionState(this.barSelection, this.selectionManager.getSelectionIds()); 74 | }); 75 | 76 | // .... 77 | 78 | private syncSelectionState( 79 | selection: Selection, 80 | selectionIds: ISelectionId[] 81 | ): void { 82 | if (!selection || !selectionIds) { 83 | return; 84 | } 85 | 86 | if (!selectionIds.length) { 87 | const opacity: number = this.formattingSettings.generalView.opacity.value / 100; 88 | selection 89 | .style("fill-opacity", opacity) 90 | .style("stroke-opacity", opacity); 91 | return; 92 | } 93 | 94 | const self: this = this; 95 | 96 | selection.each(function (barDataPoint: BarChartDataPoint) { 97 | const isSelected: boolean = self.isSelectionIdInArray(selectionIds, barDataPoint.selectionId); 98 | 99 | const opacity: number = isSelected 100 | ? BarChart.Config.solidOpacity 101 | : BarChart.Config.transparentOpacity; 102 | 103 | d3Select(this) 104 | .style("fill-opacity", opacity) 105 | .style("stroke-opacity", opacity); 106 | }); 107 | } 108 | 109 | ``` 110 | 111 | For more information, see the section about using [Selection Manager](https://github.com/Microsoft/PowerBI-visuals/blob/master/Visual/Selection.md#managing-selection-selectionmanager). -------------------------------------------------------------------------------- /Tutorial/StaticObjects.md: -------------------------------------------------------------------------------- 1 | # Static Objects 2 | Objects can be added to further customize what the visual can do. These customizations can just be UI changes, but can also be changes related to the data that was queried. 3 | We will be using static objects to render an x axis for the Bar Chart. 4 | 5 | Objects can be toggled on the property pane. 6 | 7 | ![](images/PropertyPane.png) 8 | 9 | ## Define Object in Capabilities 10 | Define an objects property inside your capabilities. This defines the object you plan to display in the property pane. 11 | `enableAxis` is the internal name that will be referenced in the `dataView`. 12 | `bool` is a `PrimitiveValue` and is typically used with static objects such as text boxes or switches. 13 | 14 | ![](images/ObjectShowProperty.png) 15 | 16 | ```typescript 17 | "objects": { 18 | "enableAxis": { 19 | "properties": { 20 | "show": { 21 | "type": { "bool": true } 22 | } 23 | } 24 | } 25 | } 26 | ``` 27 | 28 | For more information, see the section about using [Objects](https://learn.microsoft.com/en-us/power-bi/developer/visuals/capabilities#objects-define-property-pane-options). 29 | 30 | ## Defining Formatting Pane Settings Model 31 | Although this is optional, it is best to localize most settings onto a single object so that all settings can be easily referenced. 32 | 33 | ```typescript 34 | 35 | private formattingSettingsService: FormattingSettingsService; 36 | private formattingSettings: BarChartSettingsModel; 37 | 38 | 39 | constructor(options: VisualConstructorOptions) { 40 | // ... 41 | 42 | const localizationManager = this.host.createLocalizationManager(); 43 | this.formattingSettingsService = new FormattingSettingsService(localizationManager); 44 | 45 | // ... 46 | } 47 | 48 | 49 | public update(options: VisualUpdateOptions) { 50 | this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(BarChartSettingsModel, options.dataViews); 51 | this.barDataPoints = createSelectorDataPoints(options, this.host); 52 | this.formattingSettings.populateColorSelector(this.barDataPoints); 53 | // ... 54 | } 55 | ``` 56 | 57 | Formatting pane settings model : 58 | ```typescript 59 | 60 | import Card = formattingSettings.Card; 61 | import Model = formattingSettings.Model; 62 | 63 | class EnableAxisCardSettings extends Card { 64 | show = new formattingSettings.ToggleSwitch({ 65 | name: "show", 66 | displayName: undefined, 67 | value: false, 68 | topLevelToggle: true 69 | }); 70 | 71 | fill = new formattingSettings.ColorPicker({ 72 | name: "fill", 73 | displayName: "Color", 74 | value: { value: "#000000" } 75 | }); 76 | 77 | name: string = "enableAxis"; 78 | displayName: string = "Enable Axis"; 79 | slices = [this.show, this.fill]; 80 | } 81 | 82 | 83 | class ColorSelectorCardSettings extends Card { 84 | name: string = "colorSelector"; 85 | displayName: string = "Data Colors"; 86 | slices = []; 87 | } 88 | 89 | class GeneralViewCardSettings extends Card { 90 | opacity = new formattingSettings.NumUpDown({ 91 | name: "opacity", 92 | displayName: "Bars Opacity", 93 | value: 100, 94 | options: { 95 | minValue: { 96 | type: powerbiVisualsApi.visuals.ValidatorType.Min, 97 | value: 0, 98 | }, 99 | maxValue: { 100 | type: powerbiVisualsApi.visuals.ValidatorType.Max, 101 | value: 100, 102 | } 103 | } 104 | }); 105 | 106 | showHelpLink = new formattingSettings.ToggleSwitch({ 107 | name: "showHelpLink", 108 | displayName: "Show Help Button", 109 | value: false 110 | }); 111 | 112 | name: string = "generalView"; 113 | displayName: string = "General View"; 114 | helpLinkColor: string = "#80B0E0" 115 | slices = [this.opacity, this.showHelpLink]; 116 | } 117 | 118 | class AverageLineCardSettings extends Card { 119 | show = new formattingSettings.ToggleSwitch({ 120 | name: "show", 121 | displayName: undefined, 122 | value: false, 123 | topLevelToggle: true 124 | }); 125 | 126 | fill = new formattingSettings.ColorPicker({ 127 | name: "fill", 128 | displayName: "Color", 129 | value: { value: "#888888" }, 130 | }); 131 | 132 | showDataLabel = new formattingSettings.ToggleSwitch({ 133 | name: "showDataLabel", 134 | displayName: "Data Label", 135 | value: false 136 | }); 137 | 138 | name: string = "averageLine"; 139 | displayName: string = "Average Line"; 140 | analyticsPane: boolean = true; 141 | slices = [this.show, this.fill, this.showDataLabel]; 142 | } 143 | 144 | /** 145 | * BarChart formatting settings model class 146 | */ 147 | export class BarChartSettingsModel extends Model { 148 | enableAxis = new EnableAxisCardSettings(); 149 | colorSelector = new ColorSelectorCardSettings(); 150 | generalView = new GeneralViewCardSettings(); 151 | averageLine = new AverageLineCardSettings(); 152 | cards = [this.enableAxis, this.colorSelector, this.generalView, this.averageLine]; 153 | 154 | /** 155 | * populate colorSelector object categories formatting properties 156 | * @param dataPoints 157 | */ 158 | populateColorSelector(dataPoints: BarChartDataPoint[]) { 159 | let slices = this.colorSelector.slices; 160 | if (dataPoints) { 161 | dataPoints.forEach(dataPoint => { 162 | slices.push(new formattingSettings.ColorPicker({ 163 | name: "fill", 164 | displayName: dataPoint.category, 165 | value: { value: dataPoint.color }, 166 | selector: dataViewWildcard.createDataViewWildcardSelector(dataViewWildcard.DataViewWildcardMatchingOption.InstancesAndTotals), 167 | altConstantSelector: dataPoint.selectionId.getSelector(), 168 | instanceKind: powerbiVisualsApi.VisualEnumerationInstanceKinds.ConstantOrRule 169 | })); 170 | }); 171 | } 172 | } 173 | } 174 | 175 | ``` 176 | ## Defining and Using Object Enumeration Utility 177 | Object property values are available as metadata on the `dataView`. However, there is currently no service to help retrieve these properties. 178 | ObjectEnumerationUtility is a set of static functions used to retrieve object values from the `dataView`. ObjectEnumerationUtility can be used for other visual projects. 179 | 180 | **NOTE**: Object Enumeration Utility is optional, but it is great option to iterate through the `dataView` and retrieve object properties. 181 | 182 | ```typescript 183 | /** 184 | * Gets property value for a particular object. 185 | * 186 | * @function 187 | * @param {DataViewObjects} objects - Map of defined objects. 188 | * @param {string} objectName - Name of desired object. 189 | * @param {string} propertyName - Name of desired property. 190 | * @param {T} defaultValue - Default value of desired property. 191 | */ 192 | export function getValue(objects: DataViewObjects, objectName: string, propertyName: string, defaultValue: T ): T { 193 | if(objects) { 194 | let object = objects[objectName]; 195 | if(object) { 196 | let property: T = object[propertyName]; 197 | if(property !== undefined) { 198 | return property; 199 | } 200 | } 201 | } 202 | return defaultValue; 203 | } 204 | ``` 205 | 206 | ## Retrieving Property Values from DataView 207 | `createSelectorDataPoints` is the ideal place to manipulate the visual's data points. We will continue this pattern and retrieve the object properties from the `dataView`. 208 | 209 | Define the default state of the property: 210 | 211 | ```typescript 212 | 213 | class EnableAxisCardSettings extends Card { 214 | show = new formattingSettings.ToggleSwitch({ 215 | name: "show", 216 | displayName: undefined, 217 | value: false, 218 | topLevelToggle: true 219 | }); 220 | 221 | fill = new formattingSettings.ColorPicker({ 222 | name: "fill", 223 | displayName: "Color", 224 | value: { value: "#000000" } 225 | }); 226 | 227 | name: string = "enableAxis"; 228 | displayName: string = "Enable Axis"; 229 | slices = [this.show, this.fill]; 230 | } 231 | ``` 232 | 233 | And `formattingSettings` object will get the right value from `dataView` or default value if it wasn't customized in `populateFormattingSettingsModel` method: 234 | ```typescript 235 | public update(options: VisualUpdateOptions) { 236 | this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(BarChartSettingsModel, options.dataViews); 237 | this.barDataPoints = createSelectorDataPoints(options, this.host); 238 | this.formattingSettings.populateColorSelector(this.barDataPoints); 239 | 240 | // ... 241 | } 242 | ``` 243 | 244 | ## Populate Property Pane with `getFormattingModel` 245 | `getFormattingModel` is an optional method on `IVisual`. It returns properties pane formatting model content hierarchies, properties and latest formatting values, Then place them within the property pane properties pane. This method is called once every time we open properties pane or when the user edit any format property. 246 | It can be built with the help of `formattingSettingsService` by calling method `buildFormattingModel`, Where it takes `formattingSettings` and convert it to PBI required `FormattingModel`/ 247 | 248 | ```typescript 249 | 250 | /** 251 | * Returns properties pane formatting model content hierarchies, properties and latest formatting values, Then populate properties pane. 252 | * This method is called once every time we open properties pane or when the user edit any format property. 253 | */ 254 | public getFormattingModel(): powerbiVisualsApi.visuals.FormattingModel { 255 | return this.formattingSettingsService.buildFormattingModel(this.formattingSettings); 256 | } 257 | ``` 258 | 259 | ## Control Property Logic in Update 260 | Once an object has been added to the property pane, each toggle will trigger an update. 261 | Add specific object logic in `if` blocks. 262 | ```typescript 263 | if (this.formattingSettings.enableAxis.show.value) { 264 | let margins = BarChart.Config.margins; 265 | height -= margins.bottom; 266 | } 267 | ``` -------------------------------------------------------------------------------- /Tutorial/StaticVisual.md: -------------------------------------------------------------------------------- 1 | # Building a Static Visual 2 | Typically, it is easier to build your visual with static data before adding PowerBIs data binding. 3 | See [commit](https://github.com/Microsoft/PowerBI-visuals-sampleBarChart/commit/f5ef02a5851c98671b46fedc1e7f7e7133001d7c) for what was added at this step. 4 | 5 | ## Setting up ViewModel 6 | It is important to define your view model now and iterate on what is exposed to your visual as you are building it. 7 | 8 | ```typescript 9 | /** 10 | * Interface for BarCharts viewmodel. 11 | * 12 | * @interface 13 | * @property {BarChartDataPoint[]} dataPoints - Set of data points the visual will render. 14 | * @property {number} dataMax - Maximum data value in the set of data points. 15 | */ 16 | interface BarChartViewModel { 17 | dataPoints: BarChartDataPoint[]; 18 | dataMax: number; 19 | }; 20 | 21 | /** 22 | * Interface for BarChart data points. 23 | * 24 | * @interface 25 | * @property {number} value - Data value for point. 26 | * @property {string} category - Coresponding category of data value. 27 | */ 28 | interface BarChartDataPoint { 29 | value: number; 30 | category: string; 31 | }; 32 | ``` 33 | 34 | ## Using Static Data 35 | Using static data is a great way to test your visual without databinding. Notice your view model will not change even when 36 | databinding is added. We will go into how to add databinding to your visual later. 37 | 38 | ```typescript 39 | let testData: BarChartDataPoint[] = [ 40 | { 41 | value: 10, 42 | category: 'a' 43 | }, 44 | { 45 | value: 20, 46 | category: 'b' 47 | }, 48 | { 49 | value: 1, 50 | category: 'c' 51 | }, 52 | { 53 | value: 100, 54 | category: 'd' 55 | }, 56 | { 57 | value: 500, 58 | category: 'e' 59 | }]; 60 | 61 | let viewModel: BarChartViewModel = { 62 | dataPoints: testData, 63 | dataMax: d3.max(testData.map((dataPoint) => dataPoint.value)) 64 | }; 65 | ``` -------------------------------------------------------------------------------- /Tutorial/Typings.md: -------------------------------------------------------------------------------- 1 | # Installing Typings for d3 2 | Installing typings will give you access to d3 types so you can utilize typescript types. 3 | 4 | For a more in depth details about typings, visit their repo. [Typings Documentation](https://github.com/typings/typings) 5 | 6 | ## Install Typings CLI 7 | In order to use typings in your project, you must first install typings to your computer. This is a one time installation. 8 | ```javascript 9 | // install typings globally 10 | npm install typings -g 11 | ``` 12 | ## Add Typings to your Project 13 | To have types for a specific library, run the following command. 14 | 15 | * **save** - Persists the types within this project. 16 | * **global** - Tells typings that we want this dependency to be globally available. 17 | 18 | ```javascript 19 | // install d3 typings 20 | typings install --save --global dt~d3 21 | ``` 22 | 23 | You should now have a typings directory in your visual project. 24 | 25 | ![](images/InstallTypings.png) 26 | 27 | ## Adding Typings to your PowerBI Visual 28 | Open your `tsconfig.json` file and add your `index.d.ts` to your list of files 29 | 30 | ![](images/AddTypings.png) 31 | 32 | ## Utilize Types in your Visual 33 | Now you should be able to use types within your code. 34 | 35 | ![](images/UsingTypings.png) -------------------------------------------------------------------------------- /Tutorial/images/AddTypings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/AddTypings.png -------------------------------------------------------------------------------- /Tutorial/images/ApplyReportPageTooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/ApplyReportPageTooltip.png -------------------------------------------------------------------------------- /Tutorial/images/CondFormatSupport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/CondFormatSupport.png -------------------------------------------------------------------------------- /Tutorial/images/ConditionalFormattingEntry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/ConditionalFormattingEntry.png -------------------------------------------------------------------------------- /Tutorial/images/DataBinding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/DataBinding.png -------------------------------------------------------------------------------- /Tutorial/images/ExternalLibraries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/ExternalLibraries.png -------------------------------------------------------------------------------- /Tutorial/images/HC_sampleBarChart_dark2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/HC_sampleBarChart_dark2.png -------------------------------------------------------------------------------- /Tutorial/images/HC_sampleBarChart_standard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/HC_sampleBarChart_standard.png -------------------------------------------------------------------------------- /Tutorial/images/HC_sampleBarChart_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/HC_sampleBarChart_white.png -------------------------------------------------------------------------------- /Tutorial/images/InstallTypings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/InstallTypings.png -------------------------------------------------------------------------------- /Tutorial/images/LocaleInSampleBarChart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/LocaleInSampleBarChart.png -------------------------------------------------------------------------------- /Tutorial/images/MakeExternalsDirectory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/MakeExternalsDirectory.png -------------------------------------------------------------------------------- /Tutorial/images/ObjectDataBoundProperty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/ObjectDataBoundProperty.png -------------------------------------------------------------------------------- /Tutorial/images/ObjectShowProperty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/ObjectShowProperty.png -------------------------------------------------------------------------------- /Tutorial/images/PropertyPane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/PropertyPane.png -------------------------------------------------------------------------------- /Tutorial/images/ReportPageTooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/ReportPageTooltip.png -------------------------------------------------------------------------------- /Tutorial/images/SampleBarChart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/SampleBarChart.png -------------------------------------------------------------------------------- /Tutorial/images/UsingTypings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/UsingTypings.png -------------------------------------------------------------------------------- /Tutorial/images/launchURLtoggle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/Tutorial/images/launchURLtoggle.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/PowerBI-visuals-sampleBarChart/89ad8782c83c22814dc3fc1b58dca67c60781330/assets/icon.png -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | pool: 7 | vmImage: 'Ubuntu 16.04' 8 | 9 | steps: 10 | - task: NodeTool@0 11 | inputs: 12 | versionSpec: '10.x' 13 | displayName: 'Install Node.js' 14 | 15 | - script: | 16 | npm install 17 | npm run lint 18 | npm run test 19 | displayName: 'npm install, lint and test' 20 | 21 | - task: PublishTestResults@2 22 | inputs: 23 | testResultsFormat: 'JUnit' 24 | testResultsFiles: '**/TESTS-*.xml' 25 | testRunTitle: 'Custom Visual unit-tests' 26 | mergeTestResults: true 27 | 28 | #- task: PublishCodeCoverageResults@1 29 | # inputs: 30 | # codeCoverageTool: 'cobertura' # Options: cobertura, jaCoCo 31 | # summaryFileLocation: '**/*-coverage.xml' 32 | # reportDirectory: '**/html-report' 33 | # failIfCoverageEmpty: true -------------------------------------------------------------------------------- /capabilities.json: -------------------------------------------------------------------------------- 1 | { 2 | "dataRoles": [ 3 | { 4 | "displayName": "Category Data", 5 | "name": "category", 6 | "kind": "Grouping" 7 | }, 8 | { 9 | "displayName": "Measure Data", 10 | "name": "measure", 11 | "kind": "Measure" 12 | }, 13 | { 14 | "displayName": "Tooltips", 15 | "name": "Tooltips", 16 | "kind": "Measure" 17 | } 18 | ], 19 | "dataViewMappings": [ 20 | { 21 | "conditions": [ 22 | { 23 | "category": { 24 | "max": 1 25 | }, 26 | "measure": { 27 | "max": 1 28 | } 29 | } 30 | ], 31 | "categorical": { 32 | "categories": { 33 | "for": { 34 | "in": "category" 35 | } 36 | }, 37 | "values": { 38 | "select": [ 39 | { 40 | "bind": { 41 | "to": "measure" 42 | } 43 | } 44 | ] 45 | } 46 | } 47 | } 48 | ], 49 | "objects": { 50 | "general": { 51 | "properties": { 52 | "formatString": { 53 | "type": { 54 | "formatting": { 55 | "formatString": true 56 | } 57 | } 58 | } 59 | } 60 | }, 61 | "enableAxis": { 62 | "properties": { 63 | "show": { 64 | "type": { 65 | "bool": true 66 | } 67 | }, 68 | "fill": { 69 | "type": { 70 | "fill": { 71 | "solid": { 72 | "color": true 73 | } 74 | } 75 | } 76 | } 77 | } 78 | }, 79 | "directEdit": { 80 | "properties": { 81 | "show": { 82 | "type": { 83 | "bool": true 84 | } 85 | }, 86 | "textProperty": { 87 | "type": { 88 | "text": true 89 | } 90 | }, 91 | "fontFamily": { 92 | "type": { 93 | "formatting": { 94 | "fontFamily": true 95 | } 96 | } 97 | }, 98 | "fontSize": { 99 | "type": { 100 | "formatting": { 101 | "fontSize": true 102 | } 103 | } 104 | }, 105 | "bold": { 106 | "type": { 107 | "bool": true 108 | } 109 | }, 110 | "italic": { 111 | "type": { 112 | "bool": true 113 | } 114 | }, 115 | "underline": { 116 | "type": { 117 | "bool": true 118 | } 119 | }, 120 | "fontColor": { 121 | "type": { 122 | "fill": { 123 | "solid": { 124 | "color": true 125 | } 126 | } 127 | } 128 | }, 129 | "background": { 130 | "type": { 131 | "fill": { 132 | "solid": { 133 | "color": true 134 | } 135 | } 136 | } 137 | }, 138 | "position": { 139 | "type": { 140 | "enumeration": [ 141 | { 142 | "value": "Left", 143 | "displayName": "Left" 144 | }, 145 | { 146 | "value": "Right", 147 | "displayName": "Right" 148 | } 149 | ] 150 | } 151 | } 152 | } 153 | }, 154 | "colorSelector": { 155 | "properties": { 156 | "fill": { 157 | "type": { 158 | "fill": { 159 | "solid": { 160 | "color": true 161 | } 162 | } 163 | } 164 | } 165 | } 166 | }, 167 | "generalView": { 168 | "properties": { 169 | "opacity": { 170 | "type": { 171 | "integer": true 172 | } 173 | }, 174 | "showHelpLink": { 175 | "type": { 176 | "bool": true 177 | } 178 | } 179 | } 180 | }, 181 | "averageLine": { 182 | "objectCategory": 2, 183 | "properties": { 184 | "show": { 185 | "type": { 186 | "bool": true 187 | } 188 | }, 189 | "displayName": { 190 | "type": { 191 | "text": true 192 | } 193 | }, 194 | "fill": { 195 | "type": { 196 | "fill": { 197 | "solid": { 198 | "color": true 199 | } 200 | } 201 | } 202 | }, 203 | "showDataLabel": { 204 | "type": { 205 | "bool": true 206 | } 207 | } 208 | } 209 | } 210 | }, 211 | "supportsOnObjectFormatting": true, 212 | "enablePointerEventsFormatMode": true, 213 | "tooltips": { 214 | "supportedTypes": { 215 | "default": true, 216 | "canvas": true 217 | }, 218 | "roles": [ 219 | "Tooltips" 220 | ], 221 | "supportEnhancedTooltips": true 222 | }, 223 | "supportsLandingPage": false, 224 | "drilldown": { 225 | "roles": [ 226 | "category" 227 | ] 228 | }, 229 | "privileges": [] 230 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "visual", 3 | "version": "4.1.1.0", 4 | "scripts": { 5 | "pbiviz": "pbiviz", 6 | "start": "pbiviz start", 7 | "package": "pbiviz package", 8 | "lint": "npx eslint . --ext .js,.jsx,.ts,.tsx", 9 | "lint-fix": "npx eslint . --ext .js,.jsx,.ts,.tsx --fix", 10 | "test": "pbiviz package --resources --no-minify --no-pbiviz" 11 | }, 12 | "devDependencies": { 13 | "@types/d3-axis": "^3.0.4", 14 | "@types/d3-scale": "^4.0.5", 15 | "@types/d3-selection": "^3.0.7", 16 | "@typescript-eslint/eslint-plugin": "^6.7.3", 17 | "@typescript-eslint/parser": "^6.7.3", 18 | "d3-axis": "^3.0.0", 19 | "d3-scale": "^4.0.2", 20 | "d3-selection": "^3.0.0", 21 | "eslint": "^8.50.0", 22 | "eslint-plugin-powerbi-visuals": "^0.8.1", 23 | "powerbi-visuals-api": "~5.8.0", 24 | "powerbi-visuals-tools": "^5.3.0", 25 | "powerbi-visuals-utils-dataviewutils": "^6.0.1", 26 | "powerbi-visuals-utils-formattingmodel": "6.0.0", 27 | "powerbi-visuals-utils-formattingutils": "^6.0.1", 28 | "powerbi-visuals-utils-interactivityutils": "^6.0.2", 29 | "powerbi-visuals-utils-tooltiputils": "^6.0.1", 30 | "typescript": "^5.1.6" 31 | }, 32 | "dependencies": { 33 | "powerbi-visuals-utils-onobjectutils": "^6.0.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pbiviz.json: -------------------------------------------------------------------------------- 1 | { 2 | "visual": { 3 | "name": "Bar Chart", 4 | "displayName": "barChart", 5 | "guid": "PBI_CV_9894B302_1DFF_4A96_ABFE_BF8588197166", 6 | "visualClassName": "BarChart", 7 | "version": "4.1.1.0", 8 | "description": "Sample bar chart", 9 | "supportUrl": "pbicvsupport@microsoft.com", 10 | "gitHubUrl": "" 11 | }, 12 | "apiVersion": "5.8.0", 13 | "author": { 14 | "name": "Author name", 15 | "email": "author@mail.com" 16 | }, 17 | "assets": { 18 | "icon": "assets/icon.png" 19 | }, 20 | "externalJS": [], 21 | "style": "style/visual.less", 22 | "capabilities": "capabilities.json" 23 | } -------------------------------------------------------------------------------- /src/barChart.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseType, 3 | select as d3Select, 4 | Selection as d3Selection 5 | } from "d3-selection"; 6 | import { 7 | scaleBand, 8 | scaleLinear, 9 | ScaleLinear, 10 | ScaleBand 11 | } from "d3-scale"; 12 | 13 | import { Axis, axisBottom } from "d3-axis"; 14 | 15 | import powerbi from "powerbi-visuals-api"; 16 | import { createTooltipServiceWrapper, ITooltipServiceWrapper } from "powerbi-visuals-utils-tooltiputils"; 17 | import { FormattingSettingsService } from "powerbi-visuals-utils-formattingmodel"; 18 | import { textMeasurementService, valueFormatter } from "powerbi-visuals-utils-formattingutils"; 19 | import { 20 | HtmlSubSelectableClass, HtmlSubSelectionHelper, SubSelectableDirectEdit as SubSelectableDirectEditAttr, 21 | SubSelectableDisplayNameAttribute, SubSelectableObjectNameAttribute, SubSelectableTypeAttribute 22 | } from "powerbi-visuals-utils-onobjectutils"; 23 | import { dataViewObjects} from "powerbi-visuals-utils-dataviewutils"; 24 | 25 | import { BarChartSettingsModel } from "./barChartSettingsModel"; 26 | 27 | import "./../style/visual.less"; 28 | 29 | type Selection = d3Selection; 30 | 31 | // powerbi.visuals 32 | import CustomVisualSubSelection = powerbi.visuals.CustomVisualSubSelection; 33 | import DataViewCategoryColumn = powerbi.DataViewCategoryColumn; 34 | import Fill = powerbi.Fill; 35 | import ISandboxExtendedColorPalette = powerbi.extensibility.ISandboxExtendedColorPalette; 36 | import ISelectionId = powerbi.visuals.ISelectionId; 37 | import ISelectionManager = powerbi.extensibility.ISelectionManager; 38 | import IVisual = powerbi.extensibility.IVisual; 39 | import IVisualHost = powerbi.extensibility.visual.IVisualHost; 40 | import PrimitiveValue = powerbi.PrimitiveValue; 41 | import SubSelectableDirectEdit = powerbi.visuals.SubSelectableDirectEdit; 42 | import SubSelectableDirectEditStyle = powerbi.visuals.SubSelectableDirectEditStyle; 43 | import SubSelectionStyles = powerbi.visuals.SubSelectionStyles; 44 | import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions; 45 | import DataViewObjectPropertyIdentifier = powerbi.DataViewObjectPropertyIdentifier; 46 | import VisualShortcutType = powerbi.visuals.VisualShortcutType; 47 | import VisualSubSelectionShortcuts = powerbi.visuals.VisualSubSelectionShortcuts; 48 | import VisualTooltipDataItem = powerbi.extensibility.VisualTooltipDataItem; 49 | import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions; 50 | import SubSelectionStylesType = powerbi.visuals.SubSelectionStylesType; 51 | import FormattingId = powerbi.visuals.FormattingId; 52 | import ILocalizationManager = powerbi.extensibility.ILocalizationManager; 53 | 54 | 55 | /** 56 | * Interface for BarChart data points. 57 | * 58 | * @interface 59 | * @property {PrimitiveValue} value - Data value for point. 60 | * @property {string} category - Corresponding category of data value. 61 | * @property {string} color - Color corresponding to data point. 62 | * @property {string} strokeColor - Stroke color for data point column. 63 | * @property {number} strokeWidth - Stroke width for data point column. 64 | * @property {ISelectionId} selectionId - Id assigned to data point for cross filtering 65 | * and visual interaction. 66 | */ 67 | export interface BarChartDataPoint { 68 | value: PrimitiveValue; 69 | category: string; 70 | color: string; 71 | strokeColor: string; 72 | strokeWidth: number; 73 | selectionId: ISelectionId; 74 | index: number; 75 | format?: string; 76 | } 77 | 78 | interface References { 79 | cardUid?: string; 80 | groupUid?: string; 81 | fill?: FormattingId; 82 | font?: FormattingId; 83 | fontColor?: FormattingId; 84 | show?: FormattingId; 85 | fontFamily?: FormattingId; 86 | bold?: FormattingId; 87 | italic?: FormattingId; 88 | underline?: FormattingId; 89 | fontSize?: FormattingId; 90 | position?: FormattingId; 91 | textProperty?: FormattingId; 92 | } 93 | 94 | const enum BarChartObjectNames { 95 | ArcElement = "arcElement", 96 | ColorSelector = "colorSelector", 97 | EnableAxis = "enableAxis", 98 | DirectEdit = "directEdit" 99 | } 100 | 101 | const DirectEdit: SubSelectableDirectEdit = { 102 | reference: { 103 | objectName: "directEdit", 104 | propertyName: "textProperty" 105 | }, 106 | style: SubSelectableDirectEditStyle.Outline, 107 | }; 108 | 109 | const colorSelectorReferences: References = { 110 | cardUid: "Visual-colorSelector-card", 111 | groupUid: "colorSelector-group", 112 | fill: { 113 | objectName: BarChartObjectNames.ColorSelector, 114 | propertyName: "fill" 115 | } 116 | }; 117 | 118 | const enableAxisReferences: References = { 119 | cardUid: "Visual-enableAxis-card", 120 | groupUid: "enableAxis-group", 121 | fill: { 122 | objectName: BarChartObjectNames.EnableAxis, 123 | propertyName: "fill" 124 | }, 125 | show: { 126 | objectName: BarChartObjectNames.EnableAxis, 127 | propertyName: "show" 128 | } 129 | }; 130 | 131 | const directEditReferences: References = { 132 | cardUid: "Visual-directEdit-card", 133 | groupUid: "directEdit-group", 134 | fontFamily: { 135 | objectName: BarChartObjectNames.DirectEdit, 136 | propertyName: "fontFamily" 137 | }, 138 | bold: { 139 | objectName: BarChartObjectNames.DirectEdit, 140 | propertyName: "bold" 141 | }, 142 | italic: { 143 | objectName: BarChartObjectNames.DirectEdit, 144 | propertyName: "italic" 145 | }, 146 | underline: { 147 | objectName: BarChartObjectNames.DirectEdit, 148 | propertyName: "underline" 149 | }, 150 | fontSize: { 151 | objectName: BarChartObjectNames.DirectEdit, 152 | propertyName: "fontSize" 153 | }, 154 | fontColor: { 155 | objectName: BarChartObjectNames.DirectEdit, 156 | propertyName: "fontColor" 157 | }, 158 | show: { 159 | objectName: BarChartObjectNames.DirectEdit, 160 | propertyName: "show" 161 | }, 162 | position: { 163 | objectName: BarChartObjectNames.DirectEdit, 164 | propertyName: "position" 165 | }, 166 | textProperty: { 167 | objectName: BarChartObjectNames.DirectEdit, 168 | propertyName: "textProperty" 169 | } 170 | }; 171 | 172 | /** 173 | * Function that converts queried data into a view model that will be used by the visual. 174 | * 175 | * @function 176 | * @param {VisualUpdateOptions} options - Contains references to the size of the container 177 | * and the dataView which contains all the data 178 | * the visual had queried. 179 | * @param {IVisualHost} host - Contains references to the host which contains services 180 | */ 181 | function createSelectorDataPoints(options: VisualUpdateOptions, host: IVisualHost): BarChartDataPoint[] { 182 | const barChartDataPoints: BarChartDataPoint[] = [] 183 | const dataViews = options.dataViews; 184 | 185 | if (!dataViews 186 | || !dataViews[0] 187 | || !dataViews[0].categorical 188 | || !dataViews[0].categorical.categories 189 | || !dataViews[0].categorical.categories[0].source 190 | || !dataViews[0].categorical.values 191 | ) { 192 | return barChartDataPoints; 193 | } 194 | 195 | const categorical = dataViews[0].categorical; 196 | const category = categorical.categories[0]; 197 | const dataValue = categorical.values[0]; 198 | 199 | //let dataMax: number = 0; 200 | 201 | const colorPalette: ISandboxExtendedColorPalette = host.colorPalette; 202 | //const objects = dataViews[0].metadata.objects; 203 | 204 | const strokeColor: string = getColumnStrokeColor(colorPalette); 205 | 206 | const strokeWidth: number = getColumnStrokeWidth(colorPalette.isHighContrast); 207 | 208 | for (let i = 0, len = Math.max(category.values.length, dataValue.values.length); i < len; i++) { 209 | const color: string = getColumnColorByIndex(category, i, colorPalette); 210 | 211 | const selectionId: ISelectionId = host.createSelectionIdBuilder() 212 | .withCategory(category, i) 213 | .createSelectionId(); 214 | 215 | barChartDataPoints.push({ 216 | color, 217 | strokeColor, 218 | strokeWidth, 219 | selectionId, 220 | value: dataValue.values[i], 221 | category: `${category.values[i]}`, 222 | index: i, 223 | format: dataValue.objects ? dataValue.objects[i].general.formatString : null, 224 | }); 225 | } 226 | 227 | return barChartDataPoints; 228 | } 229 | 230 | function getColumnColorByIndex( 231 | category: DataViewCategoryColumn, 232 | index: number, 233 | colorPalette: ISandboxExtendedColorPalette, 234 | ): string { 235 | if (colorPalette.isHighContrast) { 236 | return colorPalette.background.value; 237 | } 238 | 239 | const defaultColor: Fill = { 240 | solid: { 241 | color: colorPalette.getColor(`${category.values[index]}`).value, 242 | } 243 | }; 244 | 245 | const prop: DataViewObjectPropertyIdentifier = { 246 | objectName: "colorSelector", 247 | propertyName: "fill" 248 | }; 249 | 250 | let colorFromObjects: Fill; 251 | if(category.objects?.[index]){ 252 | colorFromObjects = dataViewObjects.getValue(category?.objects[index], prop); 253 | } 254 | 255 | return colorFromObjects?.solid.color ?? defaultColor.solid.color; 256 | } 257 | 258 | function getColumnStrokeColor(colorPalette: ISandboxExtendedColorPalette): string { 259 | return colorPalette.isHighContrast 260 | ? colorPalette.foreground.value 261 | : null; 262 | } 263 | 264 | function getColumnStrokeWidth(isHighContrast: boolean): number { 265 | return isHighContrast 266 | ? 2 267 | : 0; 268 | } 269 | 270 | export class BarChart implements IVisual { 271 | private averageLine: Selection; 272 | private barContainer: Selection; 273 | private barDataPoints: BarChartDataPoint[]; 274 | private element: HTMLElement; 275 | private formattingSettingsService: FormattingSettingsService; 276 | private formattingSettings: BarChartSettingsModel; 277 | private helpLinkElement: Selection; 278 | private host: IVisualHost; 279 | private isLandingPageOn: boolean; 280 | private LandingPage: Selection; 281 | private LandingPageRemoved: boolean; 282 | private locale: string; 283 | private selectionManager: ISelectionManager; 284 | private svg: Selection; 285 | private tooltipServiceWrapper: ITooltipServiceWrapper; 286 | private xAxis: Selection; 287 | private barSelection: Selection; 288 | private localizationManager: ILocalizationManager; 289 | 290 | private subSelectionHelper: HtmlSubSelectionHelper; 291 | private formatMode: boolean = false; 292 | private directEditElement: Selection; 293 | private visualDirectEditSubSelection = JSON.stringify(DirectEdit); 294 | public visualOnObjectFormatting?: powerbi.extensibility.visual.VisualOnObjectFormatting; 295 | 296 | static Config = { 297 | xScalePadding: 0.1, 298 | solidOpacity: 1, 299 | transparentOpacity: 0.4, 300 | margins: { 301 | top: 0, 302 | right: 0, 303 | bottom: 25, 304 | left: 30, 305 | }, 306 | xAxisFontMultiplier: 0.04, 307 | }; 308 | 309 | /** 310 | * Creates instance of BarChart. This method is only called once. 311 | * 312 | * @constructor 313 | * @param {VisualConstructorOptions} options - Contains references to the element that will 314 | * contain the visual and a reference to the host 315 | * which contains services. 316 | */ 317 | constructor(options: VisualConstructorOptions) { 318 | this.host = options.host; 319 | this.element = options.element; 320 | this.selectionManager = options.host.createSelectionManager(); 321 | this.locale = options.host.locale; 322 | 323 | this.selectionManager.registerOnSelectCallback(() => { 324 | this.syncSelectionState(this.barSelection, this.selectionManager.getSelectionIds()); 325 | }); 326 | 327 | this.tooltipServiceWrapper = createTooltipServiceWrapper(this.host.tooltipService, options.element); 328 | 329 | //Creating the formatting settings service. 330 | this.localizationManager = this.host.createLocalizationManager(); 331 | this.formattingSettingsService = new FormattingSettingsService(this.localizationManager); 332 | 333 | this.subSelectionHelper = HtmlSubSelectionHelper.createHtmlSubselectionHelper({ 334 | hostElement: options.element, 335 | subSelectionService: options.host.subSelectionService, 336 | selectionIdCallback: (e) => this.selectionIdCallback(e), 337 | }); 338 | 339 | this.svg = d3Select(options.element) 340 | .append("svg") 341 | .classed("barChart", true); 342 | 343 | this.barContainer = this.svg 344 | .append("g") 345 | .classed("barContainer", true); 346 | 347 | this.xAxis = this.svg 348 | .append("g") 349 | .classed("xAxis", true); 350 | 351 | this.initAverageLine(); 352 | 353 | const helpLinkElement: Element = this.createHelpLinkElement(); 354 | options.element.appendChild(helpLinkElement); 355 | 356 | this.helpLinkElement = d3Select(helpLinkElement); 357 | 358 | //create direct edit box 359 | const directEditDiv = this.creatDirectEditElement(); 360 | options.element.appendChild(directEditDiv); 361 | this.directEditElement = d3Select(directEditDiv); 362 | 363 | this.visualOnObjectFormatting = { 364 | getSubSelectionStyles: (subSelections) => this.getSubSelectionStyles(subSelections), 365 | getSubSelectionShortcuts: (subSelections) => this.getSubSelectionShortcuts(subSelections), 366 | getSubSelectables: (filter) => this.getSubSelectables(filter) 367 | }; 368 | 369 | this.handleContextMenu(); 370 | } 371 | 372 | /** 373 | * Updates the state of the visual. Every sequential databinding and resize will call update. 374 | * 375 | * @function 376 | * @param {VisualUpdateOptions} options - Contains references to the size of the container 377 | * and the dataView which contains all the data 378 | * the visual had queried. 379 | */ 380 | public update(options: VisualUpdateOptions) { 381 | // Turn on landing page in capabilities and remove comment to turn on landing page! 382 | // this.HandleLandingPage(options); 383 | this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(BarChartSettingsModel, options.dataViews?.[0]); 384 | this.barDataPoints = createSelectorDataPoints(options, this.host); 385 | this.formattingSettings.populateColorSelector(this.barDataPoints); 386 | this.formatMode = options.formatMode; 387 | const width = options.viewport.width; 388 | let height = options.viewport.height; 389 | 390 | this.svg 391 | .attr("width", width) 392 | .attr("height", height); 393 | 394 | if (this.formattingSettings.enableAxis.show.value) { 395 | const margins = BarChart.Config.margins; 396 | height -= margins.bottom; 397 | } 398 | 399 | this.helpLinkElement 400 | .classed("hidden", !this.formattingSettings.generalView.showHelpLink.value) 401 | .style("border-color", this.formattingSettings.generalView.helpLinkColor) 402 | .style("color", this.formattingSettings.generalView.helpLinkColor); 403 | 404 | this.updateDirectEditElementFormat(); 405 | this.xAxis 406 | .style("font-size", Math.min(height, width) * BarChart.Config.xAxisFontMultiplier) 407 | .style("fill", this.formattingSettings.enableAxis.fill.value.value); 408 | 409 | const yScale: ScaleLinear = scaleLinear() 410 | .domain([0, options.dataViews[0].categorical.values[0].maxLocal]) 411 | .range([height, 0]); 412 | 413 | const xScale: ScaleBand = scaleBand() 414 | .domain(this.barDataPoints.map(d => d.category)) 415 | .rangeRound([0, width]) 416 | .padding(0.2); 417 | 418 | const xAxis: Axis = axisBottom(xScale); 419 | this.xAxis.attr("transform", "translate(0, " + height + ")") 420 | .call(xAxis) 421 | .attr("color", this.formattingSettings.enableAxis.fill.value.value); 422 | 423 | const textNodes: Selection = this.xAxis.selectAll("text"); 424 | textNodes 425 | .attr(SubSelectableObjectNameAttribute, "enableAxis") 426 | .attr(SubSelectableDisplayNameAttribute, "x-Axis") 427 | .attr(SubSelectableTypeAttribute, powerbi.visuals.SubSelectionStylesType.Shape) 428 | .classed(HtmlSubSelectableClass, options.formatMode && this.formattingSettings.enableAxis.show.value); 429 | BarChart.wordBreak(textNodes, xScale.bandwidth(), height); 430 | this.handleAverageLineUpdate(height, width, yScale); 431 | 432 | this.barSelection = this.barContainer 433 | .selectAll(".bar") 434 | .data(this.barDataPoints); 435 | 436 | const barSelectionMerged = this.barSelection 437 | .enter() 438 | .append("rect") 439 | .merge(this.barSelection); 440 | 441 | barSelectionMerged.classed("bar", true); 442 | 443 | const opacity: number = this.formattingSettings.generalView.opacity.value / 100; 444 | barSelectionMerged 445 | .attr(SubSelectableObjectNameAttribute, "colorSelector") 446 | .attr(SubSelectableDisplayNameAttribute, (dataPoint: BarChartDataPoint) => this.formattingSettings.colorSelector.slices[dataPoint.index].displayName) 447 | .attr(SubSelectableTypeAttribute, powerbi.visuals.SubSelectionStylesType.Shape) 448 | .classed(HtmlSubSelectableClass, options.formatMode) 449 | .attr("width", xScale.bandwidth()) 450 | .attr("height", (dataPoint: BarChartDataPoint) => height - yScale(dataPoint.value)) 451 | .attr("y", (dataPoint: BarChartDataPoint) => yScale(dataPoint.value)) 452 | .attr("x", (dataPoint: BarChartDataPoint) => xScale(dataPoint.category)) 453 | .style("fill-opacity", opacity) 454 | .style("stroke-opacity", opacity) 455 | .style("fill", (dataPoint: BarChartDataPoint) => dataPoint.color) 456 | .style("stroke", (dataPoint: BarChartDataPoint) => dataPoint.strokeColor) 457 | .style("stroke-width", (dataPoint: BarChartDataPoint) => `${dataPoint.strokeWidth}px`); 458 | 459 | this.tooltipServiceWrapper.addTooltip(barSelectionMerged, 460 | (dataPoint: BarChartDataPoint) => this.getTooltipData(dataPoint), 461 | (dataPoint: BarChartDataPoint) => dataPoint.selectionId 462 | ); 463 | 464 | this.syncSelectionState( 465 | barSelectionMerged, 466 | this.selectionManager.getSelectionIds() 467 | ); 468 | if (this.formatMode) { 469 | this.removeEventHandlers(barSelectionMerged); 470 | } else { 471 | this.addEventHandlers(barSelectionMerged); 472 | } 473 | 474 | this.subSelectionHelper.setFormatMode(options.formatMode); 475 | const shouldUpdateSubSelection = options.type & (powerbi.VisualUpdateType.Data 476 | | powerbi.VisualUpdateType.Resize 477 | | powerbi.VisualUpdateType.FormattingSubSelectionChange); 478 | if (this.formatMode && shouldUpdateSubSelection) { 479 | this.subSelectionHelper.updateOutlinesFromSubSelections(options.subSelections, true); 480 | } 481 | 482 | this.barSelection 483 | .exit() 484 | .remove(); 485 | this.handleClick(barSelectionMerged); 486 | } 487 | 488 | private removeEventHandlers(barSelectionMerged: d3Selection) { 489 | barSelectionMerged.on("click", null); 490 | this.svg.on("click", null); 491 | this.svg.on("contextmenu", null); 492 | } 493 | 494 | private addEventHandlers(barSelectionMerged: d3Selection) { 495 | this.handleBarClick(barSelectionMerged); 496 | this.handleClick(barSelectionMerged); 497 | this.handleContextMenu(); 498 | } 499 | 500 | private updateDirectEditElementFormat() { 501 | this.directEditElement 502 | .classed("direct-edit", true) 503 | .classed("hidden", !this.formattingSettings.directEditSettings.show.value) 504 | .classed(HtmlSubSelectableClass, this.formatMode && this.formattingSettings.directEditSettings.show.value) 505 | .attr(SubSelectableObjectNameAttribute, "directEdit") 506 | .attr(SubSelectableDisplayNameAttribute, "Direct Edit") 507 | .attr(SubSelectableDirectEditAttr, this.visualDirectEditSubSelection) 508 | .style("font-family", this.formattingSettings.directEditSettings.font.fontFamily.value) 509 | .style("color", this.formattingSettings.directEditSettings.fontColor.value.value) 510 | .style("font-style", this.formattingSettings.directEditSettings.font.italic.value ? "italic" : "normal") 511 | .style("text-decoration", this.formattingSettings.directEditSettings.font.underline.value ? "underline" : "none") 512 | .style("font-weight", this.formattingSettings.directEditSettings.font.bold.value ? "bold" : "normal") 513 | .style("right", this.formattingSettings.directEditSettings.position.value === "Right" ? "12px" : "60px") 514 | .style("background-color", this.formattingSettings.directEditSettings.background.value.value) 515 | .style("font-size", `${this.formattingSettings.directEditSettings.font.fontSize.value}px`) 516 | .text(this.formattingSettings.directEditSettings.textProperty.value); 517 | } 518 | private static wordBreak( 519 | textNodes: Selection, 520 | allowedWidth: number, 521 | maxHeight: number 522 | ) { 523 | textNodes.each(function () { 524 | textMeasurementService.wordBreak( 525 | this, 526 | allowedWidth, 527 | maxHeight); 528 | }); 529 | } 530 | 531 | private handleBarClick(barSelectionMerged: Selection) { 532 | barSelectionMerged.on("click", (event: Event, datum: BarChartDataPoint) => { 533 | // Allow selection only if the visual is rendered in a view that supports interactivity (e.g. Report) 534 | if (this.host.hostCapabilities.allowInteractions) { 535 | const isCtrlPressed: boolean = (event).ctrlKey; 536 | 537 | this.selectionManager 538 | .select(datum.selectionId, isCtrlPressed) 539 | .then((ids: ISelectionId[]) => { 540 | this.syncSelectionState(barSelectionMerged, ids); 541 | }); 542 | event.stopPropagation(); 543 | } 544 | }); 545 | } 546 | 547 | private handleClick(barSelection: Selection) { 548 | // Clear selection when clicking outside a bar 549 | this.svg.on("click", () => { 550 | if (this.host.hostCapabilities.allowInteractions) { 551 | this.selectionManager 552 | .clear() 553 | .then(() => { 554 | this.syncSelectionState(barSelection, []); 555 | }); 556 | } 557 | }); 558 | } 559 | 560 | private handleContextMenu() { 561 | this.svg.on("contextmenu", (event) => { 562 | const mouseEvent: MouseEvent = event; 563 | const eventTarget: EventTarget = mouseEvent.target; 564 | const dataPoint: any = d3Select(eventTarget).datum(); 565 | this.selectionManager.showContextMenu(dataPoint ? dataPoint.selectionId : {}, { 566 | x: mouseEvent.clientX, 567 | y: mouseEvent.clientY 568 | }); 569 | mouseEvent.preventDefault(); 570 | }); 571 | } 572 | 573 | private syncSelectionState( 574 | selection: Selection, 575 | selectionIds: ISelectionId[] 576 | ): void { 577 | if (!selection || !selectionIds) { 578 | return; 579 | } 580 | 581 | if (!selectionIds.length) { 582 | const opacity: number = this.formattingSettings.generalView.opacity.value / 100; 583 | selection 584 | .style("fill-opacity", opacity) 585 | .style("stroke-opacity", opacity); 586 | return; 587 | } 588 | // eslint-disable-next-line 589 | const self: this = this; 590 | 591 | selection.each(function (barDataPoint: BarChartDataPoint) { 592 | const isSelected: boolean = self.isSelectionIdInArray(selectionIds, barDataPoint.selectionId); 593 | 594 | const opacity: number = isSelected 595 | ? BarChart.Config.solidOpacity 596 | : BarChart.Config.transparentOpacity; 597 | 598 | d3Select(this) 599 | .style("fill-opacity", opacity) 600 | .style("stroke-opacity", opacity); 601 | }); 602 | } 603 | 604 | private isSelectionIdInArray(selectionIds: ISelectionId[], selectionId: ISelectionId): boolean { 605 | if (!selectionIds || !selectionId) { 606 | return false; 607 | } 608 | 609 | return selectionIds.some((currentSelectionId: ISelectionId) => { 610 | return currentSelectionId.includes(selectionId); 611 | }); 612 | } 613 | 614 | /** 615 | * Returns properties pane formatting model content hierarchies, properties and latest formatting values, Then populate properties pane. 616 | * This method is called once every time we open properties pane or when the user edit any format property. 617 | */ 618 | public getFormattingModel(): powerbi.visuals.FormattingModel { 619 | return this.formattingSettingsService.buildFormattingModel(this.formattingSettings); 620 | } 621 | 622 | private getSubSelectionStyles(subSelections: CustomVisualSubSelection[]): powerbi.visuals.SubSelectionStyles | undefined { 623 | const visualObject = subSelections[0]?.customVisualObjects[0]; 624 | if (visualObject) { 625 | switch (visualObject.objectName) { 626 | case BarChartObjectNames.ColorSelector: 627 | return this.getColorSelectorStyles(subSelections); 628 | case BarChartObjectNames.EnableAxis: 629 | return this.getEnableAxisStyles(); 630 | case BarChartObjectNames.DirectEdit: 631 | return this.getDirectEditStyles(); 632 | } 633 | } 634 | } 635 | private getSubSelectionShortcuts(subSelections: CustomVisualSubSelection[]): VisualSubSelectionShortcuts | undefined { 636 | const visualObject = subSelections[0]?.customVisualObjects[0]; 637 | if (visualObject) { 638 | switch (visualObject.objectName) { 639 | case BarChartObjectNames.ColorSelector: 640 | return this.getColorSelectorShortcuts(subSelections); 641 | case BarChartObjectNames.EnableAxis: 642 | return this.getEnableAxisShortcuts(); 643 | case BarChartObjectNames.DirectEdit: 644 | return this.getDirectEditShortcuts(); 645 | } 646 | } 647 | } 648 | private getSubSelectables?(filter?: powerbi.visuals.SubSelectionStylesType): CustomVisualSubSelection[] | undefined { 649 | return this.subSelectionHelper.getAllSubSelectables(filter); 650 | } 651 | 652 | private getColorSelectorShortcuts(subSelections: CustomVisualSubSelection[]): VisualSubSelectionShortcuts { 653 | const selector = subSelections[0].customVisualObjects[0].selectionId?.getSelector(); 654 | return [ 655 | { 656 | type: VisualShortcutType.Reset, 657 | relatedResetFormattingIds: [{ 658 | ...colorSelectorReferences.fill, 659 | selector 660 | }], 661 | }, 662 | { 663 | type: VisualShortcutType.Navigate, 664 | destinationInfo: { cardUid: colorSelectorReferences.cardUid }, 665 | label: "Color" 666 | } 667 | ]; 668 | } 669 | 670 | private getColorSelectorStyles(subSelections: CustomVisualSubSelection[]): SubSelectionStyles { 671 | const selector = subSelections[0].customVisualObjects[0].selectionId?.getSelector(); 672 | return { 673 | type: SubSelectionStylesType.Shape, 674 | fill: { 675 | label: "Fill", 676 | reference: { 677 | ...colorSelectorReferences.fill, 678 | selector 679 | }, 680 | }, 681 | }; 682 | } 683 | 684 | private getEnableAxisStyles(): SubSelectionStyles { 685 | return { 686 | type: SubSelectionStylesType.Shape, 687 | fill: { 688 | reference: { 689 | ...enableAxisReferences.fill 690 | }, 691 | label: "Enable Axis" 692 | } 693 | } 694 | } 695 | 696 | private getEnableAxisShortcuts(): VisualSubSelectionShortcuts { 697 | return [ 698 | { 699 | type: VisualShortcutType.Reset, 700 | relatedResetFormattingIds: [{ 701 | ...enableAxisReferences.fill, 702 | }], 703 | excludedResetFormattingIds: [{ 704 | ...enableAxisReferences.show, 705 | }] 706 | }, 707 | { 708 | type: VisualShortcutType.Toggle, 709 | relatedToggledFormattingIds: [{ 710 | ...enableAxisReferences.show 711 | }], 712 | ...enableAxisReferences.show, 713 | disabledLabel: "Delete", 714 | enabledLabel: "Delete" 715 | }, 716 | { 717 | type: VisualShortcutType.Navigate, 718 | destinationInfo: { cardUid: enableAxisReferences.cardUid }, 719 | label: "EnableAxis" 720 | } 721 | ]; 722 | } 723 | 724 | private getDirectEditShortcuts(): VisualSubSelectionShortcuts { 725 | return [ 726 | { 727 | type: VisualShortcutType.Reset, 728 | relatedResetFormattingIds: [ 729 | directEditReferences.bold, 730 | directEditReferences.fontFamily, 731 | directEditReferences.fontSize, 732 | directEditReferences.italic, 733 | directEditReferences.underline, 734 | directEditReferences.fontColor, 735 | directEditReferences.textProperty 736 | ] 737 | }, 738 | { 739 | type: VisualShortcutType.Toggle, 740 | relatedToggledFormattingIds: [{ 741 | ...directEditReferences.show, 742 | }], 743 | ...directEditReferences.show, 744 | disabledLabel: "Delete", 745 | 746 | }, 747 | { 748 | type: VisualShortcutType.Picker, 749 | ...directEditReferences.position, 750 | label: "Position" 751 | }, 752 | { 753 | type: VisualShortcutType.Navigate, 754 | destinationInfo: { cardUid: directEditReferences.cardUid }, 755 | label: "Direct edit" 756 | } 757 | ]; 758 | } 759 | 760 | private getDirectEditStyles(): SubSelectionStyles { 761 | return { 762 | type: powerbi.visuals.SubSelectionStylesType.Text, 763 | fontFamily: { 764 | reference: { 765 | ...directEditReferences.fontFamily 766 | }, 767 | label: "font" 768 | }, 769 | bold: { 770 | reference: { 771 | ...directEditReferences.bold 772 | }, 773 | label: "font" 774 | }, 775 | italic: { 776 | reference: { 777 | ...directEditReferences.italic 778 | }, 779 | label: "font" 780 | }, 781 | underline: { 782 | reference: { 783 | ...directEditReferences.underline 784 | }, 785 | label: "font" 786 | }, 787 | fontSize: { 788 | reference: { 789 | ...directEditReferences.fontSize 790 | }, 791 | label: "font" 792 | }, 793 | fontColor: { 794 | reference: { 795 | ...directEditReferences.fontColor 796 | }, 797 | label: "fontColor" 798 | }, 799 | background: { 800 | reference: { 801 | objectName: "directEdit", 802 | propertyName: "background" 803 | }, 804 | label: "background" 805 | } 806 | }; 807 | } 808 | 809 | public selectionIdCallback(e: Element): ISelectionId { 810 | const elementType: string = d3Select(e).attr(SubSelectableObjectNameAttribute); 811 | let selectionId: ISelectionId = undefined; 812 | 813 | switch (elementType) { 814 | case BarChartObjectNames.ColorSelector: 815 | selectionId = d3Select(e).datum().selectionId; 816 | break; 817 | } 818 | 819 | return selectionId; 820 | } 821 | 822 | private creatDirectEditElement(): Element { 823 | const element = document.createElement("div"); 824 | element.setAttribute("class", "direct-edit"); 825 | return element; 826 | } 827 | 828 | /** 829 | * Destroy runs when the visual is removed. Any cleanup that the visual needs to 830 | * do should be done here. 831 | * 832 | * @function 833 | */ 834 | public destroy(): void { 835 | // Perform any cleanup tasks here 836 | } 837 | 838 | private getTooltipData(value: any): VisualTooltipDataItem[] { 839 | const formattedValue = valueFormatter.format(value.value, value.format); 840 | const language = this.localizationManager.getDisplayName("LanguageKey"); 841 | return [{ 842 | displayName: value.category, 843 | value: formattedValue, 844 | color: value.color, 845 | header: language && "displayed language " + language 846 | }]; 847 | } 848 | 849 | private createHelpLinkElement(): Element { 850 | const linkElement = document.createElement("a"); 851 | linkElement.textContent = "?"; 852 | linkElement.setAttribute("title", "Open documentation"); 853 | linkElement.setAttribute("class", "helpLink"); 854 | linkElement.addEventListener("click", () => { 855 | this.host.launchUrl("https://microsoft.github.io/PowerBI-visuals/tutorials/building-bar-chart/adding-url-launcher-element-to-the-bar-chart/"); 856 | }); 857 | return linkElement; 858 | } 859 | 860 | private handleLandingPage(options: VisualUpdateOptions) { 861 | if (!options.dataViews || !options.dataViews.length) { 862 | if (!this.isLandingPageOn) { 863 | this.isLandingPageOn = true; 864 | const SampleLandingPage: Element = this.createSampleLandingPage(); 865 | this.element.appendChild(SampleLandingPage); 866 | 867 | this.LandingPage = d3Select(SampleLandingPage); 868 | } 869 | 870 | } else { 871 | if (this.isLandingPageOn && !this.LandingPageRemoved) { 872 | this.LandingPageRemoved = true; 873 | this.LandingPage.remove(); 874 | } 875 | } 876 | } 877 | 878 | private createSampleLandingPage(): Element { 879 | const div = document.createElement("div"); 880 | 881 | const header = document.createElement("h1"); 882 | header.textContent = "Sample Bar Chart Landing Page"; 883 | header.setAttribute("class", "LandingPage"); 884 | const p1 = document.createElement("a"); 885 | p1.setAttribute("class", "LandingPageHelpLink"); 886 | p1.textContent = "Learn more about Landing page"; 887 | 888 | p1.addEventListener("click", () => { 889 | this.host.launchUrl("https://microsoft.github.io/PowerBI-visuals/docs/overview/"); 890 | }); 891 | 892 | div.appendChild(header); 893 | div.appendChild(p1); 894 | 895 | return div; 896 | } 897 | 898 | private getColorValue(color: Fill | string): string { 899 | // Override color settings if in high contrast mode 900 | if (this.host.colorPalette.isHighContrast) { 901 | return this.host.colorPalette.foreground.value; 902 | } 903 | 904 | // If plain string, just return it 905 | if (typeof (color) === "string") { 906 | return color; 907 | } 908 | // Otherwise, extract string representation from Fill type object 909 | return color.solid.color; 910 | } 911 | 912 | private initAverageLine() { 913 | this.averageLine = this.svg 914 | .append("g") 915 | .classed("averageLine", true); 916 | 917 | this.averageLine.append("line") 918 | .attr("id", "averageLine"); 919 | 920 | this.averageLine.append("text") 921 | .attr("id", "averageLineLabel"); 922 | } 923 | 924 | private handleAverageLineUpdate(height: number, width: number, yScale: ScaleLinear) { 925 | const average = this.calculateAverage(); 926 | const fontSize = Math.min(height, width) * BarChart.Config.xAxisFontMultiplier; 927 | const chosenColor = this.getColorValue(this.formattingSettings.averageLine.fill.value.value); 928 | // If there's no room to place label above line, place it below 929 | const labelYOffset = fontSize * ((yScale(average) > fontSize * 1.5) ? -0.5 : 1.5); 930 | 931 | this.averageLine 932 | .style("font-size", fontSize) 933 | .style("display", (this.formattingSettings.averageLine.show.value) ? "initial" : "none") 934 | .attr("transform", "translate(0, " + Math.round(yScale(average)) + ")"); 935 | 936 | this.averageLine.select("#averageLine") 937 | .style("stroke", chosenColor) 938 | .style("stroke-width", "3px") 939 | .style("stroke-dasharray", "6,6") 940 | .attr("x1", 0) 941 | .attr("x1", "" + width); 942 | 943 | this.averageLine.select("#averageLineLabel") 944 | .text("Average: " + average.toFixed(2)) 945 | .attr("transform", "translate(0, " + labelYOffset + ")") 946 | .style("fill", this.formattingSettings.averageLine.showDataLabel.value ? chosenColor : "none"); 947 | } 948 | 949 | private calculateAverage(): number { 950 | if (this.barDataPoints.length === 0) { 951 | return 0; 952 | } 953 | 954 | let total = 0; 955 | 956 | this.barDataPoints.forEach((value: BarChartDataPoint) => { 957 | total += value.value; 958 | }); 959 | 960 | return total / this.barDataPoints.length; 961 | } 962 | } 963 | -------------------------------------------------------------------------------- /src/barChartSettingsModel.ts: -------------------------------------------------------------------------------- 1 | import powerbiVisualsApi from "powerbi-visuals-api"; 2 | import { formattingSettings } from "powerbi-visuals-utils-formattingmodel"; 3 | import { BarChartDataPoint } from "./barChart"; 4 | 5 | import Card = formattingSettings.SimpleCard; 6 | import Model = formattingSettings.Model; 7 | import Slice = formattingSettings.Slice; 8 | import ColorPicker = formattingSettings.ColorPicker; 9 | import ToggleSwitch = formattingSettings.ToggleSwitch; 10 | import NumUpDown = formattingSettings.NumUpDown; 11 | import TextInput = formattingSettings.TextInput; 12 | import AutoDropdown = formattingSettings.AutoDropdown; 13 | import FontControl = formattingSettings.FontControl; 14 | import FontPicker = formattingSettings.FontPicker; 15 | 16 | class EnableAxisCardSettings extends Card { 17 | show = new ToggleSwitch({ 18 | name: "show", 19 | displayName: undefined, 20 | value: false, 21 | }); 22 | 23 | fill = new ColorPicker({ 24 | name: "fill", 25 | displayName: "Color", 26 | value: { value: "#000000" } 27 | }); 28 | topLevelSlice: ToggleSwitch = this.show; 29 | name: string = "enableAxis"; 30 | displayName: string = "Enable Axis"; 31 | slices: Slice[] = [this.fill]; 32 | } 33 | 34 | 35 | class ColorSelectorCardSettings extends Card { 36 | name: string = "colorSelector"; 37 | displayName: string = "Data Colors"; 38 | slices: Slice[] = []; 39 | } 40 | 41 | class GeneralViewCardSettings extends Card { 42 | opacity = new NumUpDown({ 43 | name: "opacity", 44 | displayName: "Bars Opacity", 45 | value: 100, 46 | options: { 47 | minValue: { 48 | type: powerbiVisualsApi.visuals.ValidatorType.Min, 49 | value: 0, 50 | }, 51 | maxValue: { 52 | type: powerbiVisualsApi.visuals.ValidatorType.Max, 53 | value: 100, 54 | } 55 | } 56 | }); 57 | 58 | showHelpLink = new ToggleSwitch({ 59 | name: "showHelpLink", 60 | displayName: "Show Help Button", 61 | value: false 62 | }); 63 | 64 | name: string = "generalView"; 65 | displayName: string = "General View"; 66 | helpLinkColor: string = "#80B0E0" 67 | slices: Slice[] = [this.opacity, this.showHelpLink]; 68 | } 69 | 70 | class AverageLineCardSettings extends Card { 71 | show = new ToggleSwitch({ 72 | name: "show", 73 | displayName: undefined, 74 | value: false, 75 | }); 76 | 77 | fill = new ColorPicker({ 78 | name: "fill", 79 | displayName: "Color", 80 | value: { value: "#888888" }, 81 | }); 82 | 83 | showDataLabel = new ToggleSwitch({ 84 | name: "showDataLabel", 85 | displayName: "Data Label", 86 | value: false 87 | }); 88 | 89 | topLevelSlice: ToggleSwitch = this.show; 90 | name: string = "averageLine"; 91 | displayName: string = "Average Line"; 92 | analyticsPane: boolean = true; 93 | slices: Slice[] = [this.show, this.fill, this.showDataLabel]; 94 | } 95 | 96 | class DirectEditSettings extends Card { 97 | displayName: string = "Direct Edit"; 98 | name: string = "directEdit"; 99 | private minFontSize: number = 8; 100 | private defaultFontSize: number = 11; 101 | show = new ToggleSwitch({ 102 | name: "show", 103 | displayName: undefined, 104 | value: true, 105 | }); 106 | 107 | topLevelSlice: ToggleSwitch = this.show; 108 | textProperty = new TextInput({ 109 | displayName: "Text Property", 110 | name: "textProperty", 111 | value: "What is your quest?", 112 | placeholder: "" 113 | }); 114 | 115 | position = new AutoDropdown({ 116 | name: "position", 117 | displayName: "Position", 118 | value: "Right" 119 | }); 120 | 121 | font = new FontControl({ 122 | name: "font", 123 | displayName: "Font", 124 | fontFamily: new FontPicker({ 125 | name: "fontFamily", 126 | displayName: "Font Family", 127 | value: "Segoe UI, wf_segoe-ui_normal, helvetica, arial, sans-serif" 128 | }), 129 | fontSize: new NumUpDown({ 130 | name: "fontSize", 131 | displayName: "Font Size", 132 | value: this.defaultFontSize, 133 | options: { 134 | minValue: { 135 | type: powerbi.visuals.ValidatorType.Min, 136 | value: this.minFontSize, 137 | } 138 | } 139 | }), 140 | bold: new ToggleSwitch({ 141 | name: "bold", 142 | displayName: "bold", 143 | value: true 144 | }), 145 | italic: new ToggleSwitch({ 146 | name: "italic", 147 | displayName: "italic", 148 | value: true 149 | }), 150 | underline: new ToggleSwitch({ 151 | name: "underline", 152 | displayName: "underline", 153 | value: true 154 | }) 155 | }); 156 | 157 | fontColor = new ColorPicker({ 158 | name: "fontColor", 159 | displayName: "Color", 160 | value: { value: "#000000" } 161 | }); 162 | background = new ColorPicker({ 163 | name: "background", 164 | displayName: "Background Color", 165 | value: { value: "#FFFFFF" } 166 | }); 167 | slices: Slice[] = [this.textProperty, this.font, this.fontColor, this.background, this.position]; 168 | } 169 | 170 | /** 171 | * BarChart formatting settings model class 172 | */ 173 | export class BarChartSettingsModel extends Model { 174 | enableAxis = new EnableAxisCardSettings(); 175 | colorSelector = new ColorSelectorCardSettings(); 176 | generalView = new GeneralViewCardSettings(); 177 | averageLine = new AverageLineCardSettings(); 178 | directEditSettings = new DirectEditSettings(); 179 | cards: Card[] = [this.enableAxis, this.colorSelector, this.generalView, this.averageLine, this.directEditSettings]; 180 | 181 | /** 182 | * populate colorSelector object categories formatting properties 183 | * @param dataPoints 184 | */ 185 | populateColorSelector(dataPoints: BarChartDataPoint[]) { 186 | const slices: Slice[] = this.colorSelector.slices; 187 | if (dataPoints) { 188 | dataPoints.forEach(dataPoint => { 189 | slices.push(new ColorPicker({ 190 | name: "fill", 191 | displayName: dataPoint.category, 192 | value: { value: dataPoint.color }, 193 | selector: dataPoint.selectionId.getSelector(), 194 | })); 195 | }); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /stringResources/ar-SA/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "العربية (Arabic)" 3 | } -------------------------------------------------------------------------------- /stringResources/bg-BG/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "български (Bulgarian)" 3 | } -------------------------------------------------------------------------------- /stringResources/ca-ES/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "català (Catalan)" 3 | } -------------------------------------------------------------------------------- /stringResources/cs-CZ/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "čeština (Czech)" 3 | } -------------------------------------------------------------------------------- /stringResources/da-DK/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "dansk (Danish)" 3 | } -------------------------------------------------------------------------------- /stringResources/de-DE/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "Deutsche (German)" 3 | } -------------------------------------------------------------------------------- /stringResources/el-GR/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "ελληνικά (Greek)" 3 | } -------------------------------------------------------------------------------- /stringResources/en-US/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "English (English)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/es-ES/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "español service (Spanish)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/et-EE/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "eesti (Estonian)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/eu-ES/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "Euskal (Basque)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/fi-FI/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "suomi (Finnish)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/fr-FR/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "français (French)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/gl-ES/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "galego (Galician)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/he-IL/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "עברית (Hebrew)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/hi-IN/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "हिन्दी (Hindi)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/hr-HR/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "hrvatski (Croatian)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/hu-HU/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "magyar (Hungarian)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/id-ID/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "Bahasa Indonesia (Indonesian)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/it-IT/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "italiano (Italian)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/ja-JP/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "日本の (Japanese)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/kk-KZ/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "Қазақ (Kazakh)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/ko-KR/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "한국의 (Korean)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/lt-LT/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "Lietuvos (Lithuanian)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/lv-LV/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "Latvijas (Latvian)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/ms-MY/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "Bahasa Melayu (Malay)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/nb-NO/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "norsk (Norwegian)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/nl-NL/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "Nederlands (Dutch)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/pl-PL/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "polski (Polish)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/pt-BR/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "português (Portuguese)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/pt-PT/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "português (Portuguese)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/ro-RO/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "românesc (Romanian)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/ru-RU/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "русский (Russian)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/sk-SK/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "slovenský (Slovak)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/sl-SI/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "slovenski (Slovenian)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/sr-Cyrl-RS/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "српски (Serbian)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/sr-Latn-RS/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "srpski (Serbian)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/sv-SE/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "svenska (Swedish)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/th-TH/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "ไทย (Thai)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/tr-TR/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "Türk (Turkish)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/uk-UA/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "український (Ukrainian)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/vi-VN/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "tiếng Việt (Vietnamese)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/zh-CN/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "中国 (Chinese-Simplified)" 3 | } 4 | -------------------------------------------------------------------------------- /stringResources/zh-TW/resources.resjson: -------------------------------------------------------------------------------- 1 | { 2 | "LanguageKey": "中國 (Chinese-Tranditional)" 3 | } 4 | -------------------------------------------------------------------------------- /style/visual.less: -------------------------------------------------------------------------------- 1 | p { 2 | font-size: 20px; 3 | font-weight: bold; 4 | 5 | em { 6 | background: yellow; 7 | padding: 5px; 8 | 9 | } 10 | } 11 | 12 | .xAxis path { 13 | display: none; 14 | } 15 | 16 | .helpLink { 17 | position: absolute; 18 | top: 0px; 19 | right: 12px; 20 | display: block; 21 | width: 20px; 22 | height: 20px; 23 | border: 2px solid #80B0E0; 24 | border-radius: 20px; 25 | color: #80B0E0; 26 | text-align: center; 27 | font-size: 16px; 28 | line-height: 20px; 29 | 30 | &.hidden { 31 | display: none; 32 | } 33 | } 34 | 35 | .LandingPage { 36 | position: absolute; 37 | text-align: center; 38 | top: 12px; 39 | font-size: 12pt; 40 | right: 15px; 41 | } 42 | 43 | .LandingPageHelpLink { 44 | position: absolute; 45 | text-decoration: underline; 46 | top: 50px; 47 | font-size: 12pt; 48 | right: 15px; 49 | } 50 | 51 | .direct-edit { 52 | position: absolute; 53 | bottom: 60%; 54 | right: 12px; 55 | display: inline-block; 56 | width: 40%; 57 | height: 20%; 58 | border: 2px solid #80B0E0; 59 | border-radius: 20px; 60 | color: #80B0E0; 61 | text-align: center; 62 | font-size: 16px; 63 | padding: 5px; 64 | } 65 | 66 | .hidden { 67 | display: none; 68 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "module": "es6", 7 | "target": "ES6", 8 | "sourceMap": true, 9 | "outDir": "./.tmp/build/", 10 | "moduleResolution": "node", 11 | "declaration": true 12 | }, 13 | "files": [ 14 | "src/barChart.ts" 15 | ] 16 | } --------------------------------------------------------------------------------