├── .githooks └── pre-commit ├── .github ├── stale.yml └── workflows │ ├── build.yml │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .storybook ├── main.js ├── manager.js ├── preview.js └── theme.js ├── CHANGELOG.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── eslint.config.js ├── jest.config.js ├── just.config.ts ├── lint-staged.config.js ├── package.json ├── pretter.config.js ├── scripts ├── DeleteBlob.ps1 ├── UpdateBlobProperties.ps1 ├── prepare.js └── tasks │ └── storybook.ts ├── src ├── chart │ ├── chart-legend.tsx │ ├── chart-render.tsx │ ├── chart.tsx │ └── index.ts ├── index.ts ├── lib │ ├── builder.ts │ ├── datasets.ts │ ├── patterns.ts │ ├── plugins.ts │ ├── settings.ts │ ├── storybook.tsx │ └── utils.ts └── types │ ├── index.ts │ └── types.ts ├── stories ├── area.stories.tsx ├── bar.stories.tsx ├── components │ ├── container.tsx │ └── index.ts ├── docs │ ├── introduction.stories.mdx │ └── line.stories.mdx ├── doughnut.stories.tsx ├── grouped-bar.stories.tsx ├── horizontal-bar.stories.tsx ├── line.stories.tsx ├── pie.stories.tsx ├── stacked-bar.stories.tsx ├── stacked-line.stories.tsx ├── trend-line.stories.tsx └── utils │ ├── index.ts │ └── utils.ts ├── tsconfig.json └── yarn.lock /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | if (!fs.existsSync(path.resolve(__dirname, '../node_modules/lint-staged'))) { 7 | console.error('lint-staged is not installed. Please run `yarn` then try again.'); 8 | process.exit(1); 9 | } else if (!fs.existsSync(path.resolve(__dirname, '../node_modules/lint-staged/bin/lint-staged.js'))) { 10 | // If someone updates from master but doesn't run yarn, by default they'd be unable to commit 11 | // with some cryptic error. Explicitly detect that condition and show a helpful error. 12 | console.error('lint-staged version is out of date! Please run `yarn` to update dependencies.'); 13 | process.exit(1); 14 | } 15 | 16 | require('lint-staged/bin/lint-staged.js'); 17 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 15 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build & Host 2 | 3 | on: push 4 | defaults: 5 | run: 6 | shell: bash # Run everything using bash 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: macos-latest 12 | steps: 13 | - name: Clone repository 14 | uses: actions/checkout@v2 15 | 16 | - name: Setup NodeJS 12.x 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: "12.x" 20 | 21 | - name: Set up build environment 22 | run: | 23 | yarn 24 | - name: Build app 25 | run: | 26 | yarn build:storybook 27 | - name: Deploy 28 | uses: peaceiris/actions-gh-pages@v3 29 | with: 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | publish_dir: ./dist-storybook 32 | -------------------------------------------------------------------------------- /.github/workflows/pr.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: PR 5 | 6 | on: 7 | pull_request: 8 | branches: [main] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [12.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: yarn 25 | - run: yarn lint 26 | - run: yarn checkchange 27 | - run: yarn build 28 | -------------------------------------------------------------------------------- /.github/workflows/release.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: Release 5 | 6 | on: 7 | workflow_dispatch: 8 | push: 9 | branches: [main] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [12.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | with: 22 | token: ${{ secrets.REPO_PAT }} 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: yarn 28 | - run: yarn build 29 | - run: | 30 | git config user.email "teamsappstudio@microsoft.com" 31 | git config user.name "Automation" 32 | - run: yarn release -y -n $NPM_TOKEN 33 | env: 34 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | 352 | 353 | # Built JS files 354 | /lib 355 | dist-storybook -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const tsconfig = path.resolve(__dirname, "../tsconfig.json"); 3 | 4 | module.exports = { 5 | stories: [ 6 | "../stories/**/*.stories.+(ts|tsx)", 7 | "../stories/docs/*.stories.mdx", 8 | ], 9 | addons: [ 10 | // "@storybook/addon-actions", 11 | "@storybook/addon-links", 12 | "@storybook/addon-docs", 13 | "@storybook/addon-viewport/register", 14 | "@storybook/addon-a11y/register", 15 | // "@storybook/addon-knobs/register", 16 | ], 17 | typescript: { 18 | check: true, 19 | checkOptions: { tsconfig }, 20 | reactDocgen: "react-docgen-typescript", 21 | reactDocgenTypescriptOptions: { 22 | shouldExtractLiteralValuesFromEnum: true, 23 | propFilter: (prop) => 24 | prop.parent ? !/node_modules/.test(prop.parent.fileName) : true, 25 | }, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from "@storybook/addons"; 2 | import TeamsPlatformUITheme from "./theme"; 3 | 4 | addons.setConfig({ 5 | theme: TeamsPlatformUITheme, 6 | }); 7 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | // import { withKnobs } from "@storybook/addon-knobs"; 2 | import { withA11y } from "@storybook/addon-a11y"; 3 | import { withStorybookTheme } from "../src/lib/storybook"; 4 | 5 | export const parameters = { 6 | options: { 7 | storySort: { 8 | order: [ 9 | "Charts", 10 | [ 11 | "Line", 12 | "Area", 13 | "Stacked line", 14 | "Bar", 15 | "Stacked bar", 16 | "Grouped bar", 17 | "Horizontal bar", 18 | "Doughnut", 19 | "Pie", 20 | "Bubble", 21 | ], 22 | ], 23 | }, 24 | }, 25 | // Remove an additional padding in canvas body (Added in v.6) 26 | layout: "fullscreen", 27 | docs: { 28 | page: null, 29 | inlineStories: false, 30 | }, 31 | }; 32 | 33 | export const decorators = [withA11y, withStorybookTheme]; 34 | -------------------------------------------------------------------------------- /.storybook/theme.js: -------------------------------------------------------------------------------- 1 | import { create } from "@storybook/theming/create"; 2 | 3 | export default create({ 4 | base: "light", 5 | brandTitle: "@fluentui/react-charts", 6 | 7 | colorPrimary: "#6264A7", 8 | colorSecondary: "#616161", 9 | 10 | // UI 11 | appBg: "#EBEBEB", 12 | appContentBg: "rgb(245, 245, 245)", 13 | appBorderColor: "#E0E0E0", 14 | appBorderRadius: 4, 15 | 16 | // Typography 17 | fontBase: `"Segoe UI", Segoe UI, system-ui, "Apple Color Emoji", "Segoe UI Emoji", sans-serif`, 18 | fontCode: "monospace", 19 | 20 | // Text colors 21 | textColor: "#242424", 22 | textInverseColor: "rgba(255,255,255,0.9)", 23 | textMutedColor: "red", 24 | 25 | // Toolbar default and active colors 26 | barTextColor: "#616161", 27 | barSelectedColor: "#6264A7", 28 | barBg: "#F5F5F5", 29 | 30 | // Form colors 31 | inputBg: "white", 32 | inputBorder: "#E0E0E0", 33 | inputTextColor: "#242424", 34 | inputBorderRadius: 4, 35 | }); 36 | -------------------------------------------------------------------------------- /CHANGELOG.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fluentui/react-charts", 3 | "entries": [ 4 | { 5 | "date": "Wed, 05 May 2021 23:09:27 GMT", 6 | "tag": "@fluentui/react-charts_v1.2.0", 7 | "version": "1.2.0", 8 | "comments": { 9 | "minor": [ 10 | { 11 | "comment": "Trend Chart", 12 | "author": "olkatruk@microsoft.com", 13 | "commit": "43b5a032b5b07bd059415800dcd6e6a4c25e822f", 14 | "package": "@fluentui/react-charts" 15 | } 16 | ] 17 | } 18 | }, 19 | { 20 | "date": "Tue, 27 Apr 2021 01:22:02 GMT", 21 | "tag": "@fluentui/react-charts_v1.1.0", 22 | "version": "1.1.0", 23 | "comments": { 24 | "minor": [ 25 | { 26 | "comment": "Merge branch 'main' of github.com:OfficeDev/microsoft-data-visualization-library into legend", 27 | "author": "olkatruk@microsoft.com", 28 | "commit": "256d81637e1c24bffb0e35e3f9b65b4cd36fbc55", 29 | "package": "@fluentui/react-charts" 30 | } 31 | ] 32 | } 33 | }, 34 | { 35 | "date": "Wed, 14 Apr 2021 23:04:35 GMT", 36 | "tag": "@fluentui/react-charts_v1.0.1", 37 | "version": "1.0.1", 38 | "comments": { 39 | "patch": [ 40 | { 41 | "comment": "Publish 1.0.0 version of react-charts", 42 | "author": "nirsh@microsoft.com", 43 | "commit": "2002004c1249ab454c097a9cead14aa3a23b3311", 44 | "package": "@fluentui/react-charts" 45 | } 46 | ] 47 | } 48 | }, 49 | { 50 | "date": "Wed, 14 Apr 2021 22:25:50 GMT", 51 | "tag": "@fluentui/react-charts_v0.1.0-alpha.3", 52 | "version": "0.1.0-alpha.3", 53 | "comments": { 54 | "prerelease": [ 55 | { 56 | "comment": "Verify package publishing flow", 57 | "author": "nirsh@microsoft.com", 58 | "commit": "2a4abd15ef2a0fbf74bf64787be2fcc75f033cd8", 59 | "package": "@fluentui/react-charts" 60 | } 61 | ] 62 | } 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log - @fluentui/react-charts 2 | 3 | This log was last generated on Wed, 05 May 2021 23:09:27 GMT and should not be manually modified. 4 | 5 | 6 | 7 | ## 1.2.0 8 | 9 | Wed, 05 May 2021 23:09:27 GMT 10 | 11 | ### Minor changes 12 | 13 | - Trend Chart (olkatruk@microsoft.com) 14 | 15 | ## 1.1.0 16 | 17 | Tue, 27 Apr 2021 01:22:02 GMT 18 | 19 | ### Minor changes 20 | 21 | - Merge branch 'main' of github.com:OfficeDev/microsoft-data-visualization-library into legend (olkatruk@microsoft.com) 22 | 23 | ## 1.0.1 24 | 25 | Wed, 14 Apr 2021 23:04:35 GMT 26 | 27 | ### Patches 28 | 29 | - Publish 1.0.0 version of react-charts (nirsh@microsoft.com) 30 | 31 | ## 0.1.0-alpha.3 32 | 33 | Wed, 14 Apr 2021 22:25:50 GMT 34 | 35 | ### Changes 36 | 37 | - Verify package publishing flow (nirsh@microsoft.com) 38 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project 2 | 3 | > This repo has been populated by an initial template to help get you started. Please 4 | > make sure to update the content to build a great experience for community-building. 5 | 6 | As the maintainer of this project, please make a few updates: 7 | 8 | - Improving this README.MD file to provide a great experience 9 | - Updating SUPPORT.MD with content about this project's support experience 10 | - Understanding the security reporting process in SECURITY.MD 11 | - Remove this section from the README 12 | 13 | ## Contributing 14 | 15 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 16 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 17 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 18 | 19 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 20 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 21 | provided by the bot. You will only need to do this once across all repos using our CLA. 22 | 23 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 24 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 25 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 26 | 27 | ## Trademarks 28 | 29 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 30 | trademarks or logos is subject to and must follow 31 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 32 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 33 | Any use of third-party trademarks or logos are subject to those third-party's policies. 34 | -------------------------------------------------------------------------------- /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://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), 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://msrc.microsoft.com/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://www.microsoft.com/en-us/msrc/pgp-key-msrc). 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://www.microsoft.com/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://microsoft.com/msrc/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://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? 4 | 5 | - **No CSS support:** Fill out this template with information about how to file issues and get help. 6 | - **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). 7 | - **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide. 8 | 9 | *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* 10 | 11 | # Support 12 | 13 | ## How to file issues and get help 14 | 15 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 16 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 17 | feature request as a new Issue. 18 | 19 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 20 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 21 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 22 | 23 | ## Microsoft Support Policy 24 | 25 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 26 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["airbnb", "airbnb/hooks"], 3 | }; 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "jsdom", 4 | }; 5 | -------------------------------------------------------------------------------- /just.config.ts: -------------------------------------------------------------------------------- 1 | import * as util from "util"; 2 | import _rimraf from "rimraf"; 3 | import { task, series, jestTask, tscTask, eslintTask } from "just-scripts"; 4 | import { 5 | startStorybookTask, 6 | buildStorybookTask, 7 | } from "./scripts/tasks/storybook"; 8 | 9 | const rimraf = util.promisify(_rimraf as any); 10 | 11 | task("clean", () => rimraf("lib")); 12 | task("build:tsc", tscTask()); 13 | task("build", series("clean", "build:tsc")); 14 | 15 | task("test", jestTask()); 16 | task("start", startStorybookTask()); 17 | task("build:storybook", buildStorybookTask()); 18 | task("lint", eslintTask()); 19 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | // https://www.npmjs.com/package/lint-staged 4 | module.exports = { 5 | // Run eslint in fix mode followed by prettier 6 | [`*.{ts,tsx}`]: ["yarn prettier --write"], 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fluentui/react-charts", 3 | "version": "1.2.0", 4 | "description": "Charts in the Fluent UI design language written in React", 5 | "repository": "https://github.com/OfficeDev/microsoft-data-visualization-library.git", 6 | "license": "MIT", 7 | "main": "lib/index.js", 8 | "scripts": { 9 | "build": "just-scripts build", 10 | "build:storybook": "just-scripts build:storybook", 11 | "change": "beachball change --branch origin/main", 12 | "checkchange": "beachball check --branch origin/main", 13 | "prepare": "node ./scripts/prepare", 14 | "lint": "just-scripts lint", 15 | "release": "beachball publish --branch origin/main", 16 | "start": "just-scripts start", 17 | "test": "just-scripts test" 18 | }, 19 | "dependencies": { 20 | "chart.js": "^2.9.4", 21 | "chartjs-plugin-deferred": "^1.0.2", 22 | "react-perfect-scrollbar": "^1.5.8" 23 | }, 24 | "peerDependencies": { 25 | "react": "^16.8.0", 26 | "react-dom": "^16.8.0" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.10.5", 30 | "@storybook/addon-a11y": "^5.3.19", 31 | "@storybook/addon-actions": "^6.1.21", 32 | "@storybook/addon-docs": "^6.1.21", 33 | "@storybook/addon-knobs": "^6.1.21", 34 | "@storybook/addon-links": "^6.0.26", 35 | "@storybook/addon-viewport": "^5.3.19", 36 | "@storybook/addons": "^6.1.11", 37 | "@storybook/cli": "^6.0.26", 38 | "@storybook/preset-typescript": "^3.0.0", 39 | "@storybook/react": "^6.0.26", 40 | "@storybook/theming": "^6.1.11", 41 | "@types/chart.js": "^2.9.31", 42 | "@types/classnames": "^2.2.10", 43 | "@types/faker": "^5.1.2", 44 | "@types/jest": "^26.0.4", 45 | "@types/lodash": "^4.14.158", 46 | "@types/react": "^16.8.25", 47 | "@types/react-beautiful-dnd": "^13.0.0", 48 | "@types/react-dom": "^16.8.4", 49 | "@typescript-eslint/eslint-plugin": "3.6.1", 50 | "babel-loader": "^8.1.0", 51 | "beachball": "^1.47.0", 52 | "chalk": "^4.1.0", 53 | "eslint": "7.2.0", 54 | "eslint-config-airbnb": "18.2.0", 55 | "eslint-plugin-import": "2.21.2", 56 | "eslint-plugin-jsx-a11y": "6.3.0", 57 | "eslint-plugin-react": "7.20.0", 58 | "eslint-plugin-react-hooks": "4.0.0", 59 | "faker": "thure/faker.js#v5.1.2", 60 | "jest": "^26.1.0", 61 | "jest-expect-message": "^1.0.2", 62 | "just-plop-helpers": "^1.1.2", 63 | "just-scripts": "^0.44.1", 64 | "lint-staged": "^10.2.11", 65 | "prettier": "^2.0.5", 66 | "react": "^16.8.0", 67 | "react-docgen-typescript": "^1.18.0", 68 | "react-dom": "^16.8.0", 69 | "rimraf": "^3.0.2", 70 | "storybook": "^5.3.19", 71 | "style-loader": "^1.2.1", 72 | "ts-jest": "^26.1.3", 73 | "ts-loader": "^8.0.1", 74 | "ts-node": "^7.0.0", 75 | "tslib": "^2.0.0", 76 | "typescript": "^3.9.7", 77 | "webpack": "~4.43.0" 78 | }, 79 | "files": [ 80 | "lib" 81 | ], 82 | "publishConfig": { 83 | "access": "public" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pretter.config.js: -------------------------------------------------------------------------------- 1 | // https://prettier.io/docs/en/configuration.html 2 | module.exports = { 3 | printWidth: 120, 4 | tabWidth: 2, 5 | singleQuote: true, 6 | trailingComma: "all", 7 | overrides: [ 8 | { 9 | // These files may be run as-is in IE 11 and must not have ES5-incompatible trailing commas 10 | files: ["*.html", "*.htm"], 11 | options: { 12 | trailingComma: "es5", 13 | }, 14 | }, 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /scripts/DeleteBlob.ps1: -------------------------------------------------------------------------------- 1 | #--------------------------------------------------------------------------------- 2 | # Delete Blob 3 | #--------------------------------------------------------------------------------- 4 | 5 | #requires -Version 3.0 6 | 7 | <# 8 | .SYNOPSIS 9 | This script can be used to change the content type of multiple/single file/s. . 10 | .DESCRIPTION 11 | This script is designed to change the content type of multiple/single file/s located in an Azure container 12 | .PARAMETER ContainerName 13 | Specifies the name of container. 14 | .PARAMETER StorageAccountName 15 | Specifies the name of the storage account to be connected. 16 | .PARAMETER ResourceGroupName 17 | Specifies the name of the resource group to be connected. 18 | .EXAMPLE 19 | C:\PS> C:\Script\UpdateContentTypes.ps1 -ResourceGroupName "ResourceGroup01" -StorageAccountName "AzureStorage01" -ContainerName "pics" 20 | 21 | #> 22 | 23 | Param 24 | ( 25 | [Parameter(Mandatory = $true)] 26 | [Alias('CN')] 27 | [String]$ContainerName, 28 | [Parameter(Mandatory = $true)] 29 | [Alias('SN')] 30 | [String]$StorageAccountName, 31 | [Parameter(Mandatory = $true)] 32 | [Alias('RGN')] 33 | [String]$ResourceGroupName, 34 | [Parameter(Mandatory = $true)] 35 | [Alias('Name')] 36 | [String]$BlobName 37 | ) 38 | 39 | #Check if Windows Azure PowerShell Module is avaliable 40 | If ((Get-Module -ListAvailable Azure) -eq $null) { 41 | Write-Warning "Windows Azure PowerShell module not found! Please install from http://www.windowsazure.com/en-us/downloads/#cmd-line-tools" 42 | } 43 | Else { 44 | If ($ResourceGroupName -and $StorageAccountName) { 45 | Get-AzureRmStorageAccount -ResourceGroupName $ResourceGroupName -StorageAccountName $StorageAccountName -ErrorAction SilentlyContinue ` 46 | -ErrorVariable IsExistStorageError | Out-Null 47 | #Check if storage account is exist 48 | If ($IsExistStorageError.Exception -eq $null) { 49 | #Getting Azure storage account key 50 | $Keys = Get-AzureRmStorageAccountKey -ResourceGroupName $ResourceGroupName -StorageAccountName $StorageAccountName 51 | $StorageAccountKey = $Keys.Value[0] 52 | } 53 | Else { 54 | Write-Error "Cannot find storage account '$StorageAccountName' in resource group '$ResourceGroupName' because it does not exist. Please make sure thar the name of storage is correct." 55 | } 56 | } 57 | 58 | If ($ContainerName) { 59 | $StorageContext = New-AzureStorageContext -StorageAccountName $StorageAccountName -StorageAccountKey $StorageAccountKey 60 | Get-AzureStorageContainer -Context $StorageContext -Name $ContainerName -ErrorAction SilentlyContinue ` 61 | -ErrorVariable IsExistContainerError | Out-Null 62 | #Check if container is exist 63 | If ($IsExistContainerError.Exception -eq $null) { 64 | Write-Warning "Deleting blob with prefix '$BlobName'" 65 | Get-AzureStorageBlob -Context $StorageContext -Container $ContainerName -Prefix $BlobName -ContinuationToken $Token | Remove-AzureStorageBlob -Force -WhatIf 66 | } 67 | Else { 68 | Write-Warning "Cannot find container '$ContainerName' because it does not exist. Please make sure thar the name of container is correct." 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /scripts/UpdateBlobProperties.ps1: -------------------------------------------------------------------------------- 1 | #--------------------------------------------------------------------------------- 2 | # Update Blob Cache Control and Content Type 3 | #--------------------------------------------------------------------------------- 4 | 5 | #requires -Version 3.0 6 | 7 | <# 8 | .SYNOPSIS 9 | This script can be used to change the content type of multiple/single file/s. . 10 | .DESCRIPTION 11 | This script is designed to change the content type of multiple/single file/s located in an Azure container 12 | .PARAMETER ContainerName 13 | Specifies the name of container. 14 | .PARAMETER StorageAccountName 15 | Specifies the name of the storage account to be connected. 16 | .PARAMETER ResourceGroupName 17 | Specifies the name of the resource group to be connected. 18 | .EXAMPLE 19 | C:\PS> C:\Script\UpdateContentTypes.ps1 -ResourceGroupName "ResourceGroup01" -StorageAccountName "AzureStorage01" -ContainerName "pics" 20 | 21 | #> 22 | 23 | Param 24 | ( 25 | [Parameter(Mandatory = $true)] 26 | [Alias('CN')] 27 | [String]$ContainerName, 28 | [Parameter(Mandatory = $true)] 29 | [Alias('SN')] 30 | [String]$StorageAccountName, 31 | [Parameter(Mandatory = $true)] 32 | [Alias('RGN')] 33 | [String]$ResourceGroupName 34 | ) 35 | 36 | #Check if Windows Azure PowerShell Module is avaliable 37 | If((Get-Module -ListAvailable Azure) -eq $null) 38 | { 39 | Write-Warning "Windows Azure PowerShell module not found! Please install from http://www.windowsazure.com/en-us/downloads/#cmd-line-tools" 40 | } 41 | Else 42 | { 43 | If($ResourceGroupName -and $StorageAccountName) { 44 | Get-AzureRmStorageAccount -ResourceGroupName $ResourceGroupName -StorageAccountName $StorageAccountName -ErrorAction SilentlyContinue ` 45 | -ErrorVariable IsExistStorageError | Out-Null 46 | #Check if storage account is exist 47 | If($IsExistStorageError.Exception -eq $null) { 48 | #Getting Azure storage account key 49 | $Keys = Get-AzureRmStorageAccountKey -ResourceGroupName $ResourceGroupName -StorageAccountName $StorageAccountName 50 | $StorageAccountKey = $Keys.Value[0] 51 | } Else { 52 | Write-Error "Cannot find storage account '$StorageAccountName' in resource group '$ResourceGroupName' because it does not exist. Please make sure thar the name of storage is correct." 53 | } 54 | } 55 | 56 | If($ContainerName) { 57 | $StorageContext = New-AzureStorageContext -StorageAccountName $StorageAccountName -StorageAccountKey $StorageAccountKey 58 | Get-AzureStorageContainer -Context $StorageContext -Name $ContainerName -ErrorAction SilentlyContinue ` 59 | -ErrorVariable IsExistContainerError | Out-Null 60 | #Check if container is exist 61 | If($IsExistContainerError.Exception -eq $null) 62 | { 63 | $Total = 0 64 | $Token = $NULL 65 | #Results are grouped in 10 000 (this number works very well with azure) 66 | $MaxReturn = 10000 67 | do 68 | { 69 | $BlobItems = Get-AzureStorageBlob -Context $StorageContext -Container $ContainerName -MaxCount $MaxReturn -ContinuationToken $Token 70 | Foreach($BlobItem in $BlobItems) 71 | { 72 | Try 73 | { 74 | $BlobName = $BlobItem.Name 75 | $blobext = [System.IO.Path]::GetExtension($BlobName) 76 | $blobext = $blobext.ToLower() 77 | $blobpath = [System.IO.Path]::GetDirectoryName($BlobName) 78 | $blobpath = $blobpath.ToLower() 79 | 80 | #You can add or remove more content typs if need be, the defult of none makes sure that nothing get's changed if extension is not matched 81 | switch ($blobext) { 82 | ".jpg" {$DesiredContentType = "image/jpeg"} 83 | ".jpeg" {$DesiredContentType = "image/jpeg"} 84 | ".jpe" {$DesiredContentType = "image/jpeg"} 85 | ".gif" {$DesiredContentType = "image/gif"} 86 | ".png" {$DesiredContentType = "image/png"} 87 | ".svg" {$DesiredContentType = "image/svg+xml"} 88 | ".eot" {$DesiredContentType = "application/vnd.ms-fontobject"} 89 | ".ttf" {$DesiredContentType = "application/x-font-ttf"} 90 | ".woff" {$DesiredContentType = "application/font-woff"} 91 | ".woff2" {$DesiredContentType = "application/font-woff2"} 92 | ".html" {$DesiredContentType = "text/html"} 93 | ".js" {$DesiredContentType = "application/javascript"} 94 | ".css" {$DesiredContentType = "text/css"} 95 | default {$DesiredContentType = "none"} 96 | } 97 | 98 | switch ($blobpath) { 99 | "hashed" {$DesiredCacheControl = "public, max-age=31536000"} 100 | default {$DesiredCacheControl = "public, max-age=3600"} 101 | } 102 | 103 | Write-Output "'$BlobName' - Has path '$blobpath' and extension '$blobext'." 104 | $CloudBlob = $BlobItem.ICloudBlob 105 | $Updated = $FALSE 106 | if ($DesiredContentType -ne "none") { 107 | $CurrentContentType = $CloudBlob.Properties.ContentType 108 | Write-Output "'$BlobName' - Has content type '$CurrentContentType'." 109 | if (-not $DesiredContentType.Equals($CurrentContentType, 3)) { 110 | $CloudBlob.Properties.ContentType = $DesiredContentType 111 | $Updated = $TRUE 112 | Write-Output "'$BlobName' - Changing content type to '$DesiredContentType'." 113 | } Else { 114 | Write-Information "'$BlobName' - Already has content type '$DesiredContentType'." 115 | } 116 | } Else { 117 | Write-Warning "'$BlobName' - Extension '$blobext' does not have a mapping." 118 | } 119 | 120 | if ($DesiredCacheControl -ne "none") { 121 | $CloudBlob = $BlobItem.ICloudBlob 122 | $CurrentCacheControl = $CloudBlob.Properties.CacheControl 123 | Write-Output "'$BlobName' - Has cache control '$CurrentCacheControl'." 124 | 125 | if (-not $DesiredCacheControl.Equals($CurrentCacheControl, 3)) { 126 | $CloudBlob.Properties.CacheControl = $DesiredCacheControl 127 | $Updated = $TRUE 128 | Write-Output "'$BlobName' - Changing cache control to '$DesiredCacheControl'." 129 | } Else { 130 | Write-Information "'$BlobName' - Already has cache control '$DesiredCacheControl'." 131 | } 132 | } Else { 133 | Write-Warning "'$BlobName' - Does not have a desired cache control setting." 134 | } 135 | 136 | if ($Updated) { 137 | $CloudBlob.SetProperties() 138 | Write-Information "'$BlobName' - Saved Successfully." 139 | } 140 | } 141 | Catch { 142 | $ErrorMessage = $_.Exception.Message 143 | Write-Error "Failed to change content type of '$BlobName'. Error: '$ErrorMessage'" 144 | } 145 | } 146 | $Total += $BlobItems.Count 147 | if($BlobItems.Length -le 0) { Break;} 148 | $Token = $BlobItems[$BlobItems.Count -1].ContinuationToken; 149 | } 150 | While ($Token -ne $Null) 151 | } 152 | Else 153 | { 154 | Write-Warning "Cannot find container '$ContainerName' because it does not exist. Please make sure thar the name of container is correct." 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /scripts/prepare.js: -------------------------------------------------------------------------------- 1 | const { spawnSync } = require("child_process"); 2 | const chalk = require("chalk"); 3 | 4 | // git v2.9.0 supports a custom hooks directory. This means we just need to check in the hooks scripts. 5 | spawnSync("git", ["config", "core.hooksPath", ".githooks"]); 6 | 7 | console.log(chalk.green("\nAll dependencies are installed!")); 8 | console.log(`For inner loop development, run: 9 | ${chalk.yellow("yarn start")} 10 | `); 11 | -------------------------------------------------------------------------------- /scripts/tasks/storybook.ts: -------------------------------------------------------------------------------- 1 | import { argv } from "just-scripts"; 2 | import path from "path"; 3 | 4 | import storybook from "@storybook/react/standalone"; 5 | 6 | export function startStorybookTask(options?: any) { 7 | options = options || {}; 8 | // This shouldn't be necessary but is needed due to strange logic in 9 | // storybook lib/core/src/server/config/utils.js 10 | process.env.NODE_ENV = "development"; 11 | 12 | return async function () { 13 | let { port, quiet, ci } = argv(); 14 | 15 | port = options.port || port; 16 | quiet = options.quiet || quiet; 17 | ci = options.ci || ci; 18 | 19 | const localConfigDir = path.join(process.cwd(), ".storybook"); 20 | 21 | await storybook({ 22 | mode: "dev", 23 | staticDir: [path.join(process.cwd(), "static")], 24 | configDir: localConfigDir, 25 | port: port || 3000, 26 | quiet, 27 | ci, 28 | }); 29 | }; 30 | } 31 | 32 | export function buildStorybookTask(options?: any) { 33 | options = options || {}; 34 | return async function () { 35 | let { port, quiet, ci } = argv(); 36 | 37 | port = options.port || port; 38 | quiet = options.quiet || quiet; 39 | ci = options.ci || ci; 40 | 41 | await storybook({ 42 | mode: "static", 43 | staticDir: [path.join(process.cwd(), "static")], 44 | configDir: path.join(process.cwd(), ".storybook"), 45 | outputDir: path.join(process.cwd(), "dist-storybook"), 46 | quiet, 47 | port: port || 3000, 48 | ci, 49 | }); 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/chart/chart-legend.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Chart from "chart.js"; 3 | import { 4 | ChartTypes, 5 | HighContrastColors, 6 | IChart, 7 | IChartOptions, 8 | } from "../types"; 9 | import { HALF_PI, PI, QUARTER_PI, TWO_THIRDS_PI } from "../lib/utils"; 10 | 11 | export const ChartLegend = ({ 12 | config, 13 | onClick, 14 | }: { 15 | config: IChart; 16 | onClick: (index: number) => void; 17 | }) => { 18 | return ( 19 |
28 | {config.data!.datasets!.map((item: any, index: number) => ( 29 | 37 | ))} 38 |
39 | ); 40 | }; 41 | 42 | const LegendItem = ({ 43 | id, 44 | label, 45 | dataset, 46 | highContrast, 47 | onClick, 48 | }: { 49 | id: number; 50 | label: string; 51 | dataset: Chart.ChartDataSets; 52 | highContrast?: boolean; 53 | onClick: (index: number) => void; 54 | }) => { 55 | const [selected, setSelected] = useState(false); 56 | const legendItemRef = React.useRef(null); 57 | 58 | React.useEffect(() => { 59 | if (!legendItemRef.current) return; 60 | }, []); 61 | 62 | const style: any = { 63 | display: "flex", 64 | margin: "0 10px", 65 | cursor: "pointer", 66 | transition: "opacity ease-out .3s", 67 | }; 68 | if (selected) { 69 | style.opacity = 0.5; 70 | } 71 | if (highContrast) { 72 | style.color = selected 73 | ? HighContrastColors.Disabled 74 | : HighContrastColors.Foreground; 75 | } 76 | 77 | return ( 78 |
{ 82 | onClick(id), setSelected(!selected); 83 | }} 84 | > 85 | 90 | {dataset.label} 91 |
92 | ); 93 | }; 94 | 95 | function elementInViewport(element: HTMLDivElement) { 96 | let top = element.offsetTop; 97 | let left = element.offsetLeft; 98 | let width = element.offsetWidth; 99 | let height = element.offsetHeight; 100 | 101 | while (element.offsetParent) { 102 | (element as any) = element.offsetParent; 103 | top += element.offsetTop; 104 | left += element.offsetLeft; 105 | } 106 | 107 | return ( 108 | top >= window.pageYOffset && 109 | left >= window.pageXOffset && 110 | top + height <= window.pageYOffset + window.innerHeight && 111 | left + width <= window.pageXOffset + window.innerWidth 112 | ); 113 | } 114 | 115 | const LegendItemColor = ({ 116 | dataset, 117 | selected, 118 | highContrast, 119 | }: { 120 | dataset: Chart.ChartDataSets; 121 | selected: boolean; 122 | highContrast?: boolean; 123 | }) => { 124 | const labelColorValueRef = React.useRef(null); 125 | React.useEffect(() => { 126 | if (!labelColorValueRef.current) return; 127 | const canvasRef: HTMLCanvasElement = labelColorValueRef.current; 128 | createLegendItemCanvas({ canvas: canvasRef, dataset, highContrast }); 129 | }, []); 130 | return ( 131 | 157 | ); 158 | }; 159 | 160 | function createLegendItemCanvas({ 161 | canvas, 162 | dataset, 163 | highContrast, 164 | }: { 165 | canvas: HTMLCanvasElement; 166 | dataset: Chart.ChartDataSets; 167 | highContrast?: boolean; 168 | }) { 169 | if (!canvas) return; 170 | const ctx: CanvasRenderingContext2D = canvas.getContext("2d")!; 171 | if (!ctx) return; 172 | ctx.save(); 173 | ctx.scale(10, 8); 174 | 175 | if (!dataset.borderDash) { 176 | ctx.fillStyle = (dataset.backgroundColor === "transparent" || 177 | dataset.backgroundColor === "rgba(0,0,0,0)" 178 | ? dataset.borderColor 179 | : dataset.backgroundColor) as string; 180 | ctx.fillRect(0, 0, canvas.width, canvas.height); 181 | } 182 | if (highContrast) { 183 | ctx.strokeStyle = HighContrastColors.Foreground; 184 | ctx.lineWidth = dataset.borderWidth as number; 185 | ctx.strokeRect(0, 0, 30, 19); 186 | } 187 | 188 | if (dataset.type === ChartTypes.Line && dataset.borderDash) { 189 | if (highContrast) { 190 | const x = 22; 191 | const y = 10; 192 | 193 | ctx.strokeStyle = HighContrastColors.Background; 194 | ctx.lineWidth = dataset.borderWidth as number; 195 | ctx.strokeRect(0, 0, 30, 18); 196 | // Line 197 | ctx.beginPath(); 198 | ctx.moveTo(-10, y); 199 | ctx.lineTo(18, y); 200 | ctx.setLineDash(dataset.borderDash!); 201 | ctx.lineWidth = (dataset.borderWidth as number) || 4; 202 | ctx.strokeStyle = HighContrastColors.Foreground; 203 | ctx.stroke(); 204 | 205 | // Point 206 | const pointStyle = dataset.pointStyle; 207 | if (pointStyle) { 208 | const radius = 6; 209 | const rotation = 0; 210 | let xOffset = 0; 211 | let yOffset = 0; 212 | let cornerRadius = 1; 213 | let size = 5; 214 | let rad = 0; 215 | 216 | ctx.beginPath(); 217 | ctx.setLineDash([]); 218 | switch (pointStyle) { 219 | // Default includes circle 220 | default: 221 | ctx.arc(x, y, radius * 0.85, 0, Math.PI * 2, true); 222 | ctx.closePath(); 223 | break; 224 | case "triangle": 225 | ctx.moveTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius); 226 | rad += TWO_THIRDS_PI; 227 | ctx.lineTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius); 228 | rad += TWO_THIRDS_PI; 229 | ctx.lineTo(x + Math.sin(rad) * radius, y - Math.cos(rad) * radius); 230 | ctx.closePath(); 231 | break; 232 | case "rectRounded": 233 | cornerRadius = radius * 0.516; 234 | size = radius - cornerRadius; 235 | xOffset = Math.cos(rad + QUARTER_PI) * size; 236 | yOffset = Math.sin(rad + QUARTER_PI) * size; 237 | ctx.arc( 238 | x - xOffset, 239 | y - yOffset, 240 | cornerRadius, 241 | rad - PI, 242 | rad - HALF_PI 243 | ); 244 | ctx.arc(x + yOffset, y - xOffset, cornerRadius, rad - HALF_PI, rad); 245 | ctx.arc(x + xOffset, y + yOffset, cornerRadius, rad, rad + HALF_PI); 246 | ctx.arc( 247 | x - yOffset, 248 | y + xOffset, 249 | cornerRadius, 250 | rad + HALF_PI, 251 | rad + PI 252 | ); 253 | ctx.closePath(); 254 | break; 255 | case "rect": 256 | if (!rotation) { 257 | size = Math.SQRT1_2 * radius; 258 | ctx.rect(x - size, y - size, 2 * size, 2 * size); 259 | break; 260 | } 261 | rad += QUARTER_PI; 262 | /* falls through */ 263 | case "rectRot": 264 | xOffset = Math.cos(rad) * radius; 265 | yOffset = Math.sin(rad) * radius; 266 | ctx.moveTo(x - xOffset, y - yOffset); 267 | ctx.lineTo(x + yOffset, y - xOffset); 268 | ctx.lineTo(x + xOffset, y + yOffset); 269 | ctx.lineTo(x - yOffset, y + xOffset); 270 | ctx.closePath(); 271 | break; 272 | case "crossRot": 273 | rad += QUARTER_PI; 274 | /* falls through */ 275 | case "cross": 276 | xOffset = Math.cos(rad) * radius; 277 | yOffset = Math.sin(rad) * radius; 278 | ctx.moveTo(x - xOffset, y - yOffset); 279 | ctx.lineTo(x + xOffset, y + yOffset); 280 | ctx.moveTo(x + yOffset, y - xOffset); 281 | ctx.lineTo(x - yOffset, y + xOffset); 282 | break; 283 | case "star": 284 | xOffset = Math.cos(rad) * radius; 285 | yOffset = Math.sin(rad) * radius; 286 | ctx.moveTo(x - xOffset, y - yOffset); 287 | ctx.lineTo(x + xOffset, y + yOffset); 288 | ctx.moveTo(x + yOffset, y - xOffset); 289 | ctx.lineTo(x - yOffset, y + xOffset); 290 | rad += QUARTER_PI; 291 | xOffset = Math.cos(rad) * radius; 292 | yOffset = Math.sin(rad) * radius; 293 | ctx.moveTo(x - xOffset, y - yOffset); 294 | ctx.lineTo(x + xOffset, y + yOffset); 295 | ctx.moveTo(x + yOffset, y - xOffset); 296 | ctx.lineTo(x - yOffset, y + xOffset); 297 | break; 298 | case "line": 299 | xOffset = Math.cos(rad) * radius; 300 | yOffset = Math.sin(rad) * radius; 301 | ctx.moveTo(x - xOffset, y - yOffset); 302 | ctx.lineTo(x + xOffset, y + yOffset); 303 | break; 304 | case "dash": 305 | ctx.moveTo(x, y); 306 | ctx.lineTo(x + Math.cos(rad) * radius, y + Math.sin(rad) * radius); 307 | break; 308 | } 309 | ctx.lineWidth = 1; 310 | ctx.fillStyle = HighContrastColors.Foreground; 311 | ctx.strokeStyle = HighContrastColors.Foreground; 312 | ctx.closePath(); 313 | ctx.fill(); 314 | ctx.stroke(); 315 | ctx.restore(); 316 | } 317 | } else { 318 | ctx.fillStyle = dataset.borderColor as string; 319 | ctx.fillRect(0, 0, canvas.width, canvas.height); 320 | } 321 | } 322 | 323 | // switch (dataset.type) { 324 | // case ChartTypes.Line: 325 | // if (highContrast) { 326 | // const x = 22; 327 | // const y = 10; 328 | // // Line 329 | // ctx.beginPath(); 330 | // ctx.moveTo(-10, y); 331 | // ctx.lineTo(18, y); 332 | // ctx.setLineDash(dataset.borderDash!); 333 | // ctx.lineWidth = (dataset.borderWidth as number) || 4; 334 | // ctx.strokeStyle = HighContrastColors.Foreground; 335 | // ctx.stroke(); 336 | 337 | // // Point 338 | // const pointStyle = dataset.pointStyle; 339 | // if (pointStyle) { 340 | // const radius = 6; 341 | // const rotation = 0; 342 | // let xOffset = 0; 343 | // let yOffset = 0; 344 | // let cornerRadius = 1; 345 | // let size = 5; 346 | // let rad = 0; 347 | 348 | // ctx.beginPath(); 349 | // ctx.setLineDash([]); 350 | // switch (pointStyle) { 351 | // // Default includes circle 352 | // default: 353 | // ctx.arc(x, y, radius * 0.85, 0, Math.PI * 2, true); 354 | // ctx.closePath(); 355 | // break; 356 | // case "triangle": 357 | // ctx.moveTo( 358 | // x + Math.sin(rad) * radius, 359 | // y - Math.cos(rad) * radius 360 | // ); 361 | // rad += TWO_THIRDS_PI; 362 | // ctx.lineTo( 363 | // x + Math.sin(rad) * radius, 364 | // y - Math.cos(rad) * radius 365 | // ); 366 | // rad += TWO_THIRDS_PI; 367 | // ctx.lineTo( 368 | // x + Math.sin(rad) * radius, 369 | // y - Math.cos(rad) * radius 370 | // ); 371 | // ctx.closePath(); 372 | // break; 373 | // case "rectRounded": 374 | // cornerRadius = radius * 0.516; 375 | // size = radius - cornerRadius; 376 | // xOffset = Math.cos(rad + QUARTER_PI) * size; 377 | // yOffset = Math.sin(rad + QUARTER_PI) * size; 378 | // ctx.arc( 379 | // x - xOffset, 380 | // y - yOffset, 381 | // cornerRadius, 382 | // rad - PI, 383 | // rad - HALF_PI 384 | // ); 385 | // ctx.arc( 386 | // x + yOffset, 387 | // y - xOffset, 388 | // cornerRadius, 389 | // rad - HALF_PI, 390 | // rad 391 | // ); 392 | // ctx.arc( 393 | // x + xOffset, 394 | // y + yOffset, 395 | // cornerRadius, 396 | // rad, 397 | // rad + HALF_PI 398 | // ); 399 | // ctx.arc( 400 | // x - yOffset, 401 | // y + xOffset, 402 | // cornerRadius, 403 | // rad + HALF_PI, 404 | // rad + PI 405 | // ); 406 | // ctx.closePath(); 407 | // break; 408 | // case "rect": 409 | // if (!rotation) { 410 | // size = Math.SQRT1_2 * radius; 411 | // ctx.rect(x - size, y - size, 2 * size, 2 * size); 412 | // break; 413 | // } 414 | // rad += QUARTER_PI; 415 | // /* falls through */ 416 | // case "rectRot": 417 | // xOffset = Math.cos(rad) * radius; 418 | // yOffset = Math.sin(rad) * radius; 419 | // ctx.moveTo(x - xOffset, y - yOffset); 420 | // ctx.lineTo(x + yOffset, y - xOffset); 421 | // ctx.lineTo(x + xOffset, y + yOffset); 422 | // ctx.lineTo(x - yOffset, y + xOffset); 423 | // ctx.closePath(); 424 | // break; 425 | // case "crossRot": 426 | // rad += QUARTER_PI; 427 | // /* falls through */ 428 | // case "cross": 429 | // xOffset = Math.cos(rad) * radius; 430 | // yOffset = Math.sin(rad) * radius; 431 | // ctx.moveTo(x - xOffset, y - yOffset); 432 | // ctx.lineTo(x + xOffset, y + yOffset); 433 | // ctx.moveTo(x + yOffset, y - xOffset); 434 | // ctx.lineTo(x - yOffset, y + xOffset); 435 | // break; 436 | // case "star": 437 | // xOffset = Math.cos(rad) * radius; 438 | // yOffset = Math.sin(rad) * radius; 439 | // ctx.moveTo(x - xOffset, y - yOffset); 440 | // ctx.lineTo(x + xOffset, y + yOffset); 441 | // ctx.moveTo(x + yOffset, y - xOffset); 442 | // ctx.lineTo(x - yOffset, y + xOffset); 443 | // rad += QUARTER_PI; 444 | // xOffset = Math.cos(rad) * radius; 445 | // yOffset = Math.sin(rad) * radius; 446 | // ctx.moveTo(x - xOffset, y - yOffset); 447 | // ctx.lineTo(x + xOffset, y + yOffset); 448 | // ctx.moveTo(x + yOffset, y - xOffset); 449 | // ctx.lineTo(x - yOffset, y + xOffset); 450 | // break; 451 | // case "line": 452 | // xOffset = Math.cos(rad) * radius; 453 | // yOffset = Math.sin(rad) * radius; 454 | // ctx.moveTo(x - xOffset, y - yOffset); 455 | // ctx.lineTo(x + xOffset, y + yOffset); 456 | // break; 457 | // case "dash": 458 | // ctx.moveTo(x, y); 459 | // ctx.lineTo( 460 | // x + Math.cos(rad) * radius, 461 | // y + Math.sin(rad) * radius 462 | // ); 463 | // break; 464 | // } 465 | // ctx.lineWidth = 1; 466 | // ctx.fillStyle = HighContrastColors.Foreground; 467 | // ctx.strokeStyle = HighContrastColors.Foreground; 468 | // ctx.closePath(); 469 | // ctx.fill(); 470 | // ctx.stroke(); 471 | // ctx.restore(); 472 | // } 473 | // } else { 474 | // ctx.fillStyle = dataset.borderColor as string; 475 | // ctx.fillRect(0, 0, canvas.width, canvas.height); 476 | // } 477 | // break; 478 | // default: 479 | // ctx.fillStyle = dataset.backgroundColor as string; 480 | // ctx.fillRect(0, 0, canvas.width, canvas.height); 481 | // if (highContrast) { 482 | // ctx.strokeStyle = HighContrastColors.Foreground; 483 | // ctx.lineWidth = dataset.borderWidth as number; 484 | // ctx.strokeRect(0, 0, 30, 18); 485 | // } 486 | // break; 487 | // } 488 | // ctx.fillRect(0, 0, canvas.width, canvas.height); 489 | 490 | // if (theme === TeamsTheme.HighContrast) { 491 | // if (patterns) { 492 | // ctx.setTransform(1.4, 0, 0, 1, 0, 0); 493 | // ctx.scale(12, 10); 494 | // (ctx.fillStyle as any) = buildPattern({ 495 | // ...patterns(colorScheme)[index], 496 | // backgroundColor: colorScheme.default.background, 497 | // patternColor: colorScheme.brand.background, 498 | // }); 499 | // ctx.fillRect(-15, -15, canvasRef.width, canvasRef.height); 500 | // ctx.restore(); 501 | // } else { 502 | // ctx.scale(15, 15); 503 | // ctx.fillStyle = colorScheme.brand.shadow; 504 | // ctx.fillRect(-15, -15, canvasRef.width, canvasRef.height); 505 | // ctx.fillStyle = colorScheme.default.foreground3; 506 | // switch (lineChartPatterns[index].pointStyle) { 507 | // case PointStyles.Triangle: 508 | // ctx.moveTo(9.5, 2.5); 509 | // ctx.lineTo(5.5, 7.5); 510 | // ctx.lineTo(13.5, 7.5); 511 | // break; 512 | // case PointStyles.Rectangle: 513 | // ctx.rect(6.5, 2.5, 8, 5); 514 | // break; 515 | // case PointStyles.RectangleRotated: 516 | // ctx.moveTo(10, 2); 517 | // ctx.lineTo(14.5, 5); 518 | // ctx.lineTo(10, 8); 519 | // ctx.lineTo(5.5, 5); 520 | // break; 521 | // case PointStyles.Circle: 522 | // default: 523 | // ctx.ellipse(10, 5, 3.5, 2.5, 0, 0, 2 * Math.PI); 524 | // break; 525 | // } 526 | // ctx.fill(); 527 | 528 | // // Line Style 529 | // ctx.strokeStyle = colorScheme.default.foreground3; 530 | // ctx.beginPath(); 531 | // ctx.setLineDash( 532 | // lineChartPatterns[index].lineBorderDash.length ? [2, 2] : [] 533 | // ); 534 | // ctx.moveTo(-1.5, 5); 535 | // ctx.lineTo(20, 5); 536 | // ctx.stroke(); 537 | // ctx.restore(); 538 | // } 539 | // } else { 540 | // ctx.fillStyle = dataPointColor; 541 | // ctx.fillRect(0, 0, canvasRef.width, canvasRef.height); 542 | // } 543 | } 544 | -------------------------------------------------------------------------------- /src/chart/chart-render.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import Chart from "chart.js"; 3 | import { ChartLegend } from "./chart-legend"; 4 | import { IChart } from "../types"; 5 | 6 | export const ChartRender = (config: IChart) => { 7 | const { data, options, canvasProps } = config; 8 | 9 | const canvasRef = React.useRef(null); 10 | const chartRef = React.useRef(); 11 | const chartId = React.useMemo( 12 | () => Math.random().toString(36).substr(2, 9), 13 | [] 14 | ); 15 | 16 | useEffect(() => { 17 | if (!canvasRef.current) return; 18 | const ctx = canvasRef.current.getContext("2d"); 19 | if (!ctx) return; 20 | // Chart Init 21 | chartRef.current = new Chart(ctx, { ...config }); 22 | }, []); 23 | 24 | function onLegendItemClick(index: number) { 25 | if (!chartRef.current) return; 26 | const ci = (chartRef.current as any).chart; 27 | const meta = ci.getDatasetMeta(index); 28 | 29 | // See controller.isDatasetVisible comment 30 | meta.hidden = meta.hidden === null ? !ci.data.datasets[index].hidden : null; 31 | 32 | // We hid a dataset ... rerender the chart 33 | ci.update(); 34 | } 35 | 36 | return ( 37 |
45 |
46 | 56 | {data!.datasets!.map((set, setKey) => 57 | (set.data! as number[]).forEach((item: number, itemKey: number) => ( 58 | // Generated tooltips for screen readers 59 |
60 |

{item}

61 | 62 | {set.label}: {set!.data![itemKey]} 63 | 64 |
65 | )) 66 | )} 67 |
68 |
69 | {data && (options as any).legend.custom && ( 70 | 71 | )} 72 |
73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /src/chart/chart.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Chart from "chart.js"; 3 | import { IChart } from "../types"; 4 | import { ChartRender } from "./chart-render"; 5 | 6 | (Chart as any).defaults.global.legend.display = false; 7 | (Chart as any).defaults.global.defaultFontFamily = `Segoe UI, system-ui, sans-serif`; 8 | 9 | export function ChartContainer(config: IChart) { 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /src/chart/index.ts: -------------------------------------------------------------------------------- 1 | export { ChartContainer as Chart } from "./chart"; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Chart } from "./chart"; 2 | export * from "./types"; 3 | export * from "./lib/datasets"; 4 | export * from "./lib/patterns"; 5 | export * from "./lib/plugins"; 6 | export * from "./lib/settings"; 7 | export * from "./lib/utils"; 8 | -------------------------------------------------------------------------------- /src/lib/builder.ts: -------------------------------------------------------------------------------- 1 | import { PluginServiceRegistrationOptions } from "chart.js"; 2 | import { 3 | axisXTeamsStyle, 4 | gradientPlugin, 5 | highLightDataOnHover, 6 | horizontalBarAxisYLabels, 7 | horizontalBarValue, 8 | keyboardAccessibility, 9 | removeAllListeners, 10 | } from "./plugins"; 11 | import { 12 | ChartTypes, 13 | Entity, 14 | IChartConfig, 15 | IChartData, 16 | IChartOptions, 17 | ILineChartDataHighContrast, 18 | ILinePreSetupConfigHighContrast, 19 | IPreSetupConfig, 20 | IPreSetupConfigHighContrast, 21 | } from "../types"; 22 | import { 23 | barOptions, 24 | defaultOptions, 25 | doughnutOptions, 26 | groupedBarOptions, 27 | highContrastOptions, 28 | horizontalBarOptions, 29 | pieOptions, 30 | stackedBarOptions, 31 | stackedLineOptions, 32 | trendLineOptions, 33 | } from "./settings"; 34 | import { 35 | BarDataSetHCStyle, 36 | BarDataSetStyle, 37 | DoughnutDataSetHCStyle, 38 | DoughnutDataSetStyle, 39 | HorizontalBarDataSetHCStyle, 40 | HorizontalBarDataSetStyle, 41 | LineDataSetHCStyle, 42 | LineDataSetStyle, 43 | LineStackedDataSetHCStyle, 44 | LineStackedDataSetStyle, 45 | PieDataSetHCStyle, 46 | PieDataSetStyle, 47 | TrendLineDataSetHCStyle, 48 | TrendLineDataSetStyle, 49 | } from "./datasets"; 50 | import { deepMerge } from "./utils"; 51 | 52 | export class ChartBuilder extends Entity { 53 | type?: ChartTypes; 54 | data: IChartData; 55 | areaLabel: string; 56 | options: IChartOptions = defaultOptions; 57 | plugins: PluginServiceRegistrationOptions[] = [ 58 | { 59 | afterInit: keyboardAccessibility, 60 | }, 61 | { 62 | afterInit: axisXTeamsStyle, 63 | }, 64 | { 65 | destroy: removeAllListeners, 66 | }, 67 | ]; 68 | 69 | constructor(fields: IChartConfig) { 70 | super(fields); 71 | 72 | this.areaLabel = fields.areaLabel; 73 | this.data = fields.data; 74 | this.type = fields.type; 75 | 76 | if (fields.plugins) { 77 | this.plugins = this.plugins.concat(fields.plugins); 78 | } 79 | 80 | if (fields.options) { 81 | this.options = deepMerge(this.options, fields.options); 82 | } 83 | } 84 | } 85 | 86 | export class LineChart extends ChartBuilder { 87 | type: ChartTypes; 88 | 89 | constructor(fields: IPreSetupConfig) { 90 | super(fields); 91 | 92 | this.type = ChartTypes.Line; 93 | this.data.datasets = Array.from( 94 | this.data.datasets, 95 | (set) => new LineDataSetStyle(set) 96 | ); 97 | 98 | this.plugins.push({ 99 | beforeDatasetsDraw: highLightDataOnHover, 100 | }); 101 | } 102 | } 103 | 104 | export class TrendLineChart extends ChartBuilder { 105 | type: ChartTypes; 106 | 107 | constructor(fields: IPreSetupConfig) { 108 | super(fields); 109 | 110 | this.type = ChartTypes.Line; 111 | this.data.datasets = Array.from( 112 | this.data.datasets, 113 | (set) => new TrendLineDataSetStyle(set) 114 | ); 115 | 116 | this.options = deepMerge(this.options, trendLineOptions); 117 | 118 | if (fields.options) { 119 | this.options = deepMerge(this.options, fields.options); 120 | } 121 | 122 | this.plugins.push({ 123 | afterLayout: gradientPlugin, 124 | }); 125 | this.plugins.push({ 126 | beforeDatasetDraw: highLightDataOnHover, 127 | }); 128 | } 129 | } 130 | 131 | export class TrendLineChartHighContrast extends TrendLineChart { 132 | constructor(fields: ILinePreSetupConfigHighContrast) { 133 | super(fields); 134 | 135 | this.data.datasets = Array.from( 136 | fields.data.datasets, 137 | (set: TrendLineDataSetHCStyle) => new TrendLineDataSetHCStyle(set) 138 | ); 139 | 140 | this.options = deepMerge(this.options, highContrastOptions); 141 | 142 | this.plugins.push({ 143 | afterLayout: gradientPlugin, 144 | }); 145 | 146 | this.plugins.push({ 147 | beforeDatasetsDraw: highLightDataOnHover, 148 | }); 149 | } 150 | } 151 | 152 | export class BarChart extends ChartBuilder { 153 | type: ChartTypes; 154 | 155 | constructor(fields: IPreSetupConfig) { 156 | super(fields); 157 | 158 | this.type = ChartTypes.Bar; 159 | this.data.datasets = Array.from( 160 | this.data.datasets, 161 | (set) => new BarDataSetStyle(set) 162 | ); 163 | 164 | this.options = deepMerge(this.options, barOptions); 165 | 166 | if (fields.options) { 167 | this.options = deepMerge(this.options, fields.options); 168 | } 169 | 170 | this.plugins.push({ 171 | beforeDatasetsDraw: highLightDataOnHover, 172 | }); 173 | } 174 | } 175 | 176 | export class PieChart extends ChartBuilder { 177 | type: ChartTypes; 178 | 179 | constructor(fields: IPreSetupConfig) { 180 | super(fields); 181 | 182 | this.type = ChartTypes.Pie; 183 | this.data.datasets = Array.from( 184 | this.data.datasets, 185 | (set) => new PieDataSetStyle(set) 186 | ); 187 | 188 | this.options = deepMerge(this.options, pieOptions); 189 | 190 | if (fields.options) { 191 | this.options = deepMerge(this.options, fields.options); 192 | } 193 | } 194 | } 195 | 196 | export class DoughnutChart extends PieChart { 197 | type: ChartTypes; 198 | 199 | constructor(fields: IPreSetupConfig) { 200 | super(fields); 201 | 202 | this.type = ChartTypes.Doughnut; 203 | this.data.datasets = Array.from( 204 | this.data.datasets, 205 | (set) => new DoughnutDataSetStyle(set) 206 | ); 207 | 208 | this.options = deepMerge(this.options, doughnutOptions); 209 | 210 | if (fields.options) { 211 | this.options = deepMerge(this.options, fields.options); 212 | } 213 | } 214 | } 215 | 216 | export class GroupedBarChart extends BarChart { 217 | constructor(fields: IPreSetupConfig) { 218 | super(fields); 219 | 220 | this.options = deepMerge(this.options, groupedBarOptions); 221 | } 222 | } 223 | 224 | export class StackedBarChart extends ChartBuilder { 225 | type: ChartTypes; 226 | 227 | constructor(fields: IPreSetupConfig) { 228 | super(fields); 229 | 230 | this.type = ChartTypes.Bar; 231 | this.data.datasets = Array.from( 232 | this.data.datasets, 233 | (set) => new BarDataSetStyle(set) 234 | ); 235 | 236 | this.options = deepMerge(this.options, stackedBarOptions); 237 | 238 | if (fields.options) { 239 | this.options = deepMerge(this.options, fields.options); 240 | } 241 | 242 | this.plugins.push({ 243 | beforeDatasetsDraw: highLightDataOnHover, 244 | }); 245 | } 246 | } 247 | 248 | export class HorizontalBarChart extends ChartBuilder { 249 | type: ChartTypes; 250 | 251 | constructor(fields: IPreSetupConfig) { 252 | super(fields); 253 | 254 | this.type = ChartTypes.HorizontalBar; 255 | this.data.datasets = Array.from( 256 | this.data.datasets, 257 | (set) => new HorizontalBarDataSetStyle(set) 258 | ); 259 | 260 | this.plugins.push({ 261 | resize: horizontalBarAxisYLabels, 262 | }); 263 | this.plugins.push({ 264 | afterDatasetsDraw: horizontalBarValue, 265 | }); 266 | 267 | this.options = deepMerge(this.options, horizontalBarOptions); 268 | 269 | if (fields.options) { 270 | this.options = deepMerge(this.options, fields.options); 271 | } 272 | } 273 | } 274 | 275 | export class AreaChart extends LineChart { 276 | constructor(fields: IPreSetupConfig) { 277 | super(fields); 278 | 279 | this.plugins.push({ 280 | afterLayout: gradientPlugin, 281 | }); 282 | this.plugins.push({ 283 | beforeDatasetDraw: highLightDataOnHover, 284 | }); 285 | } 286 | } 287 | 288 | export class LineStackedChart extends ChartBuilder { 289 | type: ChartTypes; 290 | 291 | constructor(fields: IPreSetupConfig) { 292 | super(fields); 293 | 294 | this.type = ChartTypes.Line; 295 | this.data.datasets = Array.from( 296 | this.data.datasets, 297 | (set) => new LineStackedDataSetStyle(set) 298 | ); 299 | this.options = deepMerge(this.options, stackedLineOptions); 300 | 301 | if (fields.options) { 302 | this.options = deepMerge(this.options, fields.options); 303 | } 304 | 305 | this.plugins.push({ 306 | beforeDatasetsDraw: highLightDataOnHover, 307 | }); 308 | } 309 | } 310 | 311 | /** 312 | * HighContrast Chart Options 313 | */ 314 | 315 | export class HorizontalBarChartHighContrast extends ChartBuilder { 316 | type: ChartTypes; 317 | 318 | constructor(fields: IPreSetupConfigHighContrast) { 319 | super(fields); 320 | 321 | this.type = ChartTypes.HorizontalBar; 322 | this.data.datasets = Array.from( 323 | this.data.datasets, 324 | (set) => 325 | new HorizontalBarDataSetHCStyle(set as HorizontalBarDataSetHCStyle) 326 | ); 327 | 328 | this.plugins.push({ 329 | resize: horizontalBarAxisYLabels, 330 | }); 331 | this.plugins.push({ 332 | afterDatasetsDraw: horizontalBarValue, 333 | }); 334 | 335 | this.options = deepMerge(this.options, highContrastOptions); 336 | this.options = deepMerge(this.options, horizontalBarOptions); 337 | 338 | if (fields.options) { 339 | this.options = deepMerge(this.options, fields.options); 340 | } 341 | } 342 | } 343 | 344 | export class StackedBarChartHighContrast extends ChartBuilder { 345 | type: ChartTypes; 346 | 347 | constructor(fields: IPreSetupConfigHighContrast) { 348 | super(fields); 349 | 350 | this.type = ChartTypes.Bar; 351 | this.data.datasets = Array.from( 352 | this.data.datasets, 353 | (set) => new BarDataSetHCStyle(set as BarDataSetHCStyle) 354 | ); 355 | 356 | this.options = deepMerge(this.options, highContrastOptions); 357 | this.options = deepMerge(this.options, stackedBarOptions); 358 | 359 | if (fields.options) { 360 | this.options = deepMerge(this.options, fields.options); 361 | } 362 | 363 | this.plugins.push({ 364 | beforeDatasetsDraw: highLightDataOnHover, 365 | }); 366 | } 367 | } 368 | 369 | export class LineStackedChartHighContrast extends ChartBuilder { 370 | type: ChartTypes; 371 | 372 | constructor(fields: IPreSetupConfigHighContrast) { 373 | super(fields); 374 | 375 | this.type = ChartTypes.Line; 376 | this.data.datasets = Array.from( 377 | this.data.datasets, 378 | (set) => new LineStackedDataSetHCStyle(set as LineStackedDataSetHCStyle) 379 | ); 380 | this.options = deepMerge(this.options, highContrastOptions); 381 | this.options = deepMerge(this.options, stackedLineOptions); 382 | 383 | if (fields.options) { 384 | this.options = deepMerge(this.options, fields.options); 385 | } 386 | 387 | this.plugins.push({ 388 | beforeDatasetsDraw: highLightDataOnHover, 389 | }); 390 | } 391 | } 392 | 393 | export class PieChartHighContrast extends PieChart { 394 | type: ChartTypes; 395 | 396 | constructor(fields: IPreSetupConfigHighContrast) { 397 | super(fields); 398 | 399 | this.type = ChartTypes.Pie; 400 | this.data.datasets = Array.from( 401 | this.data.datasets, 402 | (set) => new PieDataSetHCStyle(set as PieDataSetHCStyle) 403 | ); 404 | 405 | this.options = deepMerge(this.options, highContrastOptions); 406 | this.options = deepMerge(this.options, pieOptions); 407 | 408 | if (fields.options) { 409 | this.options = deepMerge(this.options, fields.options); 410 | } 411 | } 412 | } 413 | 414 | export class DoughnutChartHighContrast extends DoughnutChart { 415 | type: ChartTypes; 416 | 417 | constructor(fields: IPreSetupConfigHighContrast) { 418 | super(fields); 419 | 420 | this.type = ChartTypes.Pie; 421 | this.data.datasets = Array.from( 422 | this.data.datasets, 423 | (set) => new DoughnutDataSetHCStyle(set as DoughnutDataSetHCStyle) 424 | ); 425 | 426 | this.options = deepMerge(this.options, highContrastOptions); 427 | this.options = deepMerge(this.options, doughnutOptions); 428 | 429 | if (fields.options) { 430 | this.options = deepMerge(this.options, fields.options); 431 | } 432 | } 433 | } 434 | 435 | export class LineChartHighContrast extends ChartBuilder { 436 | type: ChartTypes; 437 | data: ILineChartDataHighContrast; 438 | 439 | constructor(fields: ILinePreSetupConfigHighContrast) { 440 | super(fields); 441 | 442 | this.type = ChartTypes.Line; 443 | this.data = fields.data; 444 | this.data.datasets = Array.from( 445 | fields.data.datasets, 446 | (set: LineDataSetHCStyle) => new LineDataSetHCStyle(set) 447 | ); 448 | 449 | this.options = deepMerge(defaultOptions, highContrastOptions); 450 | 451 | if (fields.options) { 452 | this.options = deepMerge(this.options, fields.options); 453 | } 454 | 455 | this.plugins.push({ 456 | beforeDatasetsDraw: highLightDataOnHover, 457 | }); 458 | } 459 | } 460 | 461 | export class AreaChartHighContrast extends LineChartHighContrast { 462 | constructor(fields: ILinePreSetupConfigHighContrast) { 463 | super(fields); 464 | 465 | this.plugins.push({ 466 | afterLayout: gradientPlugin, 467 | }); 468 | 469 | this.plugins.push({ 470 | beforeDatasetsDraw: highLightDataOnHover, 471 | }); 472 | } 473 | } 474 | 475 | export class BarChartHighContrast extends ChartBuilder { 476 | type: ChartTypes; 477 | 478 | constructor(fields: IPreSetupConfigHighContrast) { 479 | super(fields); 480 | 481 | this.type = ChartTypes.Bar; 482 | this.data.datasets = Array.from( 483 | this.data.datasets, 484 | (set) => new BarDataSetHCStyle(set as BarDataSetHCStyle) 485 | ); 486 | 487 | this.options = deepMerge(this.options, highContrastOptions); 488 | this.options = deepMerge(this.options, barOptions); 489 | 490 | if (fields.options) { 491 | this.options = deepMerge(this.options, fields.options); 492 | } 493 | 494 | this.plugins.push({ 495 | beforeDatasetsDraw: highLightDataOnHover, 496 | }); 497 | } 498 | } 499 | 500 | export class GroupedBarChartHighContrast extends BarChartHighContrast { 501 | constructor(fields: IPreSetupConfigHighContrast) { 502 | super(fields); 503 | 504 | this.options = deepMerge(this.options, groupedBarOptions); 505 | } 506 | } 507 | -------------------------------------------------------------------------------- /src/lib/datasets.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BorderWidth, 3 | ChartColor, 4 | ChartDataSets, 5 | PointStyle, 6 | PositionType, 7 | Scriptable, 8 | } from "chart.js"; 9 | import { 10 | ChartTypes, 11 | Entity, 12 | HighContrastColors, 13 | IDraw, 14 | Point, 15 | Shapes, 16 | } from "../types"; 17 | import { buildPattern } from "./patterns"; 18 | 19 | export class ChartDataSet extends Entity { 20 | type?: string | undefined; 21 | 22 | constructor(fields: Partial) { 23 | super(fields); 24 | 25 | this.type = fields.type; 26 | } 27 | } 28 | export class LineDataSetStyle extends ChartDataSet implements ChartDataSets { 29 | borderCapStyle?: "butt" | "round" | "square"; 30 | borderJoinStyle?: "bevel" | "round" | "miter"; 31 | borderWidth?: BorderWidth | BorderWidth[] | Scriptable; 32 | hoverBackgroundColor?: ChartColor | ChartColor[] | Scriptable; 33 | hoverBorderWidth?: number | number[] | Scriptable; 34 | pointBorderWidth?: number | number[] | Scriptable; 35 | pointHoverBorderWidth?: number | number[] | Scriptable; 36 | pointHoverRadius?: number | number[] | Scriptable; 37 | pointRadius?: number | number[] | Scriptable; 38 | pointStyle?: 39 | | PointStyle 40 | | HTMLImageElement 41 | | HTMLCanvasElement 42 | | Array 43 | | Scriptable; 44 | backgroundColor?: ChartColor | ChartColor[] | Scriptable; 45 | color?: ChartColor | ChartColor[] | Scriptable; 46 | borderColor?: ChartColor | ChartColor[] | Scriptable; 47 | hoverBorderColor?: ChartColor | ChartColor[] | Scriptable; 48 | pointBorderColor?: ChartColor | ChartColor[] | Scriptable; 49 | pointBackgroundColor?: ChartColor | ChartColor[] | Scriptable; 50 | pointHoverBackgroundColor?: 51 | | ChartColor 52 | | ChartColor[] 53 | | Scriptable; 54 | pointHoverBorderColor?: ChartColor | ChartColor[] | Scriptable; 55 | 56 | constructor(fields: Partial) { 57 | super(fields); 58 | 59 | this.type = ChartTypes.Line; 60 | this.backgroundColor = fields.backgroundColor || "transparent"; 61 | this.borderCapStyle = fields.borderCapStyle || "round"; 62 | this.borderJoinStyle = fields.borderJoinStyle || "round"; 63 | this.borderWidth = fields.borderWidth || 2; 64 | this.hoverBackgroundColor = fields.hoverBackgroundColor || "transparent"; 65 | this.hoverBorderWidth = fields.hoverBorderWidth || 2.5; 66 | this.pointBorderWidth = fields.hoverBorderWidth || 0; 67 | this.pointHoverBorderWidth = fields.pointHoverBorderWidth || 0; 68 | this.pointHoverRadius = fields.pointHoverRadius || 2.5; 69 | this.pointRadius = fields.pointRadius || 2; 70 | this.pointStyle = fields.pointStyle || "circle"; 71 | this.borderColor = fields.borderColor || fields.color || "rgba(0,0,0,.1)"; 72 | this.hoverBorderColor = 73 | fields.hoverBorderColor || fields.color || "rgba(0,0,0,.1)"; 74 | this.pointBorderColor = 75 | fields.pointBorderColor || fields.color || "rgba(0,0,0,.1)"; 76 | this.pointBackgroundColor = 77 | fields.pointBackgroundColor || fields.color || "rgba(0,0,0,.1)"; 78 | this.pointHoverBackgroundColor = 79 | fields.pointHoverBackgroundColor || fields.color || "rgba(0,0,0,.1)"; 80 | this.pointHoverBorderColor = 81 | fields.pointHoverBorderColor || fields.color || "rgba(0,0,0,.1)"; 82 | } 83 | } 84 | 85 | export class TrendLineDataSetStyle 86 | extends ChartDataSet 87 | implements ChartDataSets { 88 | borderCapStyle?: "butt" | "round" | "square"; 89 | borderJoinStyle?: "bevel" | "round" | "miter"; 90 | borderWidth?: BorderWidth | BorderWidth[] | Scriptable; 91 | hoverBackgroundColor?: ChartColor | ChartColor[] | Scriptable; 92 | hoverBorderWidth?: number | number[] | Scriptable; 93 | pointBorderWidth?: number | number[] | Scriptable; 94 | pointHoverBorderWidth?: number | number[] | Scriptable; 95 | pointHoverRadius?: number | number[] | Scriptable; 96 | pointRadius?: number | number[] | Scriptable; 97 | pointStyle?: 98 | | PointStyle 99 | | HTMLImageElement 100 | | HTMLCanvasElement 101 | | Array 102 | | Scriptable; 103 | backgroundColor?: ChartColor | ChartColor[] | Scriptable; 104 | color?: ChartColor | ChartColor[] | Scriptable; 105 | borderColor?: ChartColor | ChartColor[] | Scriptable; 106 | hoverBorderColor?: ChartColor | ChartColor[] | Scriptable; 107 | pointBorderColor?: ChartColor | ChartColor[] | Scriptable; 108 | pointBackgroundColor?: ChartColor | ChartColor[] | Scriptable; 109 | pointHoverBackgroundColor?: 110 | | ChartColor 111 | | ChartColor[] 112 | | Scriptable; 113 | pointHoverBorderColor?: ChartColor | ChartColor[] | Scriptable; 114 | 115 | constructor(fields: Partial) { 116 | super(fields); 117 | 118 | this.type = ChartTypes.Line; 119 | this.backgroundColor = fields.backgroundColor || "transparent"; 120 | this.borderCapStyle = fields.borderCapStyle || "round"; 121 | this.borderJoinStyle = fields.borderJoinStyle || "round"; 122 | this.borderWidth = fields.borderWidth || 2; 123 | this.hoverBackgroundColor = fields.hoverBackgroundColor || "transparent"; 124 | this.hoverBorderWidth = fields.hoverBorderWidth || 2.5; 125 | this.pointBorderWidth = fields.hoverBorderWidth || 0; 126 | this.pointHoverBorderWidth = fields.pointHoverBorderWidth || 0; 127 | this.pointHoverRadius = fields.pointHoverRadius || 0; 128 | this.pointRadius = fields.pointRadius || 0; 129 | this.pointStyle = fields.pointStyle || "circle"; 130 | this.borderColor = fields.borderColor || fields.color || "rgba(0,0,0,.1)"; 131 | this.hoverBorderColor = 132 | fields.hoverBorderColor || fields.color || "rgba(0,0,0,.1)"; 133 | this.pointBorderColor = 134 | fields.pointBorderColor || fields.color || "rgba(0,0,0,.1)"; 135 | this.pointBackgroundColor = 136 | fields.pointBackgroundColor || fields.color || "rgba(0,0,0,.1)"; 137 | this.pointHoverBackgroundColor = 138 | fields.pointHoverBackgroundColor || fields.color || "rgba(0,0,0,.1)"; 139 | this.pointHoverBorderColor = 140 | fields.pointHoverBorderColor || fields.color || "rgba(0,0,0,.1)"; 141 | } 142 | } 143 | 144 | export class LineDataSetHCStyle extends LineDataSetStyle { 145 | borderColor?: string; 146 | hoverBorderColor?: string; 147 | pointBorderColor?: string; 148 | pointBackgroundColor?: string; 149 | pointHoverBackgroundColor?: string; 150 | pointHoverBorderColor?: string; 151 | hoverBorderWidth?: number | number[] | Scriptable; 152 | pointRadius?: number | number[] | Scriptable; 153 | pointHoverRadius?: number | number[] | Scriptable; 154 | borderDash: number[]; 155 | pointStyle: 156 | | PointStyle 157 | | HTMLImageElement 158 | | HTMLCanvasElement 159 | | Array 160 | | Scriptable; 161 | 162 | constructor(fields: LineDataSetHCStyle) { 163 | super(fields); 164 | this.borderColor = HighContrastColors.Foreground; 165 | this.hoverBorderColor = HighContrastColors.Active; 166 | this.pointBorderColor = HighContrastColors.Foreground; 167 | this.pointBackgroundColor = HighContrastColors.Foreground; 168 | this.pointHoverBackgroundColor = HighContrastColors.Foreground; 169 | this.pointHoverBorderColor = HighContrastColors.Foreground; 170 | this.hoverBorderWidth = 4; 171 | this.pointRadius = 4; 172 | this.pointHoverRadius = 4; 173 | this.borderDash = fields.borderDash || []; 174 | this.pointStyle = fields.pointStyle || Point.Circle; 175 | } 176 | } 177 | 178 | export class TrendLineDataSetHCStyle extends LineDataSetHCStyle { 179 | constructor(fields: TrendLineDataSetHCStyle) { 180 | super(fields); 181 | this.pointBorderWidth = fields.hoverBorderWidth || 0; 182 | this.pointHoverBorderWidth = fields.pointHoverBorderWidth || 0; 183 | this.pointHoverRadius = fields.pointHoverRadius || 0; 184 | this.pointRadius = fields.pointRadius || 0; 185 | } 186 | } 187 | 188 | export class LineStackedDataSetStyle extends LineDataSetStyle { 189 | constructor(fields: LineStackedDataSetStyle) { 190 | super(fields); 191 | this.backgroundColor = 192 | fields.backgroundColor || fields.color || "rgba(0,0,0,.1)"; 193 | this.hoverBackgroundColor = 194 | fields.hoverBackgroundColor || fields.color || "rgba(0,0,0,.1)"; 195 | this.borderWidth = fields.borderWidth || 1; 196 | this.borderColor = fields.borderColor || "#ffffff"; 197 | this.pointRadius = fields.pointRadius || 0; 198 | this.pointHoverBackgroundColor = 199 | fields.pointHoverBackgroundColor || "#ffffff"; 200 | this.pointHoverRadius = fields.pointHoverRadius || 3; 201 | this.pointHoverBorderWidth = fields.pointHoverBorderWidth || 2; 202 | } 203 | } 204 | 205 | export class LineStackedDataSetHCStyle extends LineStackedDataSetStyle { 206 | pattern: IDraw; 207 | 208 | constructor(fields: LineStackedDataSetHCStyle) { 209 | super(fields); 210 | this.pattern = fields.pattern || { 211 | shape: Shapes.Square, 212 | size: 10, 213 | }; 214 | this.backgroundColor = buildPattern({ 215 | backgroundColor: HighContrastColors.Background, 216 | patternColor: HighContrastColors.Foreground, 217 | ...this.pattern, 218 | }) as any; 219 | this.hoverBackgroundColor = buildPattern({ 220 | backgroundColor: HighContrastColors.Background, 221 | patternColor: HighContrastColors.Active, 222 | ...this.pattern, 223 | }) as any; 224 | this.borderColor = HighContrastColors.Foreground; 225 | this.hoverBorderColor = HighContrastColors.Active; 226 | this.borderWidth = fields.borderWidth || 3; 227 | this.hoverBorderWidth = fields.hoverBorderWidth || 4; 228 | this.hoverBorderWidth = fields.hoverBorderWidth || 5; 229 | this.pointHoverRadius = fields.pointHoverRadius || 5; 230 | } 231 | } 232 | 233 | export class BarDataSetStyle extends ChartDataSet implements ChartDataSets { 234 | backgroundColor?: ChartColor | ChartColor[] | Scriptable; 235 | borderColor?: ChartColor | ChartColor[] | Scriptable; 236 | borderWidth?: BorderWidth | BorderWidth[] | Scriptable; 237 | color?: ChartColor | ChartColor[] | Scriptable; 238 | hoverBackgroundColor?: ChartColor | ChartColor[] | Scriptable; 239 | hoverBorderWidth?: number | number[] | Scriptable; 240 | borderSkipped?: PositionType | PositionType[] | Scriptable; 241 | 242 | constructor(fields: BarDataSetStyle) { 243 | super(fields); 244 | this.borderWidth = fields.borderWidth || 0; 245 | this.backgroundColor = 246 | fields.backgroundColor || fields.color || "rgba(0,0,0,.1)"; 247 | this.hoverBorderWidth = fields.hoverBorderWidth || 0; 248 | this.hoverBackgroundColor = 249 | fields.hoverBackgroundColor || fields.color || "rgba(0,0,0,.1)"; 250 | this.borderSkipped = fields.borderSkipped || (false as any); 251 | this.type = ChartTypes.Bar as string; 252 | } 253 | } 254 | 255 | export class PieDataSetStyle extends ChartDataSet implements ChartDataSets { 256 | backgroundColor?: ChartColor | ChartColor[] | Scriptable; 257 | borderColor?: ChartColor | ChartColor[] | Scriptable; 258 | borderWidth?: BorderWidth | BorderWidth[] | Scriptable; 259 | color?: ChartColor[]; 260 | hoverBackgroundColor?: ChartColor | ChartColor[] | Scriptable; 261 | hoverBorderColor?: ChartColor | ChartColor[] | Scriptable; 262 | borderSkipped?: PositionType | PositionType[] | Scriptable; 263 | 264 | constructor(fields: PieDataSetStyle) { 265 | super(fields); 266 | this.type = ChartTypes.Pie; 267 | this.borderWidth = fields.borderWidth || 2; 268 | this.borderColor = fields.borderColor || "#fff"; 269 | this.hoverBorderColor = fields.hoverBorderColor || "#fff"; 270 | this.backgroundColor = 271 | fields.backgroundColor || fields.color || "rgba(0,0,0,.1)"; 272 | this.hoverBackgroundColor = 273 | fields.hoverBackgroundColor || fields.color || "rgba(0,0,0,.1)"; 274 | this.borderSkipped = fields.borderSkipped || (false as any); 275 | } 276 | } 277 | 278 | export class DoughnutDataSetStyle extends PieDataSetStyle { 279 | constructor(fields: DoughnutDataSetStyle) { 280 | super(fields); 281 | 282 | this.type = ChartTypes.Doughnut; 283 | } 284 | } 285 | 286 | export class HorizontalBarDataSetStyle 287 | extends ChartDataSet 288 | implements ChartDataSets { 289 | backgroundColor?: ChartColor | ChartColor[] | Scriptable; 290 | barPercentage?: number; 291 | borderColor?: ChartColor | ChartColor[] | Scriptable; 292 | borderWidth?: BorderWidth | BorderWidth[] | Scriptable; 293 | color?: ChartColor | ChartColor[] | Scriptable; 294 | hoverBackgroundColor?: ChartColor | ChartColor[] | Scriptable; 295 | hoverBorderWidth?: number | number[] | Scriptable; 296 | borderSkipped?: PositionType | PositionType[] | Scriptable; 297 | 298 | constructor(fields: HorizontalBarDataSetStyle) { 299 | super(fields); 300 | this.type = ChartTypes.HorizontalBar; 301 | this.borderWidth = fields.borderWidth || 0; 302 | this.barPercentage = fields.barPercentage || 0.5; 303 | this.backgroundColor = 304 | fields.backgroundColor || fields.color || "rgba(0,0,0,.1)"; 305 | this.hoverBorderWidth = fields.hoverBorderWidth || 0; 306 | this.hoverBackgroundColor = 307 | fields.hoverBackgroundColor || fields.color || "rgba(0,0,0,.1)"; 308 | this.borderSkipped = fields.borderSkipped || (false as any); 309 | } 310 | } 311 | 312 | export class HorizontalBarDataSetHCStyle extends HorizontalBarDataSetStyle { 313 | hoverBorderColor?: ChartColor | ChartColor[] | Scriptable; 314 | pattern: IDraw; 315 | 316 | constructor(fields: BarDataSetHCStyle) { 317 | super(fields); 318 | this.pattern = fields.pattern || { 319 | shape: Shapes.Square, 320 | size: 10, 321 | }; 322 | this.backgroundColor = buildPattern({ 323 | backgroundColor: HighContrastColors.Background, 324 | patternColor: HighContrastColors.Foreground, 325 | ...this.pattern, 326 | }) as any; 327 | this.hoverBackgroundColor = buildPattern({ 328 | backgroundColor: HighContrastColors.Background, 329 | patternColor: HighContrastColors.Active, 330 | ...this.pattern, 331 | }) as any; 332 | 333 | this.borderWidth = fields.borderWidth || 1; 334 | this.borderColor = HighContrastColors.Foreground; 335 | this.hoverBorderWidth = fields.hoverBorderWidth || 3; 336 | this.hoverBorderColor = HighContrastColors.Active; 337 | } 338 | } 339 | 340 | export class PieDataSetHCStyle extends PieDataSetStyle { 341 | pattern: IDraw[]; 342 | 343 | constructor(fields: PieDataSetHCStyle) { 344 | super(fields); 345 | this.pattern = fields.pattern || [ 346 | { 347 | shape: Shapes.Square, 348 | size: 10, 349 | }, 350 | ]; 351 | this.borderWidth = fields.borderWidth || 3; 352 | this.borderColor = HighContrastColors.Foreground; 353 | this.hoverBorderColor = HighContrastColors.Active; 354 | this.backgroundColor = Array.from( 355 | fields.pattern, 356 | (pat) => 357 | buildPattern({ 358 | backgroundColor: HighContrastColors.Background, 359 | patternColor: HighContrastColors.Foreground, 360 | ...pat, 361 | }) as any 362 | ); 363 | this.hoverBackgroundColor = Array.from( 364 | fields.pattern, 365 | (pat) => 366 | buildPattern({ 367 | backgroundColor: HighContrastColors.Background, 368 | patternColor: HighContrastColors.Active, 369 | ...pat, 370 | }) as any 371 | ); 372 | } 373 | } 374 | 375 | export class DoughnutDataSetHCStyle extends PieDataSetHCStyle { 376 | constructor(fields: PieDataSetHCStyle) { 377 | super(fields); 378 | } 379 | } 380 | 381 | export class BarDataSetHCStyle extends BarDataSetStyle { 382 | hoverBorderColor?: ChartColor | ChartColor[] | Scriptable; 383 | pattern: IDraw; 384 | 385 | constructor(fields: BarDataSetHCStyle) { 386 | super(fields); 387 | this.pattern = fields.pattern || { 388 | shape: Shapes.Square, 389 | size: 10, 390 | }; 391 | this.backgroundColor = buildPattern({ 392 | backgroundColor: HighContrastColors.Background, 393 | patternColor: HighContrastColors.Foreground, 394 | ...this.pattern, 395 | }) as any; 396 | this.hoverBackgroundColor = buildPattern({ 397 | backgroundColor: HighContrastColors.Background, 398 | patternColor: HighContrastColors.Active, 399 | ...this.pattern, 400 | }) as any; 401 | 402 | this.borderWidth = fields.borderWidth || 1; 403 | this.borderColor = HighContrastColors.Foreground; 404 | this.hoverBorderWidth = fields.hoverBorderWidth || 3; 405 | this.hoverBorderColor = HighContrastColors.Active; 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /src/lib/patterns.ts: -------------------------------------------------------------------------------- 1 | import { Entity, IChartPatterns, Shapes } from "../types"; 2 | 3 | const BACKGROUND_COLOR = "transparent"; 4 | const PATTERN_COLOR = "rgba(0, 0, 0, 0.8)"; 5 | const POINT_STYLE = "round"; 6 | const SIZE = 20; 7 | 8 | export const Patterns = { 9 | Square: { 10 | shape: Shapes.Square, 11 | size: 10, 12 | }, 13 | Diagonal: { 14 | shape: Shapes.DiagonalRightLeft, 15 | size: 5, 16 | }, 17 | Diagonal2: { 18 | shape: Shapes.Diagonal, 19 | size: 5, 20 | }, 21 | Grid: { 22 | shape: Shapes.Grid, 23 | size: 10, 24 | }, 25 | Grid2: { 26 | shape: Shapes.GridRightLeft, 27 | size: 3, 28 | }, 29 | Line: { 30 | shape: Shapes.VerticalLine, 31 | size: 10, 32 | }, 33 | }; 34 | 35 | // export const legendLabels = ({ 36 | // canvasRef, 37 | // theme, 38 | // colorScheme, 39 | // dataPointColor, 40 | // index, 41 | // patterns, 42 | // }: { 43 | // canvasRef: HTMLCanvasElement; 44 | // theme: ChartTheme; 45 | // colorScheme: any; 46 | // dataPointColor: string; 47 | // index: number; 48 | // patterns?: IChartPatterns; 49 | // }) => { 50 | // if (!canvasRef) return; 51 | // const ctx: any = canvasRef.getContext("2d"); 52 | // ctx.save(); 53 | // if (!ctx) return; 54 | // if (theme === ChartTheme.HighContrast) { 55 | // if (patterns) { 56 | // ctx.setTransform(1.4, 0, 0, 1, 0, 0); 57 | // ctx.scale(12, 10); 58 | // (ctx.fillStyle as any) = buildPattern({ 59 | // ...patterns(colorScheme)[index], 60 | // backgroundColor: colorScheme.default.background, 61 | // patternColor: colorScheme.brand.background, 62 | // }); 63 | // ctx.fillRect(-15, -15, canvasRef.width, canvasRef.height); 64 | // ctx.restore(); 65 | // } else { 66 | // ctx.scale(15, 15); 67 | // ctx.fillStyle = colorScheme.brand.shadow; 68 | // ctx.fillRect(-15, -15, canvasRef.width, canvasRef.height); 69 | // ctx.fillStyle = colorScheme.default.foreground3; 70 | // switch (lineChartPatterns[index].pointStyle) { 71 | // case PointStyles.Triangle: 72 | // ctx.moveTo(9.5, 2.5); 73 | // ctx.lineTo(5.5, 7.5); 74 | // ctx.lineTo(13.5, 7.5); 75 | // break; 76 | // case PointStyles.Rectangle: 77 | // ctx.rect(6.5, 2.5, 8, 5); 78 | // break; 79 | // case PointStyles.RectangleRotated: 80 | // ctx.moveTo(10, 2); 81 | // ctx.lineTo(14.5, 5); 82 | // ctx.lineTo(10, 8); 83 | // ctx.lineTo(5.5, 5); 84 | // break; 85 | // case PointStyles.Circle: 86 | // default: 87 | // ctx.ellipse(10, 5, 3.5, 2.5, 0, 0, 2 * Math.PI); 88 | // break; 89 | // } 90 | // ctx.fill(); 91 | 92 | // // Line Style 93 | // ctx.strokeStyle = colorScheme.default.foreground3; 94 | // ctx.beginPath(); 95 | // ctx.setLineDash( 96 | // lineChartPatterns[index].lineBorderDash.length ? [2, 2] : [] 97 | // ); 98 | // ctx.moveTo(-1.5, 5); 99 | // ctx.lineTo(20, 5); 100 | // ctx.stroke(); 101 | // ctx.restore(); 102 | // } 103 | // } else { 104 | // ctx.fillStyle = dataPointColor; 105 | // ctx.fillRect(0, 0, canvasRef.width, canvasRef.height); 106 | // } 107 | // }; 108 | 109 | // export const chartLineStackedDataPointPatterns: IChartPatterns = ( 110 | // colorScheme: any 111 | // ) => { 112 | // return [ 113 | // { 114 | // shapeType: Shapes.Square, 115 | // size: 10, 116 | // }, 117 | // { 118 | // shapeType: Shapes.DiagonalRightLeft, 119 | // size: 5, 120 | // }, 121 | // { 122 | // shapeType: Shapes.Grid, 123 | // size: 10, 124 | // }, 125 | // { 126 | // shapeType: Shapes.VerticalLine, 127 | // size: 10, 128 | // }, 129 | // { 130 | // shapeType: Shapes.GridRightLeft, 131 | // size: 3, 132 | // }, 133 | // { 134 | // shapeType: Shapes.Diagonal, 135 | // size: 5, 136 | // }, 137 | // ]; 138 | // }; 139 | 140 | // export const chartBarDataPointPatterns: IChartPatterns = (colorScheme: any) => { 141 | // return [ 142 | // { 143 | // shapeType: Shapes.DiagonalRightLeft, 144 | // size: 5, 145 | // }, 146 | // { 147 | // shapeType: Shapes.Square, 148 | // size: 10, 149 | // }, 150 | // { 151 | // shapeType: Shapes.Diagonal, 152 | // size: 5, 153 | // }, 154 | // { 155 | // shapeType: Shapes.Grid, 156 | // size: 10, 157 | // }, 158 | // { 159 | // shapeType: Shapes.GridRightLeft, 160 | // size: 3, 161 | // }, 162 | // { 163 | // shapeType: Shapes.VerticalLine, 164 | // size: 7, 165 | // }, 166 | // ]; 167 | // }; 168 | 169 | // export const chartBubbleDataPointPatterns: IChartPatterns = ( 170 | // colorScheme: any 171 | // ) => { 172 | // return [ 173 | // { 174 | // shapeType: Shapes.DiagonalRightLeft, 175 | // size: 5, 176 | // }, 177 | // { 178 | // shapeType: Shapes.Square, 179 | // size: 10, 180 | // }, 181 | // { 182 | // shapeType: Shapes.Diagonal, 183 | // size: 5, 184 | // }, 185 | // { 186 | // shapeType: Shapes.Grid, 187 | // size: 10, 188 | // }, 189 | // { 190 | // shapeType: Shapes.GridRightLeft, 191 | // size: 3, 192 | // }, 193 | // { 194 | // shapeType: Shapes.VerticalLine, 195 | // size: 7, 196 | // }, 197 | // ]; 198 | // }; 199 | 200 | export class Shape extends Entity { 201 | canvas?: HTMLCanvasElement; 202 | context?: CanvasRenderingContext2D | null; 203 | size = SIZE; 204 | backgroundColor: string = BACKGROUND_COLOR; 205 | patternColor: string = PATTERN_COLOR; 206 | 207 | constructor(fields: Partial) { 208 | super(fields); 209 | 210 | if (fields.size) { 211 | this.size = fields.size; 212 | } 213 | if (fields.backgroundColor) { 214 | this.backgroundColor = fields.backgroundColor; 215 | } 216 | if (fields.patternColor) { 217 | this.patternColor = fields.patternColor; 218 | } 219 | 220 | this.canvas = document.createElement("canvas"); 221 | this.context = this.canvas.getContext("2d"); 222 | 223 | this.canvas.width = this.size; 224 | this.canvas.height = this.size; 225 | 226 | if (this.context) { 227 | this.context.fillStyle = this.backgroundColor; 228 | this.context.fillRect(0, 0, this.canvas.width, this.canvas.height); 229 | } 230 | } 231 | 232 | setStrokeProps() { 233 | if (this.context) { 234 | this.context.strokeStyle = this.patternColor; 235 | this.context.lineWidth = this.size / 10; 236 | this.context.lineJoin = POINT_STYLE; 237 | this.context.lineCap = POINT_STYLE; 238 | } 239 | } 240 | 241 | setFillProps() { 242 | if (this.context) { 243 | this.context.fillStyle = this.patternColor; 244 | } 245 | } 246 | } 247 | 248 | class Square extends Shape { 249 | drawTile() { 250 | const halfSize = this.size / 2; 251 | if (this.context) { 252 | this.context.beginPath(); 253 | this.setFillProps(); 254 | this.drawSquare(); 255 | this.drawSquare(halfSize, halfSize); 256 | this.context.fill(); 257 | } 258 | return this.canvas; 259 | } 260 | 261 | drawSquare(offsetX = 0, offsetY = 0) { 262 | const halfSize = this.size / 2; 263 | const gap = this.size / 5; 264 | this.context!.fillRect( 265 | offsetX + gap, 266 | offsetY + gap, 267 | halfSize - gap * 2, 268 | halfSize - gap * 2 269 | ); 270 | this.context!.closePath(); 271 | } 272 | } 273 | 274 | class Diagonal extends Shape { 275 | drawTile() { 276 | const halfSize = this.size / 2; 277 | 278 | if (this.context) { 279 | this.context.beginPath(); 280 | 281 | this.setStrokeProps(); 282 | 283 | this.drawDiagonalLine(); 284 | this.drawDiagonalLine(halfSize, halfSize); 285 | 286 | this.context.stroke(); 287 | return this.canvas; 288 | } 289 | } 290 | 291 | drawDiagonalLine(offsetX = 0, offsetY = 0) { 292 | const size = this.size; 293 | const halfSize = size / 2; 294 | const gap = 1; 295 | 296 | if (this.context) { 297 | this.context.moveTo(halfSize - gap - offsetX, gap * -1 + offsetY); 298 | this.context.lineTo(size + 1 - offsetX, halfSize + 1 + offsetY); 299 | 300 | this.context.closePath(); 301 | } 302 | } 303 | } 304 | 305 | class DiagonalRightLeft extends Diagonal { 306 | drawTile() { 307 | if (this.context) { 308 | this.context.translate(this.size, 0); 309 | this.context.rotate((90 * Math.PI) / 180); 310 | 311 | Diagonal.prototype.drawTile.call(this); 312 | 313 | return this.canvas; 314 | } 315 | } 316 | } 317 | 318 | class Grid extends Shape { 319 | drawTile() { 320 | const halfSize = this.size / 2; 321 | 322 | if (this.context) { 323 | this.context.beginPath(); 324 | 325 | this.setStrokeProps(); 326 | 327 | // this.drawDiagonalLine(); 328 | // this.drawDiagonalLine(halfSize, halfSize); 329 | 330 | this.drawOpositeDiagonalLine(); 331 | this.drawOpositeDiagonalLine(halfSize, halfSize); 332 | 333 | this.context.stroke(); 334 | } 335 | 336 | return this.canvas; 337 | } 338 | 339 | drawDiagonalLine(offsetX = 0, offsetY = 0) { 340 | const size = this.size; 341 | const halfSize = size / 2; 342 | const gap = 1; 343 | 344 | if (this.context) { 345 | this.context.moveTo(halfSize - gap - offsetX, gap * -1 + offsetY); 346 | this.context.lineTo(size + 1 - offsetX, halfSize + 1 + offsetY); 347 | 348 | this.context.closePath(); 349 | } 350 | } 351 | 352 | drawOpositeDiagonalLine(offsetX = 0, offsetY = 0) { 353 | const size = this.size; 354 | const halfSize = size / 2; 355 | const gap = 1; 356 | 357 | if (this.context) { 358 | this.context.moveTo(halfSize - gap + offsetX, gap * -1 - offsetY); 359 | this.context.lineTo(size + 1 + offsetX, halfSize + 1 - offsetY); 360 | 361 | this.context.closePath(); 362 | } 363 | } 364 | } 365 | 366 | class Line extends Shape { 367 | drawTile() { 368 | if (this.context) { 369 | const halfSize = this.size / 2; 370 | 371 | this.context.beginPath(); 372 | 373 | this.setStrokeProps(); 374 | 375 | this.drawLine(); 376 | this.drawLine(halfSize, halfSize); 377 | 378 | this.context.stroke(); 379 | 380 | return this.canvas; 381 | } 382 | } 383 | 384 | drawLine(offsetX = 0, offsetY = 0) { 385 | if (this.context) { 386 | const size = this.size; 387 | const quarterSize = size / 4; 388 | 389 | this.context.moveTo(0, quarterSize + offsetY); 390 | this.context.lineTo(this.size, quarterSize + offsetY); 391 | 392 | this.context.closePath(); 393 | } 394 | } 395 | } 396 | 397 | class VerticalLine extends Line { 398 | drawTile() { 399 | if (this.context) { 400 | this.context.translate(this.size, 0); 401 | this.context.rotate((90 * Math.PI) / 180); 402 | 403 | Line.prototype.drawTile.call(this); 404 | 405 | return this.canvas; 406 | } 407 | } 408 | } 409 | 410 | class GridRightLeft extends Grid { 411 | drawTile() { 412 | if (this.context) { 413 | this.context.translate(this.size, 0); 414 | this.context.rotate((90 * Math.PI) / 180); 415 | 416 | Grid.prototype.drawTile.call(this); 417 | 418 | return this.canvas; 419 | } 420 | } 421 | } 422 | 423 | const shapes = { 424 | [Shapes.Square]: Square, 425 | [Shapes.DiagonalRightLeft]: DiagonalRightLeft, 426 | [Shapes.Grid]: Grid, 427 | [Shapes.Diagonal]: Diagonal, 428 | [Shapes.VerticalLine]: VerticalLine, 429 | [Shapes.GridRightLeft]: GridRightLeft, 430 | }; 431 | 432 | export function buildPattern({ 433 | shape, 434 | backgroundColor, 435 | patternColor, 436 | size, 437 | }: { 438 | shape: Shapes; 439 | size: number; 440 | backgroundColor: string; 441 | patternColor: string; 442 | }) { 443 | const patternCanvas = document.createElement("canvas"); 444 | const patternContext = patternCanvas.getContext("2d"); 445 | const outerSize = size * 2; 446 | 447 | const Shape = shapes[shape]; 448 | const _shape = new Shape({ size, backgroundColor, patternColor }); 449 | 450 | const pattern: CanvasPattern | null = patternContext!.createPattern( 451 | _shape.drawTile()!, 452 | "repeat" 453 | ); 454 | 455 | patternCanvas.width = outerSize; 456 | patternCanvas.height = outerSize; 457 | 458 | if (pattern) { 459 | (pattern as any).shape = shape; 460 | } 461 | 462 | return pattern; 463 | } 464 | -------------------------------------------------------------------------------- /src/lib/plugins.ts: -------------------------------------------------------------------------------- 1 | import { ChartDataSets } from "chart.js"; 2 | import { LineStackedDataSetHCStyle } from ".."; 3 | import { ChartTypes, HighContrastColors, IChartOptions } from "../types"; 4 | import { buildPattern } from "./patterns"; 5 | import { 6 | HALF_PI, 7 | hexToRgb, 8 | PI, 9 | QUARTER_PI, 10 | showTooltipOnKayboard, 11 | TWO_THIRDS_PI, 12 | } from "./utils"; 13 | 14 | export const gradientPlugin = (chartInstance: Chart) => { 15 | const { ctx } = chartInstance; 16 | if (!ctx) return; 17 | if (!chartInstance.config.data) return; 18 | if (!chartInstance.config.data.datasets) return; 19 | 20 | chartInstance.config.data.datasets.forEach((set: ChartDataSets) => { 21 | const color = String(set.borderColor); 22 | 23 | /** 24 | * TODO: Add more flexibility with color formats 25 | * Gradient works just with HEX | RGB values; 26 | */ 27 | 28 | if (!(color.includes("#") || color.includes("rgb("))) return; 29 | const gradientStroke: any = ctx.createLinearGradient( 30 | 0, 31 | 0, 32 | 0, 33 | ctx.canvas.clientHeight * 0.8 34 | ); 35 | const colorRGB = color.includes("#") 36 | ? hexToRgb(color) 37 | : color 38 | .substring(4, color.length - 1) 39 | .replace(/ /g, "") 40 | .split(","); 41 | gradientStroke.addColorStop(0, `rgba(${colorRGB}, .2)`); 42 | gradientStroke.addColorStop(1, `rgba(${colorRGB}, .0)`); 43 | set.backgroundColor = gradientStroke; 44 | 45 | const hoverColor = String(set.hoverBorderColor); 46 | if (!(hoverColor.includes("#") || hoverColor.includes("rgb("))) return; 47 | const hoverGradientStroke = ctx.createLinearGradient( 48 | 0, 49 | 0, 50 | 0, 51 | ctx.canvas.clientHeight * 0.8 52 | ); 53 | const hoverColorRGB = hoverColor.includes("#") 54 | ? hexToRgb(hoverColor) 55 | : hoverColor 56 | .substring(4, hoverColor.length - 1) 57 | .replace(/ /g, "") 58 | .split(","); 59 | hoverGradientStroke.addColorStop(0, `rgba(${hoverColorRGB}, .4)`); 60 | hoverGradientStroke.addColorStop(1, `rgba(${hoverColorRGB}, .0)`); 61 | set.hoverBackgroundColor = hoverGradientStroke; 62 | }); 63 | }; 64 | 65 | export const highLightDataOnHover = (chartInstance: any) => { 66 | const { chart, ctx, tooltip, config } = chartInstance; 67 | 68 | if (tooltip._active && tooltip._active.length) { 69 | const activePoint = tooltip._active[0], 70 | y = activePoint.tooltipPosition().y, 71 | x = activePoint.tooltipPosition().x, 72 | x_axis = chart.scales["x-axis-0"], 73 | y_axis = chart.scales["y-axis-0"], 74 | leftX = x_axis.left, 75 | rightX = x_axis.right, 76 | topY = y_axis.top, 77 | bottomY = y_axis.bottom; 78 | 79 | switch (config.type) { 80 | case ChartTypes.Line: 81 | ctx.save(); 82 | // Line 83 | ctx.beginPath(); 84 | ctx.moveTo(leftX - 44, y); 85 | ctx.lineTo(rightX, y); 86 | ctx.setLineDash([5, 5]); 87 | ctx.lineWidth = 1; 88 | ctx.strokeStyle = chart.options.scales.yAxes[0].gridLines.color; 89 | ctx.stroke(); 90 | 91 | // Point 92 | const pointStyle = 93 | activePoint._chart.config.data.datasets[activePoint._datasetIndex] 94 | .pointStyle; 95 | if (pointStyle) { 96 | const radius = 5; 97 | const rotation = 0; 98 | let xOffset = 0; 99 | let yOffset = 0; 100 | let cornerRadius = 1; 101 | let size = 5; 102 | let rad = 0; 103 | 104 | ctx.beginPath(); 105 | ctx.setLineDash([]); 106 | switch (pointStyle) { 107 | // Default includes circle 108 | default: 109 | ctx.arc(x, y, radius, 0, Math.PI * 2, true); 110 | ctx.closePath(); 111 | break; 112 | case "triangle": 113 | ctx.moveTo( 114 | x + Math.sin(rad) * radius, 115 | y - Math.cos(rad) * radius 116 | ); 117 | rad += TWO_THIRDS_PI; 118 | ctx.lineTo( 119 | x + Math.sin(rad) * radius, 120 | y - Math.cos(rad) * radius 121 | ); 122 | rad += TWO_THIRDS_PI; 123 | ctx.lineTo( 124 | x + Math.sin(rad) * radius, 125 | y - Math.cos(rad) * radius 126 | ); 127 | ctx.closePath(); 128 | break; 129 | case "rectRounded": 130 | cornerRadius = radius * 0.516; 131 | size = radius - cornerRadius; 132 | xOffset = Math.cos(rad + QUARTER_PI) * size; 133 | yOffset = Math.sin(rad + QUARTER_PI) * size; 134 | ctx.arc( 135 | x - xOffset, 136 | y - yOffset, 137 | cornerRadius, 138 | rad - PI, 139 | rad - HALF_PI 140 | ); 141 | ctx.arc( 142 | x + yOffset, 143 | y - xOffset, 144 | cornerRadius, 145 | rad - HALF_PI, 146 | rad 147 | ); 148 | ctx.arc( 149 | x + xOffset, 150 | y + yOffset, 151 | cornerRadius, 152 | rad, 153 | rad + HALF_PI 154 | ); 155 | ctx.arc( 156 | x - yOffset, 157 | y + xOffset, 158 | cornerRadius, 159 | rad + HALF_PI, 160 | rad + PI 161 | ); 162 | ctx.closePath(); 163 | break; 164 | case "rect": 165 | if (!rotation) { 166 | size = Math.SQRT1_2 * radius; 167 | ctx.rect(x - size, y - size, 2 * size, 2 * size); 168 | break; 169 | } 170 | rad += QUARTER_PI; 171 | /* falls through */ 172 | case "rectRot": 173 | xOffset = Math.cos(rad) * radius; 174 | yOffset = Math.sin(rad) * radius; 175 | ctx.moveTo(x - xOffset, y - yOffset); 176 | ctx.lineTo(x + yOffset, y - xOffset); 177 | ctx.lineTo(x + xOffset, y + yOffset); 178 | ctx.lineTo(x - yOffset, y + xOffset); 179 | ctx.closePath(); 180 | break; 181 | case "crossRot": 182 | rad += QUARTER_PI; 183 | /* falls through */ 184 | case "cross": 185 | xOffset = Math.cos(rad) * radius; 186 | yOffset = Math.sin(rad) * radius; 187 | ctx.moveTo(x - xOffset, y - yOffset); 188 | ctx.lineTo(x + xOffset, y + yOffset); 189 | ctx.moveTo(x + yOffset, y - xOffset); 190 | ctx.lineTo(x - yOffset, y + xOffset); 191 | break; 192 | case "star": 193 | xOffset = Math.cos(rad) * radius; 194 | yOffset = Math.sin(rad) * radius; 195 | ctx.moveTo(x - xOffset, y - yOffset); 196 | ctx.lineTo(x + xOffset, y + yOffset); 197 | ctx.moveTo(x + yOffset, y - xOffset); 198 | ctx.lineTo(x - yOffset, y + xOffset); 199 | rad += QUARTER_PI; 200 | xOffset = Math.cos(rad) * radius; 201 | yOffset = Math.sin(rad) * radius; 202 | ctx.moveTo(x - xOffset, y - yOffset); 203 | ctx.lineTo(x + xOffset, y + yOffset); 204 | ctx.moveTo(x + yOffset, y - xOffset); 205 | ctx.lineTo(x - yOffset, y + xOffset); 206 | break; 207 | case "line": 208 | xOffset = Math.cos(rad) * radius; 209 | yOffset = Math.sin(rad) * radius; 210 | ctx.moveTo(x - xOffset, y - yOffset); 211 | ctx.lineTo(x + xOffset, y + yOffset); 212 | break; 213 | case "dash": 214 | ctx.moveTo(x, y); 215 | ctx.lineTo( 216 | x + Math.cos(rad) * radius, 217 | y + Math.sin(rad) * radius 218 | ); 219 | break; 220 | } 221 | ctx.lineWidth = 2; 222 | ctx.fillStyle = HighContrastColors.Foreground; 223 | ctx.strokeStyle = 224 | chart.data.datasets[activePoint._datasetIndex].hoverBorderColor; 225 | ctx.closePath(); 226 | ctx.fill(); 227 | ctx.stroke(); 228 | ctx.restore(); 229 | } 230 | break; 231 | case ChartTypes.Bar: 232 | default: 233 | ctx.save(); 234 | // Line 235 | ctx.beginPath(); 236 | ctx.moveTo(leftX - 20, y); 237 | ctx.lineTo(rightX, y); 238 | ctx.setLineDash([5, 5]); 239 | ctx.lineWidth = 1; 240 | ctx.strokeStyle = chart.options.scales.yAxes[0].gridLines.color; 241 | ctx.stroke(); 242 | ctx.restore(); 243 | break; 244 | } 245 | } 246 | }; 247 | 248 | export const keyboardAccessibility = (chart: Chart) => { 249 | const { canvas, config } = chart; 250 | const { data, type } = config; 251 | let selectedIndex = -1; 252 | let selectedDataSet = 0; 253 | const orderedDataSet = 254 | type === ChartTypes.Line || !(config.options?.scales as any); 255 | 256 | if (!canvas) return; 257 | canvas.addEventListener("click", removeFocusStyleOnClick); 258 | canvas.addEventListener("keydown", changeFocus); 259 | canvas.addEventListener("focusout", resetChartStates); 260 | 261 | function meta() { 262 | return chart.getDatasetMeta(selectedDataSet); 263 | } 264 | 265 | function removeFocusStyleOnClick(): void { 266 | // Remove focus state style if selected by mouse 267 | if (canvas) { 268 | canvas.style.boxShadow = "none"; 269 | } 270 | } 271 | 272 | function removeDataPointsHoverStates() { 273 | if (selectedIndex > -1) { 274 | meta().controller.removeHoverStyle( 275 | meta().data[selectedIndex], 276 | 0, 277 | selectedIndex 278 | ); 279 | } 280 | } 281 | 282 | function hoverDataPoint(pointID: number) { 283 | meta().controller.setHoverStyle( 284 | meta().data[pointID], 285 | selectedDataSet, 286 | pointID 287 | ); 288 | } 289 | 290 | function showFocusedDataPoint() { 291 | hoverDataPoint(selectedIndex); 292 | if (chart.config.data) { 293 | showTooltipOnKayboard({ 294 | chart, 295 | setIndex: selectedDataSet, 296 | selectedPointIndex: selectedIndex, 297 | }); 298 | } 299 | document 300 | .getElementById( 301 | `${canvas!.id}-tooltip-${selectedDataSet}-${selectedIndex}` 302 | ) 303 | ?.focus(); 304 | } 305 | 306 | function resetChartStates() { 307 | removeDataPointsHoverStates(); 308 | const activeElements = (chart as any).tooltip._active; 309 | const requestedElem = chart.getDatasetMeta(selectedDataSet).data[ 310 | selectedIndex 311 | ]; 312 | activeElements.find((v: any, i: number) => { 313 | if (requestedElem._index === v._index) { 314 | activeElements.splice(i, 1); 315 | return true; 316 | } 317 | }); 318 | 319 | for (let i = 0; i < activeElements.length; i++) { 320 | if (requestedElem._index === activeElements[i]._index) { 321 | activeElements.splice(i, 1); 322 | break; 323 | } 324 | } 325 | if ((config.options as IChartOptions).highContrastMode) { 326 | data!.datasets!.map((dataset: any) => { 327 | dataset.borderColor = HighContrastColors.Foreground; 328 | dataset.borderWidth = 2; 329 | }); 330 | const fakeSet = new LineStackedDataSetHCStyle({} as any); 331 | config.data?.datasets?.forEach((set: any) => { 332 | if (typeof set.backgroundColor !== "string") { 333 | set.backgroundColor = buildPattern({ 334 | backgroundColor: HighContrastColors.Background, 335 | patternColor: fakeSet.borderColor, 336 | ...set.pattern, 337 | }) as any; 338 | } 339 | }); 340 | chart.update(); 341 | } 342 | (chart as any).tooltip._active = activeElements; 343 | (chart as any).tooltip.update(true); 344 | (chart as any).draw(); 345 | } 346 | 347 | function changeFocus(e: KeyboardEvent) { 348 | if (!data) return; 349 | if (!data.datasets) return; 350 | removeDataPointsHoverStates(); 351 | switch (e.key) { 352 | case "ArrowRight": 353 | e.preventDefault(); 354 | selectedIndex = (selectedIndex + 1) % meta().data.length; 355 | break; 356 | case "ArrowLeft": 357 | e.preventDefault(); 358 | selectedIndex = (selectedIndex || meta().data.length) - 1; 359 | break; 360 | case "ArrowUp": 361 | case "ArrowDown": 362 | e.preventDefault(); 363 | if (orderedDataSet) { 364 | if (e.key === "ArrowUp") { 365 | selectedDataSet += 1; 366 | if (selectedDataSet === data.datasets.length) { 367 | selectedDataSet = 0; 368 | } 369 | } else { 370 | selectedDataSet -= 1; 371 | if (selectedDataSet < 0) { 372 | selectedDataSet = data.datasets.length - 1; 373 | } 374 | } 375 | } else { 376 | // Get all values for the current data point 377 | const values = data.datasets.map( 378 | (dataset) => dataset.data![selectedIndex] 379 | ); 380 | // Sort an array to define next available number 381 | const sorted = Array.from(new Set(values)).sort( 382 | (a, b) => Number(a) - Number(b) 383 | ); 384 | let nextValue = 385 | sorted[ 386 | sorted.findIndex((v) => v === values[selectedDataSet]) + 387 | (e.key === "ArrowUp" ? 1 : -1) 388 | ]; 389 | 390 | // Find dataset ID by the next higher number after current 391 | let nextDataSet = values.findIndex((v) => v === nextValue); 392 | 393 | // If there is no next number that could selected, get number from oposite side 394 | if (nextDataSet < 0) { 395 | nextDataSet = values.findIndex( 396 | (v) => 397 | v === 398 | sorted[e.key === "ArrowUp" ? 0 : data!.datasets!.length - 1] 399 | ); 400 | } 401 | selectedDataSet = nextDataSet; 402 | selectedIndex = selectedIndex % meta().data.length; 403 | } 404 | break; 405 | } 406 | 407 | showFocusedDataPoint(); 408 | } 409 | }; 410 | 411 | export const axisXTeamsStyle = ({ config, ctx }: Chart) => { 412 | const xAxes = (config as any).options?.scales?.xAxes; 413 | const color = xAxes[0].gridLines?.color; 414 | const axesXGridLines = ctx!.createLinearGradient(100, 100, 100, 0); 415 | axesXGridLines.addColorStop(0.01, color); 416 | axesXGridLines.addColorStop(0.01, "transparent"); 417 | xAxes.forEach((xAxes: any, index: number) => { 418 | if (index < 1) { 419 | xAxes.gridLines.color = axesXGridLines; 420 | xAxes.gridLines.zeroLineColor = axesXGridLines; 421 | } else { 422 | xAxes.gridLines.color = "transparent"; 423 | } 424 | }); 425 | }; 426 | 427 | export const removeAllListeners = (chartInstance: Chart) => { 428 | const { canvas } = chartInstance; 429 | if (!canvas) return; 430 | canvas.replaceWith(canvas.cloneNode(true)); 431 | }; 432 | 433 | /** 434 | * Required review 435 | */ 436 | export const tooltipAxisXLine = ({ chart, ctx, tooltip }: any) => { 437 | if (tooltip._active && tooltip._active.length) { 438 | const activePoint = tooltip._active[0], 439 | y = activePoint.tooltipPosition().y, 440 | x_axis = chart.scales["x-axis-0"], 441 | leftX = x_axis.left, 442 | rightX = x_axis.right; 443 | 444 | ctx.save(); 445 | // Line 446 | ctx.beginPath(); 447 | ctx.moveTo(leftX - 20, y); 448 | ctx.lineTo(rightX, y); 449 | ctx.setLineDash([5, 5]); 450 | ctx.lineWidth = 1; 451 | ctx.strokeStyle = chart.options.scales.yAxes[0].gridLines.color; 452 | ctx.stroke(); 453 | ctx.restore(); 454 | } 455 | }; 456 | 457 | export const horizontalBarValue = ({ chart, ctx, config }: any) => { 458 | ctx.font = "bold 11px Segoe UI, system-ui, sans-serif"; 459 | ctx.textAlign = "left"; 460 | ctx.textBaseline = "middle"; 461 | ctx.fillStyle = config.options.scales.xAxes[0].ticks.fontColor; 462 | if (config.options.scales.yAxes[0].stacked) { 463 | const meta = chart.controller.getDatasetMeta( 464 | chart.data.datasets.length - 1 465 | ); 466 | meta.data.forEach((bar: any, index: number) => { 467 | let data = 0; 468 | chart.data.datasets.map((dataset: ChartDataSets) => { 469 | const value = dataset!.data![index]; 470 | if (typeof value === "number") { 471 | return (data += value); 472 | } 473 | }); 474 | ctx.fillText(data, bar._model.x + 8, bar._model.y); 475 | }); 476 | } else { 477 | chart.data.datasets.forEach((dataset: any, i: number) => { 478 | const meta = chart.controller.getDatasetMeta(i); 479 | meta.data.forEach((bar: any, index: number) => { 480 | const data = dataset.data[index]; 481 | ctx.fillText(data, bar._model.x + 8, bar._model.y); 482 | }); 483 | }); 484 | } 485 | }; 486 | 487 | export const horizontalBarAxisYLabels = (chartInstance: Chart) => { 488 | const { config, chartArea } = chartInstance; 489 | const { options, data } = config; 490 | 491 | if (!options) return; 492 | if (!chartArea) return; 493 | if (!options.scales) return; 494 | if (!options.scales.yAxes) return; 495 | if (!options.scales.yAxes[0]) return; 496 | if (!options.scales.yAxes[0].ticks) return; 497 | 498 | options.scales.yAxes[0].ticks.labelOffset = 499 | chartArea.bottom / data!.datasets![0]!.data!.length / 2 - 10; 500 | chartInstance.update(); 501 | }; 502 | -------------------------------------------------------------------------------- /src/lib/settings.ts: -------------------------------------------------------------------------------- 1 | import { ChartDataSets } from "chart.js"; 2 | import { HighContrastColors, IChartOptions } from "../types"; 3 | import { usNumberFormat, shortTicks } from "./utils"; 4 | import "chartjs-plugin-deferred"; 5 | 6 | const stackedTooltipTitle = ( 7 | item: Chart.ChartTooltipItem[], 8 | data: Chart.ChartData 9 | ): string => { 10 | let total = 0; 11 | if (!item) return ""; 12 | if (!data) return ""; 13 | if (!data.datasets) return ""; 14 | data.datasets.map((dataset: ChartDataSets) => { 15 | const value = dataset!.data![item[0].index!]; 16 | if (typeof value === "number") { 17 | return (total += value); 18 | } 19 | }); 20 | return `${((Number(item[0].yLabel) / total) * 100).toPrecision( 21 | 2 22 | )}% (${usNumberFormat(Number(item[0].yLabel))})`; 23 | }; 24 | 25 | export const defaultOptions: IChartOptions = { 26 | responsive: true, 27 | maintainAspectRatio: false, 28 | defaultColor: "#C8C6C4", 29 | legend: { 30 | display: false, 31 | custom: true, 32 | }, 33 | animation: { 34 | duration: 1000, 35 | }, 36 | layout: { 37 | padding: { 38 | left: 0, 39 | right: 16, 40 | top: 0, 41 | bottom: 0, 42 | }, 43 | }, 44 | elements: { 45 | line: { 46 | tension: 0.4, 47 | }, 48 | }, 49 | hover: { 50 | mode: "dataset", 51 | intersect: false, 52 | }, 53 | tooltips: { 54 | yPadding: 12, 55 | xPadding: 20, 56 | caretPadding: 10, 57 | // Tooltip Title 58 | titleFontStyle: "200", 59 | titleFontSize: 20, 60 | // Tooltip Body 61 | bodySpacing: 4, 62 | bodyFontSize: 11.5, 63 | bodyFontStyle: "400", 64 | // Tooltip Footer 65 | footerFontStyle: "300", 66 | footerFontSize: 10, 67 | 68 | backgroundColor: "rgba(0, 0, 0, 0.88)", 69 | 70 | callbacks: { 71 | title: (tooltipItems: any) => { 72 | const value = tooltipItems[0].yLabel; 73 | return typeof value === "number" && value > 999 74 | ? usNumberFormat(value) 75 | : value; 76 | }, 77 | label: (tooltipItem: any, data: any) => 78 | data.datasets[tooltipItem.datasetIndex].label, 79 | footer: (tooltipItems: any) => { 80 | const value = tooltipItems[0].xLabel; 81 | return typeof value === "number" && value > 999 82 | ? usNumberFormat(value) 83 | : value; 84 | }, 85 | }, 86 | }, 87 | scales: { 88 | xAxes: [ 89 | { 90 | ticks: { 91 | fontSize: 10, 92 | padding: 0, 93 | maxRotation: 0, 94 | minRotation: 0, 95 | callback: shortTicks, 96 | fontColor: "#605E5C", 97 | }, 98 | gridLines: { 99 | borderDash: [5, 9999], 100 | zeroLineBorderDash: [5, 9999], 101 | color: "#E1DFDD", 102 | }, 103 | }, 104 | ], 105 | yAxes: [ 106 | { 107 | ticks: { 108 | callback: shortTicks, 109 | fontSize: 10, 110 | padding: -16, 111 | labelOffset: 10, 112 | maxTicksLimit: 5, 113 | fontColor: "#605E5C", 114 | }, 115 | gridLines: { 116 | lineWidth: 1, 117 | drawBorder: false, 118 | drawTicks: true, 119 | tickMarkLength: 44, 120 | zeroLineColor: "#E1DFDD", 121 | color: "#E1DFDD", 122 | }, 123 | }, 124 | ], 125 | }, 126 | plugins: { 127 | deferred: { 128 | delay: 150, // delay of 500 ms after the canvas is considered inside the viewport 129 | }, 130 | }, 131 | }; 132 | 133 | export const highContrastOptions: IChartOptions = { 134 | highContrastMode: true, 135 | defaultColor: HighContrastColors.Foreground, 136 | tooltips: { 137 | backgroundColor: HighContrastColors.Background, 138 | borderColor: HighContrastColors.Active, 139 | multiKeyBackground: "transparent", 140 | titleFontColor: HighContrastColors.Foreground, 141 | bodyFontColor: HighContrastColors.Foreground, 142 | footerFontColor: HighContrastColors.Foreground, 143 | borderWidth: 2, 144 | displayColors: false, 145 | }, 146 | hover: { 147 | mode: "dataset", 148 | intersect: false, 149 | }, 150 | scales: { 151 | xAxes: [ 152 | { 153 | ticks: { 154 | fontColor: HighContrastColors.Foreground, 155 | }, 156 | }, 157 | ], 158 | yAxes: [ 159 | { 160 | ticks: { 161 | fontColor: HighContrastColors.Foreground, 162 | }, 163 | gridLines: { 164 | zeroLineColor: "rgba(255,255,255, .3)", 165 | color: "rgba(255,255,255, .3)", 166 | }, 167 | }, 168 | ], 169 | }, 170 | }; 171 | 172 | export const stackedLineOptions: IChartOptions = { 173 | scales: { 174 | yAxes: [ 175 | { 176 | stacked: true, 177 | }, 178 | ], 179 | }, 180 | tooltips: { 181 | callbacks: { 182 | title: stackedTooltipTitle, 183 | labelColor: (tooltipItem: any, chart: any) => { 184 | return { 185 | borderColor: "transparent", 186 | backgroundColor: 187 | chart.config.data.datasets[tooltipItem.datasetIndex] 188 | .backgroundColor, 189 | } as any; 190 | }, 191 | }, 192 | }, 193 | }; 194 | 195 | export const trendLineOptions: IChartOptions = { 196 | hover: { 197 | mode: "nearest", 198 | intersect: false, 199 | }, 200 | legend: { 201 | display: false, 202 | custom: false, 203 | }, 204 | layout: { 205 | padding: { 206 | left: -40, 207 | right: 0, 208 | top: 0, 209 | bottom: 0, 210 | }, 211 | }, 212 | scales: { 213 | xAxes: [ 214 | { 215 | ticks: { 216 | display: false, 217 | }, 218 | gridLines: { 219 | display: false, 220 | }, 221 | }, 222 | ], 223 | yAxes: [ 224 | { 225 | ticks: { 226 | display: false, 227 | }, 228 | gridLines: { 229 | display: false, 230 | }, 231 | }, 232 | ], 233 | }, 234 | plugins: { 235 | deferred: { 236 | xOffset: 150, // defer until 150px of the canvas width are inside the viewport 237 | yOffset: "100%", // defer until 50% of the canvas height are inside the viewport 238 | delay: 150, // delay of 500 ms after the canvas is considered inside the viewport 239 | }, 240 | }, 241 | }; 242 | 243 | export const barOptions: IChartOptions = { 244 | hover: { 245 | mode: "nearest", 246 | intersect: false, 247 | }, 248 | scales: { 249 | xAxes: [ 250 | { 251 | gridLines: { 252 | offsetGridLines: false, 253 | }, 254 | }, 255 | ], 256 | }, 257 | }; 258 | 259 | export const pieOptions: IChartOptions = { 260 | layout: { 261 | padding: { 262 | top: 32, 263 | right: 32, 264 | bottom: 32, 265 | left: -16, 266 | }, 267 | }, 268 | hover: { 269 | mode: "point", 270 | intersect: false, 271 | }, 272 | scales: { 273 | xAxes: [ 274 | { 275 | ticks: { 276 | display: false, 277 | }, 278 | gridLines: { 279 | display: false, 280 | }, 281 | }, 282 | ], 283 | yAxes: [ 284 | { 285 | ticks: { 286 | display: false, 287 | }, 288 | gridLines: { 289 | display: false, 290 | }, 291 | }, 292 | ], 293 | }, 294 | tooltips: { 295 | callbacks: { 296 | title: (tooltipItems: any, data: any) => { 297 | return `${( 298 | (Number(data.datasets[0].data[tooltipItems[0].index]) / 299 | (data.datasets[0].data as number[]).reduce((a, b) => a + b)) * 300 | 100 301 | ).toPrecision(2)}% (${usNumberFormat( 302 | Number(data.datasets[0].data[tooltipItems[0].index]) 303 | )})`; 304 | }, 305 | label: (tooltipItem: any, data: any) => data.labels[tooltipItem.index], 306 | }, 307 | }, 308 | }; 309 | 310 | export const doughnutOptions: IChartOptions = { 311 | cutoutPercentage: 70, 312 | ...pieOptions, 313 | }; 314 | 315 | export const groupedBarOptions: IChartOptions = { 316 | scales: { 317 | xAxes: [ 318 | { 319 | gridLines: { 320 | offsetGridLines: true, 321 | }, 322 | }, 323 | ], 324 | }, 325 | }; 326 | 327 | export const stackedBarOptions: IChartOptions = { 328 | hover: { 329 | mode: "nearest", 330 | intersect: false, 331 | }, 332 | scales: { 333 | xAxes: [ 334 | { 335 | stacked: true, 336 | gridLines: { 337 | offsetGridLines: false, 338 | }, 339 | }, 340 | ], 341 | yAxes: [{ stacked: true }], 342 | }, 343 | tooltips: { 344 | callbacks: { 345 | title: stackedTooltipTitle, 346 | }, 347 | }, 348 | }; 349 | 350 | export const horizontalBarOptions: IChartOptions = { 351 | layout: { 352 | padding: { 353 | top: -6, 354 | left: -32, 355 | }, 356 | }, 357 | hover: { 358 | mode: "index", 359 | intersect: false, 360 | }, 361 | scales: { 362 | xAxes: [ 363 | { 364 | ticks: { 365 | display: false, 366 | }, 367 | gridLines: { 368 | display: false, 369 | }, 370 | }, 371 | ], 372 | yAxes: [ 373 | { 374 | ticks: { 375 | mirror: true, 376 | padding: 0, 377 | callback: (v: string) => v, 378 | labelOffset: 26, 379 | }, 380 | gridLines: { 381 | display: false, 382 | }, 383 | }, 384 | ], 385 | }, 386 | tooltips: { 387 | position: "nearest", 388 | }, 389 | }; 390 | -------------------------------------------------------------------------------- /src/lib/storybook.tsx: -------------------------------------------------------------------------------- 1 | import { StoryFn } from "@storybook/addons"; 2 | import React, { ReactNode } from "react"; 3 | 4 | export interface IChartsProvider { 5 | children: ReactNode; 6 | } 7 | 8 | export const ChartsProvider = ({ children }: IChartsProvider) => { 9 | return ( 10 | <> 11 | 23 | {children} 24 | 25 | ); 26 | }; 27 | 28 | export interface IStorybookThemeProviderProps { 29 | children: ReactNode; 30 | } 31 | 32 | export const StorybookThemeProvider = ({ 33 | children, 34 | }: IStorybookThemeProviderProps) => {children}; 35 | 36 | export const withStorybookTheme = (storyFn: StoryFn) => ( 37 | {storyFn()} 38 | ); 39 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { ChartDataSets } from "chart.js"; 2 | import { ChartTypes, HighContrastColors, IChartOptions } from "../types"; 3 | import { buildPattern } from "./patterns"; 4 | import { LineDataSetHCStyle, LineStackedDataSetHCStyle } from "./datasets"; 5 | 6 | export const PI = Math.PI; 7 | export const HALF_PI = PI / 2; 8 | export const QUARTER_PI = PI / 4; 9 | export const TWO_THIRDS_PI = (PI * 2) / 3; 10 | 11 | export const random = (min: number, max: number): number => 12 | Math.round(Math.random() * (max - min) + min); 13 | 14 | // TODO: Localization 15 | const suffixes = ["K", "M", "G", "T", "P", "E"]; 16 | 17 | export const shortTicks = (value: number | string): string => { 18 | if (typeof value === "number") { 19 | if (value < 1000) { 20 | return String(value); 21 | } 22 | const exp = Math.floor(Math.log(Number(value)) / Math.log(1000)); 23 | value = `${Number(value) / Math.pow(1000, exp)}${suffixes[exp - 1]}`; 24 | // There is no support for label aligment in Chart.js, 25 | // to be able align axis labels by left (right is by default) 26 | // add an additional spaces depends on label length 27 | switch (value.length) { 28 | case 2: 29 | return value + " "; 30 | case 1: 31 | return value + " "; 32 | case 3: 33 | default: 34 | return value; 35 | } 36 | } else { 37 | return value; 38 | } 39 | }; 40 | 41 | export const hexToRgb = (hex: string) => { 42 | if (hex.length < 6) { 43 | hex = `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`; 44 | } 45 | const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 46 | return result 47 | ? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt( 48 | result[3], 49 | 16 50 | )}` 51 | : null; 52 | }; 53 | 54 | export const getRgbValues = (color: string) => { 55 | if (color.indexOf("#")) { 56 | return hexToRgb(color); 57 | } 58 | if (color.indexOf("rgba")) { 59 | return color 60 | .substring(5, color.length - 1) 61 | .replace(/ /g, "") 62 | .split(","); 63 | } 64 | if (color.indexOf("rgb")) { 65 | return color 66 | .substring(4, color.length - 1) 67 | .replace(/ /g, "") 68 | .split(","); 69 | } 70 | }; 71 | 72 | export const usNumberFormat = (value: number | string): string => 73 | String(value) 74 | .split("") 75 | .reverse() 76 | .join("") 77 | .replace(/(\d{3})/g, "$1,") 78 | .replace(/\,$/, "") 79 | .split("") 80 | .reverse() 81 | .join(""); 82 | 83 | export function isHCThemeApplied(chart: any): boolean { 84 | return ( 85 | typeof chart.data.datasets[0].backgroundColor !== "string" || 86 | chart.data.datasets[0].borderDash 87 | ); 88 | } 89 | 90 | export function showTooltipOnKayboard({ 91 | chart, 92 | setIndex, 93 | selectedPointIndex, 94 | }: { 95 | chart: Chart; 96 | setIndex: number; 97 | selectedPointIndex: number; 98 | }) { 99 | const { type, data, options } = chart.config; 100 | const { datasets } = data as any; 101 | if ( 102 | type === ChartTypes.Line || 103 | !(chart.config.options?.scales as any).yAxes[0].stacked 104 | ) { 105 | const duplicates: number[] = []; 106 | const segments: any[] = []; 107 | const fakeSet = new LineDataSetHCStyle({} as any); 108 | datasets!.filter((dataset: ChartDataSets, i: number) => { 109 | if ( 110 | dataset!.data![selectedPointIndex] === 111 | data!.datasets![setIndex].data![selectedPointIndex] 112 | ) { 113 | duplicates.push(i); 114 | } 115 | if ((options as IChartOptions).highContrastMode) { 116 | datasets[i].borderColor = fakeSet.borderColor; 117 | datasets[i].borderWidth = fakeSet.borderWidth; 118 | if (typeof datasets[i].backgroundColor !== "string") { 119 | datasets[i].backgroundColor = buildPattern({ 120 | backgroundColor: HighContrastColors.Background, 121 | patternColor: fakeSet.borderColor, 122 | ...datasets[i].pattern, 123 | }); 124 | } 125 | } 126 | }); 127 | duplicates.forEach((segmentId) => { 128 | segments.push(chart.getDatasetMeta(segmentId).data[selectedPointIndex]); 129 | if ((options as IChartOptions).highContrastMode) { 130 | datasets[segmentId].borderColor = fakeSet.hoverBorderColor; 131 | datasets[segmentId].borderWidth = fakeSet.hoverBorderWidth; 132 | if (typeof datasets[segmentId].backgroundColor !== "string") { 133 | datasets[segmentId].backgroundColor = buildPattern({ 134 | backgroundColor: HighContrastColors.Background, 135 | patternColor: fakeSet.hoverBorderColor, 136 | ...datasets[segmentId].pattern, 137 | }); 138 | } 139 | } 140 | }); 141 | if ((options as IChartOptions).highContrastMode) { 142 | chart.update(); 143 | } 144 | (chart as any).tooltip._active = segments; 145 | } else { 146 | const fakeSet = new LineStackedDataSetHCStyle({} as any); 147 | const segment = chart.getDatasetMeta(setIndex).data[selectedPointIndex]; 148 | (chart as any).tooltip._active = [segment]; 149 | if ((options as IChartOptions).highContrastMode) { 150 | datasets.map((dataset: any, i: number) => { 151 | if (dataset.pattern) { 152 | dataset.borderColor = fakeSet.borderColor; 153 | dataset.borderWidth = fakeSet.borderWidth; 154 | dataset.backgroundColor = buildPattern({ 155 | backgroundColor: HighContrastColors.Background, 156 | patternColor: fakeSet.borderColor, 157 | ...dataset.pattern, 158 | }); 159 | } 160 | }); 161 | datasets[setIndex].borderColor = fakeSet.hoverBorderColor; 162 | datasets[setIndex].borderWidth = fakeSet.hoverBorderWidth; 163 | datasets[setIndex].backgroundColor = datasets[ 164 | setIndex 165 | ].backgroundColor = buildPattern({ 166 | backgroundColor: HighContrastColors.Background, 167 | patternColor: fakeSet.hoverBorderColor, 168 | ...datasets[setIndex].pattern, 169 | }); 170 | } 171 | } 172 | 173 | chart.update(); 174 | (chart as any).tooltip.update(); 175 | (chart as any).draw(); 176 | } 177 | 178 | // export const setTooltipColorScheme = ({ 179 | // chart, 180 | // siteVariables, 181 | // chartDataPointColors, 182 | // patterns, 183 | // verticalDataAlignment, 184 | // }: { 185 | // chart: Chart; 186 | // siteVariables: SiteVariablesPrepared; 187 | // chartDataPointColors: string[]; 188 | // patterns?: IChartPatterns; 189 | // verticalDataAlignment?: boolean; 190 | // }) => { 191 | // const { colorScheme } = siteVariables; 192 | // chart.options.tooltips = { 193 | // ...chart.options.tooltips, 194 | // borderColor: colorScheme.default.borderHover, 195 | // multiKeyBackground: colorScheme.white.foreground, 196 | // titleFontColor: colorScheme.default.foreground3, 197 | // bodyFontColor: colorScheme.default.foreground3, 198 | // footerFontColor: colorScheme.default.foreground3, 199 | // // callbacks: { 200 | // // ...chart.options.tooltips?.callbacks, 201 | // // labelColor: 202 | // // patterns && theme === ChartTheme.HighContrast 203 | // // ? (tooltipItem: any) => ({ 204 | // // borderColor: "transparent", 205 | // // backgroundColor: buildPattern({ 206 | // // ...patterns(colorScheme)[ 207 | // // verticalDataAlignment 208 | // // ? tooltipItem.index 209 | // // : tooltipItem.datasetIndex 210 | // // ], 211 | // // backgroundColor: colorScheme.default.background, 212 | // // patternColor: colorScheme.default.borderHover, 213 | // // }) as any, 214 | // // }) 215 | // // : (tooltipItem: any) => ({ 216 | // // borderColor: "transparent", 217 | // // backgroundColor: 218 | // // chartDataPointColors[ 219 | // // verticalDataAlignment 220 | // // ? tooltipItem.index 221 | // // : tooltipItem.datasetIndex 222 | // // ], 223 | // // }), 224 | // // }, 225 | // }; 226 | // // if (siteVariables.theme === ChartTheme.HighContrast) { 227 | // // (chart as any).options.scales.yAxes[0].gridLines.lineWidth = 0.25; 228 | // // } else { 229 | // // (chart as any).options.scales.yAxes[0].gridLines.lineWidth = 1; 230 | // // } 231 | // }; 232 | 233 | export const tooltipConfig = () => ({ 234 | yPadding: 12, 235 | xPadding: 20, 236 | caretPadding: 10, 237 | // Tooltip Title 238 | titleFontStyle: "200", 239 | titleFontSize: 20, 240 | // Tooltip Body 241 | bodySpacing: 4, 242 | bodyFontSize: 11.5, 243 | bodyFontStyle: "400", 244 | // Tooltip Footer 245 | footerFontStyle: "300", 246 | footerFontSize: 10, 247 | 248 | callbacks: { 249 | title: (tooltipItems: any) => { 250 | const value = tooltipItems[0].yLabel; 251 | return typeof value === "number" && value > 999 252 | ? usNumberFormat(value) 253 | : value; 254 | }, 255 | label: (tooltipItem: any, data: any) => 256 | data.datasets[tooltipItem.datasetIndex].label, 257 | footer: (tooltipItems: any) => { 258 | const value = tooltipItems[0].xLabel; 259 | return typeof value === "number" && value > 999 260 | ? usNumberFormat(value) 261 | : value; 262 | }, 263 | }, 264 | }); 265 | 266 | export const deepMerge = ( 267 | target: any, 268 | source: any, 269 | isMergingArrays = true 270 | ): any => { 271 | target = ((obj) => Object.assign({}, obj))(target); 272 | 273 | const isObject = (obj: any) => obj && typeof obj === "object"; 274 | 275 | if (!isObject(target) || !isObject(source)) return source; 276 | 277 | Object.keys(source).forEach((key) => { 278 | const targetValue = target[key]; 279 | const sourceValue = source[key]; 280 | 281 | if (Array.isArray(targetValue) && Array.isArray(sourceValue)) 282 | if (isMergingArrays) { 283 | target[key] = targetValue.map((x, i) => 284 | sourceValue.length <= i 285 | ? x 286 | : deepMerge(x, sourceValue[i], isMergingArrays) 287 | ); 288 | if (sourceValue.length > targetValue.length) 289 | target[key] = target[key].concat( 290 | sourceValue.slice(targetValue.length) 291 | ); 292 | } else { 293 | target[key] = targetValue.concat(sourceValue); 294 | } 295 | else if (isObject(targetValue) && isObject(sourceValue)) 296 | target[key] = deepMerge( 297 | Object.assign({}, targetValue), 298 | sourceValue, 299 | isMergingArrays 300 | ); 301 | else target[key] = sourceValue; 302 | }); 303 | 304 | return target; 305 | }; 306 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | -------------------------------------------------------------------------------- /src/types/types.ts: -------------------------------------------------------------------------------- 1 | import Chart, { 2 | ChartDataSets, 3 | PluginServiceRegistrationOptions, 4 | PointStyle, 5 | Scriptable, 6 | } from "chart.js"; 7 | import { Moment } from "moment"; 8 | import { LineDataSetHCStyle } from "../lib/datasets"; 9 | 10 | export class Entity { 11 | constructor(fields?: any) { 12 | Object.assign(this, fields); 13 | } 14 | } 15 | 16 | export interface IChart extends Chart.ChartConfiguration { 17 | areaLabel: string; 18 | type?: ChartTypes; 19 | canvasProps?: React.HTMLProps; 20 | } 21 | 22 | export interface IChartLegend extends Chart.ChartLegendOptions { 23 | custom: boolean; 24 | } 25 | 26 | export interface IChartOptions extends Chart.ChartOptions { 27 | highContrastMode?: boolean; 28 | legend?: IChartLegend; 29 | } 30 | 31 | export interface IBubbleChartData { 32 | x: number; 33 | y: number; 34 | r: number; 35 | } 36 | 37 | export interface IChartDataSet { 38 | label: string; 39 | data: number[] | IBubbleChartData[]; 40 | hidden?: boolean; 41 | color?: string; 42 | } 43 | 44 | export enum ChartTypes { 45 | Line = "line", 46 | Bar = "bar", 47 | HorizontalBar = "horizontalBar", 48 | Pie = "pie", 49 | Doughnut = "doughnut", 50 | Bubble = "bubble", 51 | } 52 | 53 | export enum Point { 54 | Circle = "circle", 55 | Rectangle = "rect", 56 | Triangle = "triangle", 57 | RectangleRotated = "rectRot", 58 | } 59 | 60 | export enum Shapes { 61 | Square = "square", 62 | DiagonalRightLeft = "diagonalRightLeft", 63 | Grid = "grid", 64 | Diagonal = "diagonal", 65 | VerticalLine = "verticalLine", 66 | GridRightLeft = "gridRightLeft", 67 | } 68 | 69 | export interface IDraw { 70 | shape: Shapes; 71 | size: number; 72 | } 73 | 74 | export enum HighContrastColors { 75 | Foreground = "#fff", 76 | Background = "#000", 77 | Active = "#1aebff", 78 | Selected = "#ffff01", 79 | Disabled = "#3ff23f", 80 | } 81 | 82 | export type IChartPatterns = (colorScheme: any) => IDraw[]; 83 | 84 | export interface IChartData { 85 | labels: Array< 86 | string | string[] | number | number[] | Date | Date[] | Moment | Moment[] 87 | >; 88 | datasets: ChartDataSets[]; 89 | } 90 | 91 | export interface IChartDataHighContrast extends IChartData { 92 | datasets: HighContrastChartDataSets[]; 93 | } 94 | 95 | export interface ILineChartDataHighContrast extends IChartData { 96 | datasets: LineDataSetHCStyle[]; 97 | } 98 | 99 | export interface LineHighContrastChartDataSets extends ChartDataSets { 100 | borderDash: number[]; 101 | pointStyle: 102 | | PointStyle 103 | | HTMLImageElement 104 | | HTMLCanvasElement 105 | | Array 106 | | Scriptable; 107 | } 108 | 109 | export interface HighContrastChartDataSets extends ChartDataSets { 110 | pattern: IDraw | IDraw[]; 111 | } 112 | 113 | export interface IChartConfig { 114 | type?: ChartTypes; 115 | data: IChartData; 116 | areaLabel: string; 117 | options?: IChartOptions; 118 | plugins?: PluginServiceRegistrationOptions[]; 119 | highContrastMode?: boolean; 120 | } 121 | 122 | export interface IPreSetupConfig { 123 | areaLabel: string; 124 | data: IChartData; 125 | options?: IChartOptions; 126 | plugins?: PluginServiceRegistrationOptions[]; 127 | } 128 | 129 | export interface IPreSetupConfigHighContrast extends IPreSetupConfig { 130 | data: IChartDataHighContrast; 131 | } 132 | export interface ILinePreSetupConfigHighContrast extends IPreSetupConfig { 133 | data: ILineChartDataHighContrast; 134 | } 135 | -------------------------------------------------------------------------------- /stories/area.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Chart } from "../src/chart"; 3 | import { IChart, Point } from "../src/types"; 4 | import { AreaChart, AreaChartHighContrast } from "../src/lib/builder"; 5 | import { customOptions } from "./utils"; 6 | import { Container, DarkContainer, HighContrastContainer } from "./components"; 7 | 8 | export default { 9 | title: "Charts/Area", 10 | component: Chart, 11 | }; 12 | 13 | const datasets = [ 14 | { 15 | label: "Laptops", 16 | data: [1860, 7700, 4100, 3012, 2930], 17 | color: "#6264A7", 18 | }, 19 | { 20 | label: "Watches", 21 | data: [200, 3600, 480, 5049, 4596], 22 | color: "#C8C6C4", 23 | }, 24 | ]; 25 | 26 | export const Default = () => { 27 | const config: IChart = new AreaChart({ 28 | areaLabel: "Line chart sample", 29 | data: { 30 | labels: ["Jan", "Feb", "March", "April", "May"], 31 | datasets, 32 | }, 33 | }); 34 | return ( 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | const datasetsHighContrast = [ 42 | { 43 | label: "Tablets", 44 | data: [860, 6700, 3100, 2012, 1930], 45 | borderDash: [], 46 | pointStyle: Point.Circle, 47 | }, 48 | { 49 | label: "Phones", 50 | data: [100, 1600, 180, 3049, 3596], 51 | borderDash: [], 52 | pointStyle: Point.Rectangle, 53 | }, 54 | ]; 55 | 56 | export const HighContrast = () => { 57 | const config: IChart = new AreaChartHighContrast({ 58 | areaLabel: "Line chart sample", 59 | data: { 60 | labels: ["Jan", "Feb", "March", "April", "May"], 61 | datasets: datasetsHighContrast, 62 | }, 63 | }); 64 | return ( 65 | 66 | 67 | 68 | ); 69 | }; 70 | 71 | const datasetsCustomTheme = [ 72 | { 73 | label: "Tablets", 74 | data: [860, 6700, 3100, 2012, 1930], 75 | color: "rgb(255, 99, 132)", 76 | }, 77 | { 78 | label: "Phones", 79 | data: [100, 1600, 180, 3049, 3596], 80 | color: "rgb(255, 159, 64)", 81 | }, 82 | ]; 83 | 84 | export const CustomTheme = () => { 85 | const config: IChart = new AreaChart({ 86 | areaLabel: "Line chart sample", 87 | data: { 88 | labels: ["Jan", "Feb", "March", "April", "May"], 89 | datasets: datasetsCustomTheme, 90 | }, 91 | options: customOptions, 92 | }); 93 | return ( 94 | 95 | 96 | 97 | ); 98 | }; 99 | -------------------------------------------------------------------------------- /stories/bar.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Chart } from "../src/chart"; 3 | import { IChart } from "../src/types"; 4 | import { Patterns } from "../src/lib/patterns"; 5 | import { BarChart, BarChartHighContrast } from "../src/lib/builder"; 6 | import { Container, DarkContainer, HighContrastContainer } from "./components"; 7 | import { customOptions } from "./utils"; 8 | 9 | export default { 10 | title: "Charts/Bar", 11 | component: Chart, 12 | }; 13 | 14 | const datasets = [ 15 | { 16 | label: "Tablets", 17 | data: [860, 6700, 3100, 2012, 1930], 18 | color: "#6264A7", 19 | }, 20 | ]; 21 | 22 | export const Default = () => { 23 | const config: IChart = new BarChart({ 24 | areaLabel: "Bar chart sample", 25 | data: { 26 | labels: ["Jan", "Feb", "March", "April", "May"], 27 | datasets, 28 | }, 29 | }); 30 | return ( 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | const datasetsHighContrast = [ 38 | { 39 | label: "Tablets", 40 | data: [860, 6700, 3100, 2012, 1930], 41 | pattern: Patterns.Diagonal, 42 | }, 43 | ]; 44 | 45 | export const HighContrast = () => { 46 | const config: IChart = new BarChartHighContrast({ 47 | areaLabel: "Bar chart sample", 48 | data: { 49 | labels: ["Jan", "Feb", "March", "April", "May"], 50 | datasets: datasetsHighContrast, 51 | }, 52 | }); 53 | return ( 54 | 55 | 56 | 57 | ); 58 | }; 59 | 60 | const datasetsCustomTheme = [ 61 | { 62 | label: "Tablets", 63 | data: [860, 6700, 3100, 2012, 1930], 64 | color: "rgb(255, 99, 132)", 65 | }, 66 | ]; 67 | 68 | export const CustomTheme = () => { 69 | const config: IChart = new BarChart({ 70 | areaLabel: "Bar chart sample", 71 | data: { 72 | labels: ["Jan", "Feb", "March", "April", "May"], 73 | datasets: datasetsCustomTheme, 74 | }, 75 | options: customOptions, 76 | }); 77 | return ( 78 | 79 | 80 | 81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /stories/components/container.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const Container = (props) => ( 4 |
19 |
35 | {props.children} 36 |
37 |
38 | ); 39 | 40 | export const DarkContainer = (props) => ( 41 |
57 |
73 | {props.children} 74 |
75 |
76 | ); 77 | 78 | export const HighContrastContainer = (props) => ( 79 |
89 | 114 |
{props.children}
115 |
116 | ); 117 | 118 | export const TrendLineContainer = (props) => ( 119 |
130 |
146 | {props.children} 147 |
148 |
149 | ); 150 | 151 | export const TrendLineHighContrastContainer = (props) => ( 152 |
169 | 194 |
{props.children}
195 |
196 | ); 197 | 198 | export const TrendLineDarkContainer = (props) => ( 199 |
216 |
232 | {props.children} 233 |
234 |
235 | ); 236 | -------------------------------------------------------------------------------- /stories/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./container"; 2 | -------------------------------------------------------------------------------- /stories/docs/introduction.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from "@storybook/addon-docs/blocks"; 2 | import { Chart } from "../../src/chart"; 3 | 4 | 5 | 6 | # `@fluentui/react-charts` 7 | 8 | # Introduction 9 | 10 | ## Installation 11 | 12 | You can get the latest version of `@fluentui/react-charts` from [npm](https://npmjs.com/package/@fluentui/react-charts), the [GitHub releases](https://github.com/OfficeDev/microsoft-data-visualization-library). 13 | 14 | ## Creating a Chart 15 | 16 | It's easy to get started with `@fluentui/react-charts`. 17 | 18 | In this React component example, we create a line chart for a single dataset and render that in our page. 19 | 20 | ```tsx 21 | 36 | ``` 37 | 38 | ## Installation 39 | 40 | ```sh 41 | npm install @fluentui/react-charts 42 | ``` 43 | 44 | ## Github 45 | 46 | You can download the latest version of [@fluentui/react-charts on GitHub](https://github.com/OfficeDev/microsoft-data-visualization-library/releases/latest). 47 | -------------------------------------------------------------------------------- /stories/docs/line.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta, Story, Canvas } from "@storybook/addon-docs/blocks"; 2 | import { IChart } from "../../src"; 3 | import { Chart } from "../../src/chart"; 4 | import { Point } from "../../src/types"; 5 | import { LineChart, LineChartHighContrast } from "../../src/lib/builder"; 6 | import { Container, DarkContainer, HighContrastContainer } from "../components"; 7 | import { customOptions } from "../utils"; 8 | 9 | 10 | 11 | # Line 12 | 13 | A line chart is a way of plotting data points on a line. Often, it is used to show trend data, or the comparison of two data sets. 14 | 15 | ## Dataset Properties 16 | 17 | The line chart allows a number of properties to be specified for each dataset. These are used to set display properties for a specific dataset. For example, the colour of a line is generally set this way. 18 | 19 | `color` property will assing color value to all following dataset properties: `borderColor`, `hoverBorderColor`, `pointBorderColor`, `pointBackgroundColor`, `pointHoverBackgroundColor`, `pointHoverBorderColor` or you could reassign it manually. 20 | 21 | | Name | Type | [Scriptable](../general/options.md#scriptable-options) | [Indexable](../general/options.md#indexable-options) | Default | 22 | | --------------------------------------------------- | -------------------------------------------- | :----------------------------------------------------: | :--------------------------------------------------: | ---------------------- | 23 | | [`backgroundColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `'rgba(0, 0, 0, 0.1)'` | 24 | | [`borderCapStyle`](#line-styling) | `string` | Yes | - | `'butt'` | 25 | | [`borderColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `'rgba(0, 0, 0, 0.1)'` | 26 | | [`borderDash`](#line-styling) | `number[]` | Yes | - | `[]` | 27 | | [`borderDashOffset`](#line-styling) | `number` | Yes | - | `0.0` | 28 | | [`borderJoinStyle`](#line-styling) | `string` | Yes | - | `'miter'` | 29 | | [`borderWidth`](#line-styling) | `number` | Yes | - | `3` | 30 | | [`clip`](#general) | `number`\|`object` | - | - | `undefined` | 31 | | [`data`](#data-structure) | `object`\|`object[]`\|`number[]`\|`string[]` | - | - | **required** | 32 | | [`cubicInterpolationMode`](#cubicinterpolationmode) | `string` | Yes | - | `'default'` | 33 | | [`fill`](#line-styling) | `boolean`\|`string` | Yes | - | `false` | 34 | | [`hoverBackgroundColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `undefined` | 35 | | [`hoverBorderCapStyle`](#line-styling) | `string` | Yes | - | `undefined` | 36 | | [`hoverBorderColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `undefined` | 37 | | [`hoverBorderDash`](#line-styling) | `number[]` | Yes | - | `undefined` | 38 | | [`hoverBorderDashOffset`](#line-styling) | `number` | Yes | - | `undefined` | 39 | | [`hoverBorderJoinStyle`](#line-styling) | `string` | Yes | - | `undefined` | 40 | | [`hoverBorderWidth`](#line-styling) | `number` | Yes | - | `undefined` | 41 | | [`indexAxis`](#general) | `string` | - | - | `'x'` | 42 | | [`label`](#general) | `string` | - | - | `''` | 43 | | [`tension`](#line-styling) | `number` | - | - | `0` | 44 | | [`order`](#general) | `number` | - | - | `0` | 45 | | [`pointBackgroundColor`](#point-styling) | `Color` | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` | 46 | | [`pointBorderColor`](#point-styling) | `Color` | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` | 47 | | [`pointBorderWidth`](#point-styling) | `number` | Yes | Yes | `1` | 48 | | [`pointHitRadius`](#point-styling) | `number` | Yes | Yes | `1` | 49 | | [`pointHoverBackgroundColor`](#interactions) | `Color` | Yes | Yes | `undefined` | 50 | | [`pointHoverBorderColor`](#interactions) | `Color` | Yes | Yes | `undefined` | 51 | | [`pointHoverBorderWidth`](#interactions) | `number` | Yes | Yes | `1` | 52 | | [`pointHoverRadius`](#interactions) | `number` | Yes | Yes | `4` | 53 | | [`pointRadius`](#point-styling) | `number` | Yes | Yes | `3` | 54 | | [`pointRotation`](#point-styling) | `number` | Yes | Yes | `0` | 55 | | [`pointStyle`](#point-styling) | `string`\|`Image` | Yes | Yes | `'circle'` | 56 | | [`showLine`](#line-styling) | `boolean` | - | - | `true` | 57 | | [`spanGaps`](#line-styling) | `boolean`\|`number` | - | - | `undefined` | 58 | | [`stepped`](#stepped) | `boolean`\|`string` | - | - | `false` | 59 | | [`xAxisID`](#general) | `string` | - | - | first x axis | 60 | | [`yAxisID`](#general) | `string` | - | - | first y axis | 61 | 62 | ### General 63 | 64 | | Name | Description | 65 | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 66 | | `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` | 67 | | `indexAxis` | The base axis of the dataset. `'x'` for horizontal lines and `'y'` for vertical lines. | 68 | | `label` | The label for the dataset which appears in the legend and tooltips. | 69 | | `order` | The drawing order of dataset. Also affects order for stacking, tooltip, and legend. | 70 | | `xAxisID` | The ID of the x-axis to plot this dataset on. | 71 | | `yAxisID` | The ID of the y-axis to plot this dataset on. | 72 | 73 | ### Point Styling 74 | 75 | The style of each point can be controlled with the following properties: 76 | 77 | | Name | Description | 78 | | ---------------------- | ------------------------------------------------------------------------ | 79 | | `pointBackgroundColor` | The fill color for points. | 80 | | `pointBorderColor` | The border color for points. | 81 | | `pointBorderWidth` | The width of the point border in pixels. | 82 | | `pointHitRadius` | The pixel size of the non-displayed point that reacts to mouse events. | 83 | | `pointRadius` | The radius of the point shape. If set to 0, the point is not rendered. | 84 | | `pointRotation` | The rotation of the point in degrees. | 85 | | `pointStyle` | Style of the point. [more...](../configuration/elements.md#point-styles) | 86 | 87 | All these values, if `undefined`, fallback first to the dataset options then to the associated [`elements.point.*`](../configuration/elements.md#point-configuration) options. 88 | 89 | ### Line Styling 90 | 91 | The style of the line can be controlled with the following properties: 92 | 93 | | Name | Description | 94 | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 95 | | `backgroundColor` | The line fill color. | 96 | | `borderCapStyle` | Cap style of the line. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap). | 97 | | `borderColor` | The line color. | 98 | | `borderDash` | Length and spacing of dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). | 99 | | `borderDashOffset` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). | 100 | | `borderJoinStyle` | Line joint style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). | 101 | | `borderWidth` | The line width (in pixels). | 102 | | `fill` | How to fill the area under the line. See [area charts](area.md). | 103 | | `tension` | Bezier curve tension of the line. Set to 0 to draw straightlines. This option is ignored if monotone cubic interpolation is used. | 104 | | `showLine` | If false, the line is not drawn for this dataset. | 105 | | `spanGaps` | If true, lines will be drawn between points with no or null data. If false, points with `null` data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. | 106 | 107 | If the value is `undefined`, `showLine` and `spanGaps` fallback to the associated [chart configuration options](#configuration-options). The rest of the values fallback to the associated [`elements.line.*`](../configuration/elements.md#line-configuration) options. 108 | 109 | ### Interactions 110 | 111 | The interaction with each point can be controlled with the following properties: 112 | 113 | | Name | Description | 114 | | --------------------------- | ------------------------------------- | 115 | | `pointHoverBackgroundColor` | Point background color when hovered. | 116 | | `pointHoverBorderColor` | Point border color when hovered. | 117 | | `pointHoverBorderWidth` | Border width of point when hovered. | 118 | | `pointHoverRadius` | The radius of the point when hovered. | 119 | 120 | ### cubicInterpolationMode 121 | 122 | The following interpolation modes are supported. 123 | 124 | - `'default'` 125 | - `'monotone'` 126 | 127 | The `'default'` algorithm uses a custom weighted cubic interpolation, which produces pleasant curves for all types of datasets. 128 | 129 | The `'monotone'` algorithm is more suited to `y = f(x)` datasets: it preserves monotonicity (or piecewise monotonicity) of the dataset being interpolated, and ensures local extremums (if any) stay at input data points. 130 | 131 | If left untouched (`undefined`), the global `options.elements.line.cubicInterpolationMode` property is used. 132 | 133 | ### Stepped 134 | 135 | The following values are supported for `stepped`. 136 | 137 | - `false`: No Step Interpolation (default) 138 | - `true`: Step-before Interpolation (eq. `'before'`) 139 | - `'before'`: Step-before Interpolation 140 | - `'after'`: Step-after Interpolation 141 | - `'middle'`: Step-middle Interpolation 142 | 143 | If the `stepped` value is set to anything other than false, `tension` will be ignored. 144 | 145 | ## Configuration Options 146 | 147 | The line chart defines the following configuration options. These options are looked up on access, and form together with the global chart configuration, `Chart.defaults`, the options of the chart. 148 | 149 | | Name | Type | Default | Description | 150 | | ---------- | ------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 151 | | `showLine` | `boolean` | `true` | If false, the lines between points are not drawn. | 152 | | `spanGaps` | `boolean`\|`number` | `false` | If true, lines will be drawn between points with no or null data. If false, points with `null` data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. | 153 | 154 | ## Default Options 155 | 156 | It is common to want to apply a configuration setting to all created line charts. The global line chart settings are stored in `Chart.overrides.line`. Changing the global options only affects charts created after the change. Existing charts are not changed. 157 | 158 | For example, to configure all line charts with `spanGaps = true` you would do: 159 | 160 | ```javascript 161 | Chart.overrides.line.spanGaps = true; 162 | ``` 163 | 164 | ## Data Structure 165 | 166 | All of the supported [data structures](../general/data-structures.md) can be used with line charts. 167 | 168 | ## Customization example 169 | 170 | ```ts 171 | const dataset = [ 172 | { 173 | label: "Tablets", 174 | data: [860, 6700, 3100, 2012, 1930], 175 | color: "rgb(255, 99, 132)", 176 | }, 177 | { 178 | label: "Phones", 179 | data: [100, 1600, 180, 3049, 3596], 180 | color: "rgb(255, 159, 64)", 181 | }, 182 | { 183 | label: "Laptops", 184 | data: [1860, 7700, 4100, 3012, 2930], 185 | color: "rgb(255, 205, 86)", 186 | }, 187 | { 188 | label: "Watches", 189 | data: [200, 3600, 480, 5049, 4596], 190 | color: "rgb(75, 192, 192)", 191 | }, 192 | { 193 | label: "TVs", 194 | data: [960, 8700, 5100, 5012, 3930], 195 | color: "rgb(54, 162, 235)", 196 | }, 197 | { 198 | label: "Displays", 199 | data: [1000, 4600, 480, 4049, 3596], 200 | color: "rgb(153, 102, 255)", 201 | }, 202 | ]; 203 | 204 | const config = new LineChart({ 205 | areaLabel: "Line chart sample", 206 | data: { 207 | labels: ["Jan", "Feb", "March", "April", "May"], 208 | datasets, 209 | }, 210 | }); 211 | 212 | ; 213 | ``` 214 | 215 | export const datasetsCustomTheme = [ 216 | { 217 | label: "Tablets", 218 | data: [860, 6700, 3100, 2012, 1930], 219 | color: "rgb(255, 99, 132)", 220 | }, 221 | { 222 | label: "Phones", 223 | data: [100, 1600, 180, 3049, 3596], 224 | color: "rgb(255, 159, 64)", 225 | }, 226 | { 227 | label: "Laptops", 228 | data: [1860, 7700, 4100, 3012, 2930], 229 | color: "rgb(255, 205, 86)", 230 | }, 231 | { 232 | label: "Watches", 233 | data: [200, 3600, 480, 5049, 4596], 234 | color: "rgb(75, 192, 192)", 235 | }, 236 | { 237 | label: "TVs", 238 | data: [960, 8700, 5100, 5012, 3930], 239 | color: "rgb(54, 162, 235)", 240 | }, 241 | { 242 | label: "Displays", 243 | data: [1000, 4600, 480, 4049, 3596], 244 | color: "rgb(153, 102, 255)", 245 | }, 246 | ]; 247 | 248 | export const CustomTheme = () => { 249 | const config = new LineChart({ 250 | areaLabel: "Line chart sample", 251 | data: { 252 | labels: ["Jan", "Feb", "March", "April", "May"], 253 | datasets: datasetsCustomTheme, 254 | }, 255 | options: customOptions, 256 | }); 257 | return ( 258 | 259 | 260 | 261 | ); 262 | }; 263 | 264 | 265 | 266 | 267 | 277 | 278 | 279 | 280 | -------------------------------------------------------------------------------- /stories/doughnut.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Chart } from "../src/chart"; 3 | import { IChart } from "../src/types"; 4 | import { Patterns } from "../src/lib/patterns"; 5 | import { DoughnutChart, DoughnutChartHighContrast } from "../src/lib/builder"; 6 | import { Container, DarkContainer, HighContrastContainer } from "./components"; 7 | import { customPieOptions } from "./utils"; 8 | 9 | export default { 10 | title: "Charts/Doughnut", 11 | component: Chart, 12 | }; 13 | 14 | const datasets = [ 15 | { 16 | label: "Sales", 17 | data: [2004, 1600, 480, 504, 1000], 18 | color: ["#6264A7", "#C8C6C4", "#BDBDE6", "#605E5C", "#464775", "#252423"], 19 | }, 20 | ]; 21 | 22 | export const Default = () => { 23 | const config: IChart = new DoughnutChart({ 24 | areaLabel: "Doughnut chart sample", 25 | data: { 26 | labels: ["Laptops", "Tablets", "Phones", "Displays", "Watches"], 27 | datasets, 28 | }, 29 | }); 30 | return ( 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | const datasetsHighContrast = [ 38 | { 39 | label: "Tablets", 40 | data: [860, 6700, 3100, 2012, 1930], 41 | pattern: [ 42 | Patterns.Diagonal, 43 | Patterns.Square, 44 | Patterns.Grid, 45 | Patterns.Grid2, 46 | Patterns.Line, 47 | ], 48 | }, 49 | ]; 50 | 51 | export const HighContrast = () => { 52 | const config: IChart = new DoughnutChartHighContrast({ 53 | areaLabel: "Pie chart sample", 54 | data: { 55 | labels: ["Laptops", "Tablets", "Phones", "Displays", "Watches"], 56 | datasets: datasetsHighContrast, 57 | }, 58 | }); 59 | return ( 60 | 61 | 62 | 63 | ); 64 | }; 65 | 66 | const datasetsCustomTheme = [ 67 | { 68 | label: "Tablets", 69 | data: [860, 6700, 3100, 2012, 1930], 70 | color: [ 71 | "rgb(255, 99, 132)", 72 | "rgb(255, 159, 64)", 73 | "rgb(255, 205, 86)", 74 | "rgb(75, 192, 192)", 75 | "rgb(54, 162, 235)", 76 | ], 77 | }, 78 | ]; 79 | 80 | export const CustomTheme = () => { 81 | const config: IChart = new DoughnutChart({ 82 | areaLabel: "Pie chart sample", 83 | data: { 84 | labels: ["Laptops", "Tablets", "Phones", "Displays", "Watches"], 85 | datasets: datasetsCustomTheme, 86 | }, 87 | options: customPieOptions, 88 | }); 89 | return ( 90 | 91 | 92 | 93 | ); 94 | }; 95 | -------------------------------------------------------------------------------- /stories/grouped-bar.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Chart } from "../src/chart"; 3 | import { IChart } from "../src/types"; 4 | import { Patterns } from "../src/lib/patterns"; 5 | import { 6 | GroupedBarChartHighContrast, 7 | GroupedBarChart, 8 | } from "../src/lib/builder"; 9 | import { Container, DarkContainer, HighContrastContainer } from "./components"; 10 | import { customOptions } from "./utils"; 11 | 12 | export default { 13 | title: "Charts/Grouped bar", 14 | component: Chart, 15 | }; 16 | 17 | const datasets = [ 18 | { 19 | label: "Tablets", 20 | data: [860, 6700, 3100, 2012, 1930], 21 | color: "#6264A7", 22 | }, 23 | { 24 | label: "Phones", 25 | data: [100, 1600, 180, 3049, 3596], 26 | color: "#C8C6C4", 27 | }, 28 | { 29 | label: "Laptops", 30 | data: [1860, 7700, 4100, 3012, 2930], 31 | color: "#BDBDE6", 32 | }, 33 | { 34 | label: "Watches", 35 | data: [200, 3600, 480, 5049, 4596], 36 | color: "#605E5C", 37 | }, 38 | ]; 39 | 40 | export const Default = () => { 41 | const config: IChart = new GroupedBarChart({ 42 | areaLabel: "Grouped bar chart sample", 43 | data: { 44 | labels: ["Jan", "Feb", "March", "April", "May"], 45 | datasets, 46 | }, 47 | }); 48 | return ( 49 | 50 | 51 | 52 | ); 53 | }; 54 | 55 | const datasetsHighContrast = [ 56 | { 57 | label: "Tablets", 58 | data: [860, 6700, 3100, 2012, 1930], 59 | pattern: Patterns.Diagonal, 60 | }, 61 | { 62 | label: "Phones", 63 | data: [100, 1600, 180, 3049, 3596], 64 | pattern: Patterns.Square, 65 | }, 66 | { 67 | label: "Laptops", 68 | data: [1860, 7700, 4100, 3012, 2930], 69 | pattern: Patterns.Grid2, 70 | }, 71 | { 72 | label: "Watches", 73 | data: [200, 3600, 480, 5049, 4596], 74 | pattern: Patterns.Grid, 75 | }, 76 | ]; 77 | 78 | export const HighContrast = () => { 79 | const config: IChart = new GroupedBarChartHighContrast({ 80 | areaLabel: "Grouped bar chart sample", 81 | data: { 82 | labels: ["Jan", "Feb", "March", "April", "May"], 83 | datasets: datasetsHighContrast, 84 | }, 85 | }); 86 | return ( 87 | 88 | 89 | 90 | ); 91 | }; 92 | 93 | const datasetsCustomTheme = [ 94 | { 95 | label: "Tablets", 96 | data: [860, 6700, 3100, 2012, 1930], 97 | color: "rgb(255, 99, 132)", 98 | }, 99 | { 100 | label: "Phones", 101 | data: [100, 1600, 180, 3049, 3596], 102 | color: "rgb(255, 159, 64)", 103 | }, 104 | { 105 | label: "Laptops", 106 | data: [1860, 7700, 4100, 3012, 2930], 107 | color: "rgb(255, 205, 86)", 108 | }, 109 | { 110 | label: "Watches", 111 | data: [200, 3600, 480, 5049, 4596], 112 | color: "rgb(75, 192, 192)", 113 | }, 114 | ]; 115 | 116 | export const CustomTheme = () => { 117 | const config: IChart = new GroupedBarChart({ 118 | areaLabel: "Grouped bar chart sample", 119 | data: { 120 | labels: ["Jan", "Feb", "March", "April", "May"], 121 | datasets: datasetsCustomTheme, 122 | }, 123 | options: customOptions, 124 | }); 125 | return ( 126 | 127 | 128 | 129 | ); 130 | }; 131 | -------------------------------------------------------------------------------- /stories/horizontal-bar.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Chart } from "../src/chart"; 3 | import { IChart } from "../src/types"; 4 | import { Patterns } from "../src/lib/patterns"; 5 | import { 6 | BarChart, 7 | BarChartHighContrast, 8 | HorizontalBarChart, 9 | HorizontalBarChartHighContrast, 10 | } from "../src/lib/builder"; 11 | import { Container, DarkContainer, HighContrastContainer } from "./components"; 12 | import { customOptions } from "./utils"; 13 | 14 | export default { 15 | title: "Charts/Horizontal bar", 16 | component: Chart, 17 | }; 18 | 19 | const datasets = [ 20 | { 21 | label: "Sales", 22 | data: [860, 6700, 3100, 2012, 1930], 23 | color: "#6264A7", 24 | }, 25 | ]; 26 | 27 | export const Default = () => { 28 | const config: IChart = new HorizontalBarChart({ 29 | areaLabel: "Horizontal bar chart sample", 30 | data: { 31 | labels: ["Jan", "Feb", "March", "April", "May"], 32 | datasets, 33 | }, 34 | }); 35 | return ( 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | const datasetsHighContrast = [ 43 | { 44 | label: "Sales", 45 | data: [860, 6700, 3100, 2012, 1930], 46 | pattern: Patterns.Diagonal, 47 | }, 48 | ]; 49 | 50 | export const HighContrast = () => { 51 | const config: IChart = new HorizontalBarChartHighContrast({ 52 | areaLabel: "Horizontal bar chart sample", 53 | data: { 54 | labels: ["Jan", "Feb", "March", "April", "May"], 55 | datasets: datasetsHighContrast, 56 | }, 57 | }); 58 | return ( 59 | 60 | 61 | 62 | ); 63 | }; 64 | 65 | const datasetsCustomTheme = [ 66 | { 67 | label: "Sales", 68 | data: [860, 6700, 3100, 2012, 1930], 69 | color: "rgb(255, 99, 132)", 70 | }, 71 | ]; 72 | 73 | export const CustomTheme = () => { 74 | const config: IChart = new HorizontalBarChart({ 75 | areaLabel: "Horizontal bar chart sample", 76 | data: { 77 | labels: ["Jan", "Feb", "March", "April", "May"], 78 | datasets: datasetsCustomTheme, 79 | }, 80 | }); 81 | return ( 82 | 83 | 84 | 85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /stories/line.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Chart } from "../src/chart"; 3 | import { IChart, Point } from "../src/types"; 4 | import { LineChart, LineChartHighContrast } from "../src/lib/builder"; 5 | import { Container, DarkContainer, HighContrastContainer } from "./components"; 6 | import { customOptions } from "./utils"; 7 | 8 | export default { 9 | title: "Charts/Line", 10 | component: Chart, 11 | // parameters: { 12 | // docs: { 13 | // source: { 14 | // code: "Some custom string here", 15 | // }, 16 | // }, 17 | // }, 18 | }; 19 | 20 | const datasets = [ 21 | { 22 | label: "Tablets", 23 | data: [860, 6700, 3100, 2012, 1930], 24 | color: "#6264A7", 25 | }, 26 | { 27 | label: "Phones", 28 | data: [100, 1600, 180, 3049, 3596], 29 | color: "#C8C6C4", 30 | }, 31 | { 32 | label: "Laptops", 33 | data: [1860, 7700, 4100, 3012, 2930], 34 | color: "#BDBDE6", 35 | }, 36 | { 37 | label: "Watches", 38 | data: [200, 3600, 480, 5049, 4596], 39 | color: "#605E5C", 40 | }, 41 | { 42 | label: "TVs", 43 | data: [960, 8700, 5100, 5012, 3930], 44 | color: "#464775", 45 | }, 46 | { 47 | label: "Displays", 48 | data: [1000, 4600, 480, 4049, 3596], 49 | color: "#252423", 50 | }, 51 | ]; 52 | 53 | export const Default = () => { 54 | const config: IChart = new LineChart({ 55 | areaLabel: "Line chart sample", 56 | data: { 57 | labels: ["Jan", "Feb", "March", "April", "May"], 58 | datasets, 59 | }, 60 | }); 61 | return ( 62 | 63 | 64 | 65 | ); 66 | }; 67 | 68 | const datasetsHighContrast = [ 69 | { 70 | label: "Tablets", 71 | data: [860, 6700, 3100, 2012, 1930], 72 | borderDash: [], 73 | pointStyle: Point.Circle, 74 | }, 75 | { 76 | label: "Phones", 77 | data: [100, 1600, 180, 3049, 3596], 78 | borderDash: [], 79 | pointStyle: Point.Rectangle, 80 | }, 81 | { 82 | label: "Laptops", 83 | data: [1860, 7700, 4100, 3012, 2930], 84 | borderDash: [], 85 | pointStyle: Point.Triangle, 86 | }, 87 | { 88 | label: "Watches", 89 | data: [200, 3600, 480, 5049, 4596], 90 | borderDash: [5, 5], 91 | pointStyle: Point.Circle, 92 | }, 93 | { 94 | label: "TVs", 95 | data: [960, 8700, 5100, 5012, 3930], 96 | borderDash: [5, 5], 97 | pointStyle: Point.Rectangle, 98 | }, 99 | { 100 | label: "Displays", 101 | data: [1000, 4600, 480, 4049, 3596], 102 | borderDash: [5, 5], 103 | pointStyle: Point.Triangle, 104 | }, 105 | ]; 106 | 107 | export const HighContrast = () => { 108 | const config: IChart = new LineChartHighContrast({ 109 | areaLabel: "Line chart sample", 110 | data: { 111 | labels: ["Jan", "Feb", "March", "April", "May"], 112 | datasets: datasetsHighContrast, 113 | }, 114 | }); 115 | return ( 116 | 117 | 118 | 119 | ); 120 | }; 121 | 122 | const datasetsCustomTheme = [ 123 | { 124 | label: "Tablets", 125 | data: [860, 6700, 3100, 2012, 1930], 126 | color: "rgb(255, 99, 132)", 127 | }, 128 | { 129 | label: "Phones", 130 | data: [100, 1600, 180, 3049, 3596], 131 | color: "rgb(255, 159, 64)", 132 | }, 133 | { 134 | label: "Laptops", 135 | data: [1860, 7700, 4100, 3012, 2930], 136 | color: "rgb(255, 205, 86)", 137 | }, 138 | { 139 | label: "Watches", 140 | data: [200, 3600, 480, 5049, 4596], 141 | color: "rgb(75, 192, 192)", 142 | }, 143 | { 144 | label: "TVs", 145 | data: [960, 8700, 5100, 5012, 3930], 146 | color: "rgb(54, 162, 235)", 147 | }, 148 | { 149 | label: "Displays", 150 | data: [1000, 4600, 480, 4049, 3596], 151 | color: "rgb(153, 102, 255)", 152 | }, 153 | ]; 154 | 155 | export const CustomTheme = () => { 156 | const config: IChart = new LineChart({ 157 | areaLabel: "Line chart sample", 158 | data: { 159 | labels: ["Jan", "Feb", "March", "April", "May"], 160 | datasets: datasetsCustomTheme, 161 | }, 162 | options: customOptions, 163 | }); 164 | return ( 165 | 166 | 167 | 168 | ); 169 | }; 170 | -------------------------------------------------------------------------------- /stories/pie.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Chart } from "../src/chart"; 3 | import { IChart } from "../src/types"; 4 | import { Patterns } from "../src/lib/patterns"; 5 | import { PieChart, PieChartHighContrast } from "../src/lib/builder"; 6 | import { Container, DarkContainer, HighContrastContainer } from "./components"; 7 | import { customPieOptions } from "./utils"; 8 | 9 | export default { 10 | title: "Charts/Pie", 11 | component: Chart, 12 | }; 13 | 14 | const datasets = [ 15 | { 16 | label: "Sales", 17 | data: [2004, 1600, 480, 504, 1000], 18 | color: ["#6264A7", "#C8C6C4", "#BDBDE6", "#605E5C", "#464775", "#252423"], 19 | }, 20 | ]; 21 | 22 | export const Default = () => { 23 | const config: IChart = new PieChart({ 24 | areaLabel: "Pie chart sample", 25 | data: { 26 | labels: ["Laptops", "Tablets", "Phones", "Displays", "Watches"], 27 | datasets, 28 | }, 29 | }); 30 | return ( 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | const datasetsHighContrast = [ 38 | { 39 | label: "Tablets", 40 | data: [860, 6700, 3100, 2012, 1930], 41 | pattern: [ 42 | Patterns.Diagonal, 43 | Patterns.Square, 44 | Patterns.Grid, 45 | Patterns.Grid2, 46 | Patterns.Line, 47 | ], 48 | }, 49 | ]; 50 | 51 | export const HighContrast = () => { 52 | const config: IChart = new PieChartHighContrast({ 53 | areaLabel: "Pie chart sample", 54 | data: { 55 | labels: ["Laptops", "Tablets", "Phones", "Displays", "Watches"], 56 | datasets: datasetsHighContrast, 57 | }, 58 | }); 59 | return ( 60 | 61 | 62 | 63 | ); 64 | }; 65 | 66 | const datasetsCustomTheme = [ 67 | { 68 | label: "Tablets", 69 | data: [860, 6700, 3100, 2012, 1930], 70 | color: [ 71 | "rgb(255, 99, 132)", 72 | "rgb(255, 159, 64)", 73 | "rgb(255, 205, 86)", 74 | "rgb(75, 192, 192)", 75 | "rgb(54, 162, 235)", 76 | ], 77 | }, 78 | ]; 79 | 80 | export const CustomTheme = () => { 81 | const config: IChart = new PieChart({ 82 | areaLabel: "Pie chart sample", 83 | data: { 84 | labels: ["Laptops", "Tablets", "Phones", "Displays", "Watches"], 85 | datasets: datasetsCustomTheme, 86 | }, 87 | options: customPieOptions, 88 | }); 89 | return ( 90 | 91 | 92 | 93 | ); 94 | }; 95 | -------------------------------------------------------------------------------- /stories/stacked-bar.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Chart } from "../src/chart"; 3 | import { IChart } from "../src/types"; 4 | import { Patterns } from "../src/lib/patterns"; 5 | import { 6 | StackedBarChart, 7 | StackedBarChartHighContrast, 8 | } from "../src/lib/builder"; 9 | import { Container, DarkContainer, HighContrastContainer } from "./components"; 10 | import { customOptions } from "./utils"; 11 | 12 | export default { 13 | title: "Charts/Stacked bar", 14 | component: Chart, 15 | }; 16 | 17 | const datasets = [ 18 | { 19 | label: "Laptops", 20 | data: [1860, 7700, 4100, 3012, 2930], 21 | color: "#585A96", 22 | }, 23 | { 24 | label: "Watches", 25 | data: [1200, 3600, 2480, 5049, 4596], 26 | color: "#BDBDE6", 27 | }, 28 | { 29 | label: "Phones", 30 | data: [1000, 1600, 1800, 3049, 3596], 31 | color: "#464775", 32 | }, 33 | ]; 34 | 35 | export const Default = () => { 36 | const config: IChart = new StackedBarChart({ 37 | areaLabel: "Stacked bar chart sample", 38 | data: { 39 | labels: ["Jan", "Feb", "March", "April", "May"], 40 | datasets, 41 | }, 42 | }); 43 | return ( 44 | 45 | 46 | 47 | ); 48 | }; 49 | 50 | const datasetsHighContrast = [ 51 | { 52 | label: "Tablets", 53 | data: [860, 6700, 3100, 2012, 1930], 54 | pattern: Patterns.Diagonal, 55 | }, 56 | { 57 | label: "Phones", 58 | data: [1000, 1600, 1800, 3049, 3596], 59 | pattern: Patterns.Square, 60 | }, 61 | { 62 | label: "Laptops", 63 | data: [1860, 7700, 4100, 3012, 2930], 64 | pattern: Patterns.Grid, 65 | }, 66 | ]; 67 | 68 | export const HighContrast = () => { 69 | const config: IChart = new StackedBarChartHighContrast({ 70 | areaLabel: "Stacked bar chart sample", 71 | data: { 72 | labels: ["Jan", "Feb", "March", "April", "May"], 73 | datasets: datasetsHighContrast, 74 | }, 75 | }); 76 | return ( 77 | 78 | 79 | 80 | ); 81 | }; 82 | 83 | const datasetsCustomTheme = [ 84 | { 85 | label: "Tablets", 86 | data: [860, 6700, 3100, 2012, 1930], 87 | color: "rgb(255, 99, 132)", 88 | }, 89 | { 90 | label: "Watches", 91 | data: [1200, 3600, 2480, 5049, 4596], 92 | color: "rgb(255, 159, 64)", 93 | }, 94 | { 95 | label: "Phones", 96 | data: [1000, 1600, 1800, 3049, 3596], 97 | color: "rgb(255, 205, 86)", 98 | }, 99 | ]; 100 | 101 | export const CustomTheme = () => { 102 | const config: IChart = new StackedBarChart({ 103 | areaLabel: "Stacked bar chart sample", 104 | data: { 105 | labels: ["Jan", "Feb", "March", "April", "May"], 106 | datasets: datasetsCustomTheme, 107 | }, 108 | options: customOptions, 109 | }); 110 | return ( 111 | 112 | 113 | 114 | ); 115 | }; 116 | -------------------------------------------------------------------------------- /stories/stacked-line.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Chart } from "../src/chart"; 3 | import { IChart } from "../src/types"; 4 | import { Patterns } from "../src/lib/patterns"; 5 | import { 6 | LineStackedChart, 7 | LineStackedChartHighContrast, 8 | } from "../src/lib/builder"; 9 | import { Container, DarkContainer, HighContrastContainer } from "./components"; 10 | import { customOptions } from "./utils"; 11 | 12 | export default { 13 | title: "Charts/Stacked line", 14 | component: Chart, 15 | }; 16 | 17 | const datasets = [ 18 | { 19 | label: "Tablets", 20 | data: [860, 6700, 3100, 2012, 1930], 21 | color: "#6264A7", 22 | }, 23 | { 24 | label: "Phones", 25 | data: [100, 1600, 180, 3049, 3596], 26 | color: "#E2E2F6", 27 | }, 28 | { 29 | label: "Laptops", 30 | data: [1860, 7700, 4100, 3012, 2930], 31 | color: "#BDBDE6", 32 | }, 33 | ]; 34 | 35 | export const Default = () => { 36 | const config: IChart = new LineStackedChart({ 37 | areaLabel: "Stacked line chart sample", 38 | data: { 39 | labels: ["Jan", "Feb", "March", "April", "May"], 40 | datasets, 41 | }, 42 | }); 43 | return ( 44 | 45 | 46 | 47 | ); 48 | }; 49 | 50 | const datasetsHighContrast = [ 51 | { 52 | label: "Tablets", 53 | data: [860, 6700, 3100, 2012, 1930], 54 | pattern: Patterns.Square, 55 | }, 56 | { 57 | label: "Phones", 58 | data: [100, 1600, 180, 3049, 3596], 59 | pattern: Patterns.Diagonal, 60 | }, 61 | { 62 | label: "Laptops", 63 | data: [1860, 7700, 4100, 3012, 2930], 64 | pattern: Patterns.Grid, 65 | }, 66 | ]; 67 | 68 | export const HighContrast = () => { 69 | const config: IChart = new LineStackedChartHighContrast({ 70 | areaLabel: "Stacked line chart sample", 71 | data: { 72 | labels: ["Jan", "Feb", "March", "April", "May"], 73 | datasets: datasetsHighContrast, 74 | }, 75 | }); 76 | return ( 77 | 78 | 79 | 80 | ); 81 | }; 82 | 83 | const datasetsCustomTheme = [ 84 | { 85 | label: "Tablets", 86 | data: [860, 6700, 3100, 2012, 1930], 87 | color: "rgb(255, 99, 132)", 88 | }, 89 | { 90 | label: "Phones", 91 | data: [100, 1600, 180, 3049, 3596], 92 | color: "rgb(255, 159, 64)", 93 | }, 94 | { 95 | label: "Laptops", 96 | data: [1860, 7700, 4100, 3012, 2930], 97 | color: "rgb(255, 205, 86)", 98 | }, 99 | ]; 100 | 101 | export const CustomTheme = () => { 102 | const config: IChart = new LineStackedChart({ 103 | areaLabel: "Stacked line chart sample", 104 | data: { 105 | labels: ["Jan", "Feb", "March", "April", "May"], 106 | datasets: datasetsCustomTheme, 107 | }, 108 | options: customOptions, 109 | }); 110 | return ( 111 | 112 | 113 | 114 | ); 115 | }; 116 | -------------------------------------------------------------------------------- /stories/trend-line.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Chart } from "../src/chart"; 3 | import { IChart, IChartData, IChartDataSet, Point } from "../src/types"; 4 | import { 5 | LineChart, 6 | LineChartHighContrast, 7 | TrendLineChart, 8 | TrendLineChartHighContrast, 9 | } from "../src/lib/builder"; 10 | import { 11 | DarkContainer, 12 | HighContrastContainer, 13 | TrendLineContainer, 14 | TrendLineDarkContainer, 15 | TrendLineHighContrastContainer, 16 | } from "./components"; 17 | import { customOptions } from "./utils"; 18 | import { usNumberFormat } from "../src/lib/utils"; 19 | 20 | export default { 21 | title: "Charts/TrendLine", 22 | component: Chart, 23 | // parameters: { 24 | // docs: { 25 | // source: { 26 | // code: "Some custom string here", 27 | // }, 28 | // }, 29 | // }, 30 | }; 31 | 32 | const datasetsTablets: IChartDataSet = { 33 | label: "Tablets", 34 | data: [860, 700, 910, 1201, 1300, 1530, 1490, 1400, 1600, 1550], 35 | color: "#6264A7", 36 | }; 37 | const datasetsPhones: IChartDataSet = { 38 | label: "Phones", 39 | data: [1860, 1700, 1910, 1201, 1300, 1130, 990, 1050, 960, 950], 40 | color: "rgb(192,57,77)", 41 | }; 42 | export const Default = () => { 43 | const configTablets: IChart = new TrendLineChart({ 44 | areaLabel: "Trend line chart sample", 45 | data: { 46 | labels: [ 47 | "Jan", 48 | "Feb", 49 | "March", 50 | "April", 51 | "May", 52 | "June", 53 | "July", 54 | "August", 55 | "September", 56 | "October", 57 | ], 58 | datasets: [datasetsTablets], 59 | }, 60 | }); 61 | const configPhones: IChart = new TrendLineChart({ 62 | areaLabel: "Trend line chart sample", 63 | data: { 64 | labels: [ 65 | "Jan", 66 | "Feb", 67 | "March", 68 | "April", 69 | "May", 70 | "June", 71 | "July", 72 | "August", 73 | "September", 74 | "October", 75 | ], 76 | datasets: [datasetsPhones], 77 | }, 78 | }); 79 | return ( 80 | 81 |

Sales trends

82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 | ); 90 | }; 91 | 92 | const datasetsTabletsHC = { 93 | label: "Tablets", 94 | data: [860, 700, 910, 1201, 1300, 1530, 1490, 1400, 1600, 1550], 95 | borderDash: [], 96 | pointStyle: Point.Circle, 97 | }; 98 | const datasetsPhonesHC = { 99 | label: "Phones", 100 | data: [1860, 1700, 1910, 1201, 1300, 1130, 990, 1050, 960, 950], 101 | borderDash: [], 102 | pointStyle: Point.Circle, 103 | }; 104 | 105 | export const HighContrast = () => { 106 | const configTablets: IChart = new TrendLineChartHighContrast({ 107 | areaLabel: "Trend line chart sample", 108 | data: { 109 | labels: [ 110 | "Jan", 111 | "Feb", 112 | "March", 113 | "April", 114 | "May", 115 | "June", 116 | "July", 117 | "August", 118 | "September", 119 | "October", 120 | ], 121 | datasets: [datasetsTabletsHC], 122 | }, 123 | }); 124 | const configPhones: IChart = new TrendLineChartHighContrast({ 125 | areaLabel: "Trend line chart sample", 126 | data: { 127 | labels: [ 128 | "Jan", 129 | "Feb", 130 | "March", 131 | "April", 132 | "May", 133 | "June", 134 | "July", 135 | "August", 136 | "September", 137 | "October", 138 | ], 139 | datasets: [datasetsPhonesHC], 140 | }, 141 | }); 142 | return ( 143 | 144 |

Sales trends

145 | 146 | 147 | 148 | 149 | 150 | 151 |
152 | ); 153 | }; 154 | 155 | const datasetsTabletsCustom: IChartDataSet = { 156 | label: "Tablets", 157 | data: [860, 700, 910, 1201, 1300, 1530, 1490, 1400, 1600, 1550], 158 | color: "rgb(255, 99, 132)", 159 | }; 160 | const datasetsPhonesCustom: IChartDataSet = { 161 | label: "Phones", 162 | data: [1860, 1700, 1910, 1201, 1300, 1130, 990, 1050, 960, 950], 163 | color: "rgb(255, 159, 64)", 164 | }; 165 | 166 | export const CustomTheme = () => { 167 | const configTablets: IChart = new TrendLineChart({ 168 | areaLabel: "Trend line chart sample", 169 | data: { 170 | labels: [ 171 | "Jan", 172 | "Feb", 173 | "March", 174 | "April", 175 | "May", 176 | "June", 177 | "July", 178 | "August", 179 | "September", 180 | "October", 181 | ], 182 | datasets: [datasetsTabletsCustom], 183 | }, 184 | }); 185 | const configPhones: IChart = new TrendLineChart({ 186 | areaLabel: "Trend line chart sample", 187 | data: { 188 | labels: [ 189 | "Jan", 190 | "Feb", 191 | "March", 192 | "April", 193 | "May", 194 | "June", 195 | "July", 196 | "August", 197 | "September", 198 | "October", 199 | ], 200 | datasets: [datasetsPhonesCustom], 201 | }, 202 | }); 203 | return ( 204 | 205 |

Sales trends

206 | 207 | 208 | 209 | 210 | 211 | 212 |
213 | ); 214 | }; 215 | 216 | const TrendLineWidgetRow = ({ dataset, ...props }) => { 217 | return ( 218 |
219 | {props.children} 220 |
228 | 229 | {usNumberFormat(dataset.data[dataset.data.length - 1] as number)} 230 | 231 | {dataset.label} 232 |
233 |
234 | ); 235 | }; 236 | -------------------------------------------------------------------------------- /stories/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./utils"; 2 | -------------------------------------------------------------------------------- /stories/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export const customOptions = { 2 | defaultColor: "#605E5C", 3 | layout: { 4 | padding: { 5 | left: 0, 6 | right: 32, 7 | top: 0, 8 | bottom: 0, 9 | }, 10 | }, 11 | scales: { 12 | yAxes: [ 13 | { 14 | ticks: { 15 | fontColor: "#979593", 16 | maxTicksLimit: 8, 17 | }, 18 | gridLines: { 19 | color: "#484644", 20 | zeroLineColor: "#484644", 21 | }, 22 | scaleLabel: { 23 | display: true, 24 | labelString: "Sales", 25 | }, 26 | }, 27 | ], 28 | xAxes: [ 29 | { 30 | ticks: { 31 | fontColor: "#979593", 32 | }, 33 | gridLines: { 34 | color: "#484644", 35 | }, 36 | scaleLabel: { 37 | display: true, 38 | labelString: "Product category", 39 | }, 40 | }, 41 | ], 42 | }, 43 | }; 44 | 45 | export const customPieOptions = { 46 | defaultColor: "#605E5C", 47 | scales: { 48 | yAxes: [ 49 | { 50 | ticks: { 51 | fontColor: "#979593", 52 | maxTicksLimit: 8, 53 | }, 54 | gridLines: { 55 | color: "#484644", 56 | zeroLineColor: "#484644", 57 | }, 58 | scaleLabel: { 59 | display: true, 60 | }, 61 | }, 62 | ], 63 | xAxes: [ 64 | { 65 | ticks: { 66 | fontColor: "#979593", 67 | }, 68 | gridLines: { 69 | color: "#484644", 70 | }, 71 | scaleLabel: { 72 | display: true, 73 | }, 74 | }, 75 | ], 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "ES2020", 5 | "moduleResolution": "Node", 6 | "jsx": "react", 7 | "strict": true, 8 | "allowSyntheticDefaultImports": true, 9 | "esModuleInterop": true, 10 | "importHelpers": true, 11 | "declaration": true, 12 | "outDir": "lib", 13 | "sourceMap": true 14 | }, 15 | "include": ["src"] 16 | } 17 | --------------------------------------------------------------------------------