diff --git a/CHANGELOG.JSON b/CHANGELOG.JSON index ee7a17952..cde145e9e 100644 --- a/CHANGELOG.JSON +++ b/CHANGELOG.JSON @@ -1,5 +1,34 @@ { "versions": [ + { + "version": "1.11.0", + "changes": { + "new": [ + "`Map`: Newly introduced map control is available [#14](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/14)", + "`ChartControl`: Newly introduced control to render charts [#15](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/15)" + ], + "enhancements": [ + "`PeoplePicker`: Allow the people picker to search on site level and on tenant level [#97](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/97)", + "`ListView`: Added support for filtering [#99](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/99)", + "`PeoplePicker`: Make the titleText property not required [#184](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/184)", + "`Placeholder`: Added the ability to specify if the button can be hidden [#206](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/206)", + "Updated the `office-ui-fabric-react` to the same version as in SPFx 1.7.0" + ], + "fixes": [ + "`IFrameDialog`: fix for spinner which keeps appearing on the iframe [#154](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/154)", + "`PeoplePicker`: fix SharePoint groups which could not be retrieved [#161](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/161)", + "`TaxonomyPicker`: fix sort order with lowercased terms [#205](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/205)" + ] + }, + "contributions": [ + "[Hugo Bernier](https://github.com/hugoabernier)", + "[Asish Padhy](https://github.com/AsishP)", + "[Piotr Siatka](https://github.com/siata13)", + "[Anoop Tatti](https://github.com/anoopt)", + "[Alex Terentiev](https://github.com/AJIXuMuK)", + "[Tse Kit Yam](https://github.com/tsekityam)" + ] + }, { "version": "1.10.0", "changes": { diff --git a/CHANGELOG.md b/CHANGELOG.md index ca7c8e430..4d1ec2f57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,37 @@ # Releases +## 1.11.0 + +### New control(s) + +- `Map`: Newly introduced map control is available [#14](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/14) +- `ChartControl`: Newly introduced control to render charts [#15](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/15) + +### Enhancements + +- `PeoplePicker`: Allow the people picker to search on site level and on tenant level [#97](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/97) +- `ListView`: Added support for filtering [#99](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/99) +- `PeoplePicker`: Make the titleText property not required [#184](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/184) +- `Placeholder`: Added the ability to specify if the button can be hidden [#206](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/206) +- Updated the `office-ui-fabric-react` to the same version as in SPFx 1.7.0 + +### Fixes + +- `IFrameDialog`: fix for spinner which keeps appearing on the iframe [#154](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/154) +- `PeoplePicker`: fix SharePoint groups which could not be retrieved [#161](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/161) +- `TaxonomyPicker`: fix sort order with lowercased terms [#205](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/205) + +### Contributors + +Special thanks to our contributors (in alphabetical order): [Hugo Bernier](https://github.com/hugoabernier), [Asish Padhy](https://github.com/AsishP), [Piotr Siatka](https://github.com/siata13), [Anoop Tatti](https://github.com/anoopt), [Alex Terentiev](https://github.com/AJIXuMuK), [Tse Kit Yam](https://github.com/tsekityam). + ## 1.10.0 -**New control(s)** +### New control(s) - `ListItemPicker`: New field control [#165](https://github.com/SharePoint/sp-dev-fx-controls-react/pull/165) -**Enhancements** +### Enhancements - Dutch localization added [#100](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/100) - German localization added [#101](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/101) @@ -25,11 +50,11 @@ Special thanks to our contributors (in alphabetical order): [Marc D Anderson](ht ## 1.9.0 -**Enhancements** +### Enhancements - Optimize bundle size for latest SPFx version due to Office UI Fabric specific versioning [#136](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/136) -**Fixes** +### Fixes - `FieldLookupRenderer`: Lookup dialog is empty [#131](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/131) - `IFrameDialog`: Unnecessary horizontal scroll in IFrame dialog [#132](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/132) @@ -41,7 +66,7 @@ Special thanks to our contributors (in alphabetical order): [Gautam Sheth](https ## 1.8.0 -**Enhancements** +### Enhancements - `PeoplePicker`: Specify to hide or show the users/groups which are hidden in the UI [#122](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/122) - `WebPartTitle`: changing font-sizes on different resolutions [#114](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/114) @@ -53,7 +78,7 @@ Special thanks to our contributors (in alphabetical order): [Gautam Sheth](https - `TaxonomyPicker`: Disable the terms which are set as deprecated or unavailable for tagging [#109](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/109) - `PeoplePicker`: Specify principle type to retrieve (users, groups, ...) [#94](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/94) -**Fixes** +### Fixes - `FieldLookupRenderer`: Fixed URL querystring params [#126](https://github.com/SharePoint/sp-dev-fx-controls-react/pull/126) - `IFrameDialog`: dialog width is not correct in IE11 [#118](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/118) @@ -65,14 +90,14 @@ Special thanks to our contributors (in alphabetical order): [Thomas Lamb](https: ## 1.7.0 -**Enhancements** +### Enhancements - `PeoplePicker`: added functionality to initialize the control with person(s) or group(s) [#98](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/98) - `PeoplePicker`: support for searching on contains [#93](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/93) - `PeoplePicker`: find user based on email address [#95](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/95) - Bundle size: statically reference Office UI Fabric components in the FieldRenderer controls [#107](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/107) -**Fixes** +### Fixes - `FieldNameRenderer` onClick does not suppress default link behavior [#103](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/103) @@ -82,11 +107,11 @@ Special thanks to our contributors (in alphabetical order): Octavie van Haaften, ## 1.6.0 -**Enhancements** +### Enhancements - Disabled property for PeoplePicker [#88](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/88) -**Fixes** +### Fixes - New telemetry approach which allows you to use Application Insights [#81](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/81) - PeoplePicker property selectedItems not implemented? [#90](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/90) @@ -97,15 +122,15 @@ Special thanks to our contributor: Octavie van Haaften. ## 1.5.0 -**New control(s)** +### New control(s) - New `PeoplePicker` control added [#19](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/19) -**Enhancements** +### Enhancements - Added properties to the `TaxonomyPicker` to specify which terms are disabled/not-selectable [#82](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/82) -**Fixes** +### Fixes - Bug in `TaxonomyPicker` where values are not updated by an async change [#83](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/83) - `FieldUserRenderer` uses email prop for `GetPropertiesFor` [#84](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/84) @@ -117,128 +142,128 @@ Special thanks to our contributors (in alphabetical order): Asish Padhy, Alex Te ## 1.4.0 -**New control(s)** +### New control(s) - `SecurityTrimmedControl` control got added [#74](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/74) -**Enhancements** +### Enhancements - Allow the `TaxonomyPicker` to also be used in Application Customizer [#77](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/77) - Add `npm postinstall` script to automatically add the locale config [#78](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/78) -**Fixes** +### Fixes - Icon not showing up in the `Placeholder` control [#76](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/76) ## 1.3.0 -**Enhancements** +### Enhancements - `TaxonomyPicker` control got added [#22](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/22) [#63](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/63) [#64](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/64) - `ListPicker` control got added [#34](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/34) -**Fixes** +### Fixes - Issue fixed when the optional `selection` property was not provided to the `ListView` [#65](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/65) ## 1.2.5 -**Fixes** +### Fixes - Undo `ListView` item selection after items array updates [#55](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/55) ## 1.2.4 -**Enhancements** +### Enhancements - Hiding placeholder title on small zones -**Fixes** +### Fixes - iFrame dialog reference fix [#52](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/52) ## 1.2.3 -**Enhancements** +### Enhancements - Optimized telemetry so that it only pushes control data - `WebPartTitle` hide control completely when empty ## 1.2.2 -**Fixes** +### Fixes - Fixes an issue sorting in the `ListView` control while items were selected. Indexes were not updated. ## 1.2.1 -**Fixes** +### Fixes - `FieldTaxonomyRenderer` got fixed to support single and multiple values ## 1.2.0 -**New control(s)** +### New control(s) - Field controls are added to the project - `IFrameDialog` was added to the project -**Fixes** +### Fixes - Fixed theming in the `WebPartTitle` control ## 1.1.3 -**Fixes** +### Fixes - `FileTypeIcon` icon fixed where it did not render an icon. This control should now works in SPFx extensions. ## 1.1.2 -**Enhancements** +### Enhancements - Improved telemetry with some object checks -**Fixes** +### Fixes - Fix for `WebPartTitle` control to inherit color ## 1.1.1 -**Enhancements** +### Enhancements - Removed operation name from telemetry ## 1.1.0 -**Enhancements** +### Enhancements - Telemetry added ## 1.0.0 -**New control(s)** +### New control(s) - `WebPartTitle` control got added -**Enhancements** +### Enhancements - ListView control got extended with the ability to specify a set of preselected items. ## Beta 1.0.0-beta.8 -**Fixes** +### Fixes - Fix for the `ListView` control when selection is used in combination with `setState`. ## Beta 1.0.0-beta.7 -**New control(s)** +### New control(s) - Grouping functionality added to the `ListView` control ## Beta 1.0.0-beta.6 -**New control(s)** +### New control(s) - Initial release diff --git a/docs/documentation/docs/about/release-notes.md b/docs/documentation/docs/about/release-notes.md index ca7c8e430..4d1ec2f57 100644 --- a/docs/documentation/docs/about/release-notes.md +++ b/docs/documentation/docs/about/release-notes.md @@ -1,12 +1,37 @@ # Releases +## 1.11.0 + +### New control(s) + +- `Map`: Newly introduced map control is available [#14](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/14) +- `ChartControl`: Newly introduced control to render charts [#15](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/15) + +### Enhancements + +- `PeoplePicker`: Allow the people picker to search on site level and on tenant level [#97](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/97) +- `ListView`: Added support for filtering [#99](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/99) +- `PeoplePicker`: Make the titleText property not required [#184](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/184) +- `Placeholder`: Added the ability to specify if the button can be hidden [#206](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/206) +- Updated the `office-ui-fabric-react` to the same version as in SPFx 1.7.0 + +### Fixes + +- `IFrameDialog`: fix for spinner which keeps appearing on the iframe [#154](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/154) +- `PeoplePicker`: fix SharePoint groups which could not be retrieved [#161](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/161) +- `TaxonomyPicker`: fix sort order with lowercased terms [#205](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/205) + +### Contributors + +Special thanks to our contributors (in alphabetical order): [Hugo Bernier](https://github.com/hugoabernier), [Asish Padhy](https://github.com/AsishP), [Piotr Siatka](https://github.com/siata13), [Anoop Tatti](https://github.com/anoopt), [Alex Terentiev](https://github.com/AJIXuMuK), [Tse Kit Yam](https://github.com/tsekityam). + ## 1.10.0 -**New control(s)** +### New control(s) - `ListItemPicker`: New field control [#165](https://github.com/SharePoint/sp-dev-fx-controls-react/pull/165) -**Enhancements** +### Enhancements - Dutch localization added [#100](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/100) - German localization added [#101](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/101) @@ -25,11 +50,11 @@ Special thanks to our contributors (in alphabetical order): [Marc D Anderson](ht ## 1.9.0 -**Enhancements** +### Enhancements - Optimize bundle size for latest SPFx version due to Office UI Fabric specific versioning [#136](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/136) -**Fixes** +### Fixes - `FieldLookupRenderer`: Lookup dialog is empty [#131](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/131) - `IFrameDialog`: Unnecessary horizontal scroll in IFrame dialog [#132](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/132) @@ -41,7 +66,7 @@ Special thanks to our contributors (in alphabetical order): [Gautam Sheth](https ## 1.8.0 -**Enhancements** +### Enhancements - `PeoplePicker`: Specify to hide or show the users/groups which are hidden in the UI [#122](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/122) - `WebPartTitle`: changing font-sizes on different resolutions [#114](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/114) @@ -53,7 +78,7 @@ Special thanks to our contributors (in alphabetical order): [Gautam Sheth](https - `TaxonomyPicker`: Disable the terms which are set as deprecated or unavailable for tagging [#109](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/109) - `PeoplePicker`: Specify principle type to retrieve (users, groups, ...) [#94](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/94) -**Fixes** +### Fixes - `FieldLookupRenderer`: Fixed URL querystring params [#126](https://github.com/SharePoint/sp-dev-fx-controls-react/pull/126) - `IFrameDialog`: dialog width is not correct in IE11 [#118](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/118) @@ -65,14 +90,14 @@ Special thanks to our contributors (in alphabetical order): [Thomas Lamb](https: ## 1.7.0 -**Enhancements** +### Enhancements - `PeoplePicker`: added functionality to initialize the control with person(s) or group(s) [#98](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/98) - `PeoplePicker`: support for searching on contains [#93](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/93) - `PeoplePicker`: find user based on email address [#95](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/95) - Bundle size: statically reference Office UI Fabric components in the FieldRenderer controls [#107](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/107) -**Fixes** +### Fixes - `FieldNameRenderer` onClick does not suppress default link behavior [#103](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/103) @@ -82,11 +107,11 @@ Special thanks to our contributors (in alphabetical order): Octavie van Haaften, ## 1.6.0 -**Enhancements** +### Enhancements - Disabled property for PeoplePicker [#88](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/88) -**Fixes** +### Fixes - New telemetry approach which allows you to use Application Insights [#81](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/81) - PeoplePicker property selectedItems not implemented? [#90](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/90) @@ -97,15 +122,15 @@ Special thanks to our contributor: Octavie van Haaften. ## 1.5.0 -**New control(s)** +### New control(s) - New `PeoplePicker` control added [#19](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/19) -**Enhancements** +### Enhancements - Added properties to the `TaxonomyPicker` to specify which terms are disabled/not-selectable [#82](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/82) -**Fixes** +### Fixes - Bug in `TaxonomyPicker` where values are not updated by an async change [#83](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/83) - `FieldUserRenderer` uses email prop for `GetPropertiesFor` [#84](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/84) @@ -117,128 +142,128 @@ Special thanks to our contributors (in alphabetical order): Asish Padhy, Alex Te ## 1.4.0 -**New control(s)** +### New control(s) - `SecurityTrimmedControl` control got added [#74](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/74) -**Enhancements** +### Enhancements - Allow the `TaxonomyPicker` to also be used in Application Customizer [#77](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/77) - Add `npm postinstall` script to automatically add the locale config [#78](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/78) -**Fixes** +### Fixes - Icon not showing up in the `Placeholder` control [#76](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/76) ## 1.3.0 -**Enhancements** +### Enhancements - `TaxonomyPicker` control got added [#22](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/22) [#63](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/63) [#64](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/64) - `ListPicker` control got added [#34](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/34) -**Fixes** +### Fixes - Issue fixed when the optional `selection` property was not provided to the `ListView` [#65](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/65) ## 1.2.5 -**Fixes** +### Fixes - Undo `ListView` item selection after items array updates [#55](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/55) ## 1.2.4 -**Enhancements** +### Enhancements - Hiding placeholder title on small zones -**Fixes** +### Fixes - iFrame dialog reference fix [#52](https://github.com/SharePoint/sp-dev-fx-controls-react/issues/52) ## 1.2.3 -**Enhancements** +### Enhancements - Optimized telemetry so that it only pushes control data - `WebPartTitle` hide control completely when empty ## 1.2.2 -**Fixes** +### Fixes - Fixes an issue sorting in the `ListView` control while items were selected. Indexes were not updated. ## 1.2.1 -**Fixes** +### Fixes - `FieldTaxonomyRenderer` got fixed to support single and multiple values ## 1.2.0 -**New control(s)** +### New control(s) - Field controls are added to the project - `IFrameDialog` was added to the project -**Fixes** +### Fixes - Fixed theming in the `WebPartTitle` control ## 1.1.3 -**Fixes** +### Fixes - `FileTypeIcon` icon fixed where it did not render an icon. This control should now works in SPFx extensions. ## 1.1.2 -**Enhancements** +### Enhancements - Improved telemetry with some object checks -**Fixes** +### Fixes - Fix for `WebPartTitle` control to inherit color ## 1.1.1 -**Enhancements** +### Enhancements - Removed operation name from telemetry ## 1.1.0 -**Enhancements** +### Enhancements - Telemetry added ## 1.0.0 -**New control(s)** +### New control(s) - `WebPartTitle` control got added -**Enhancements** +### Enhancements - ListView control got extended with the ability to specify a set of preselected items. ## Beta 1.0.0-beta.8 -**Fixes** +### Fixes - Fix for the `ListView` control when selection is used in combination with `setState`. ## Beta 1.0.0-beta.7 -**New control(s)** +### New control(s) - Grouping functionality added to the `ListView` control ## Beta 1.0.0-beta.6 -**New control(s)** +### New control(s) - Initial release diff --git a/docs/documentation/docs/assets/AreaChart.png b/docs/documentation/docs/assets/AreaChart.png new file mode 100644 index 000000000..4078215c2 Binary files /dev/null and b/docs/documentation/docs/assets/AreaChart.png differ diff --git a/docs/documentation/docs/assets/AreaChartCurved.png b/docs/documentation/docs/assets/AreaChartCurved.png new file mode 100644 index 000000000..3acefd878 Binary files /dev/null and b/docs/documentation/docs/assets/AreaChartCurved.png differ diff --git a/docs/documentation/docs/assets/AreaChartDefault.png b/docs/documentation/docs/assets/AreaChartDefault.png new file mode 100644 index 000000000..9d8430db4 Binary files /dev/null and b/docs/documentation/docs/assets/AreaChartDefault.png differ diff --git a/docs/documentation/docs/assets/AreaChartFillEnd.png b/docs/documentation/docs/assets/AreaChartFillEnd.png new file mode 100644 index 000000000..ab7d058d0 Binary files /dev/null and b/docs/documentation/docs/assets/AreaChartFillEnd.png differ diff --git a/docs/documentation/docs/assets/AreaChartFillStart.png b/docs/documentation/docs/assets/AreaChartFillStart.png new file mode 100644 index 000000000..eb23918a7 Binary files /dev/null and b/docs/documentation/docs/assets/AreaChartFillStart.png differ diff --git a/docs/documentation/docs/assets/BarChart.png b/docs/documentation/docs/assets/BarChart.png new file mode 100644 index 000000000..2b68b45fd Binary files /dev/null and b/docs/documentation/docs/assets/BarChart.png differ diff --git a/docs/documentation/docs/assets/BarChartDefaultColors.png b/docs/documentation/docs/assets/BarChartDefaultColors.png new file mode 100644 index 000000000..fcc64471b Binary files /dev/null and b/docs/documentation/docs/assets/BarChartDefaultColors.png differ diff --git a/docs/documentation/docs/assets/BarTimeScale.png b/docs/documentation/docs/assets/BarTimeScale.png new file mode 100644 index 000000000..c4e72de94 Binary files /dev/null and b/docs/documentation/docs/assets/BarTimeScale.png differ diff --git a/docs/documentation/docs/assets/BubbleChart.png b/docs/documentation/docs/assets/BubbleChart.png new file mode 100644 index 000000000..e8b3f19bd Binary files /dev/null and b/docs/documentation/docs/assets/BubbleChart.png differ diff --git a/docs/documentation/docs/assets/ChartControl.gif b/docs/documentation/docs/assets/ChartControl.gif new file mode 100644 index 000000000..313d06d25 Binary files /dev/null and b/docs/documentation/docs/assets/ChartControl.gif differ diff --git a/docs/documentation/docs/assets/ChartControlDataPromise.gif b/docs/documentation/docs/assets/ChartControlDataPromise.gif new file mode 100644 index 000000000..d4e0f39bf Binary files /dev/null and b/docs/documentation/docs/assets/ChartControlDataPromise.gif differ diff --git a/docs/documentation/docs/assets/ChartControlDataPromiseError.gif b/docs/documentation/docs/assets/ChartControlDataPromiseError.gif new file mode 100644 index 000000000..28148593b Binary files /dev/null and b/docs/documentation/docs/assets/ChartControlDataPromiseError.gif differ diff --git a/docs/documentation/docs/assets/ChartControlDataPromiseSpinner.gif b/docs/documentation/docs/assets/ChartControlDataPromiseSpinner.gif new file mode 100644 index 000000000..7703e857f Binary files /dev/null and b/docs/documentation/docs/assets/ChartControlDataPromiseSpinner.gif differ diff --git a/docs/documentation/docs/assets/ChartControlSample1.png b/docs/documentation/docs/assets/ChartControlSample1.png new file mode 100644 index 000000000..82e7ee71e Binary files /dev/null and b/docs/documentation/docs/assets/ChartControlSample1.png differ diff --git a/docs/documentation/docs/assets/ChartControlSample2.png b/docs/documentation/docs/assets/ChartControlSample2.png new file mode 100644 index 000000000..76467d4b9 Binary files /dev/null and b/docs/documentation/docs/assets/ChartControlSample2.png differ diff --git a/docs/documentation/docs/assets/ChartControlSample3.png b/docs/documentation/docs/assets/ChartControlSample3.png new file mode 100644 index 000000000..25f70d944 Binary files /dev/null and b/docs/documentation/docs/assets/ChartControlSample3.png differ diff --git a/docs/documentation/docs/assets/DoughnutChart.png b/docs/documentation/docs/assets/DoughnutChart.png new file mode 100644 index 000000000..e60152084 Binary files /dev/null and b/docs/documentation/docs/assets/DoughnutChart.png differ diff --git a/docs/documentation/docs/assets/HorizontalBarChart.png b/docs/documentation/docs/assets/HorizontalBarChart.png new file mode 100644 index 000000000..caf0e6cd6 Binary files /dev/null and b/docs/documentation/docs/assets/HorizontalBarChart.png differ diff --git a/docs/documentation/docs/assets/LineChart.png b/docs/documentation/docs/assets/LineChart.png new file mode 100644 index 000000000..4e129afa1 Binary files /dev/null and b/docs/documentation/docs/assets/LineChart.png differ diff --git a/docs/documentation/docs/assets/LineChartCurved.png b/docs/documentation/docs/assets/LineChartCurved.png new file mode 100644 index 000000000..f9b28dae9 Binary files /dev/null and b/docs/documentation/docs/assets/LineChartCurved.png differ diff --git a/docs/documentation/docs/assets/OfficeColorful1.png b/docs/documentation/docs/assets/OfficeColorful1.png new file mode 100644 index 000000000..9de238a1d Binary files /dev/null and b/docs/documentation/docs/assets/OfficeColorful1.png differ diff --git a/docs/documentation/docs/assets/OfficeColorful2.png b/docs/documentation/docs/assets/OfficeColorful2.png new file mode 100644 index 000000000..985a773b4 Binary files /dev/null and b/docs/documentation/docs/assets/OfficeColorful2.png differ diff --git a/docs/documentation/docs/assets/OfficeColorful3.png b/docs/documentation/docs/assets/OfficeColorful3.png new file mode 100644 index 000000000..75802c37e Binary files /dev/null and b/docs/documentation/docs/assets/OfficeColorful3.png differ diff --git a/docs/documentation/docs/assets/OfficeColorful4.png b/docs/documentation/docs/assets/OfficeColorful4.png new file mode 100644 index 000000000..0f379c6ab Binary files /dev/null and b/docs/documentation/docs/assets/OfficeColorful4.png differ diff --git a/docs/documentation/docs/assets/OfficeMono1.png b/docs/documentation/docs/assets/OfficeMono1.png new file mode 100644 index 000000000..1f6f2cecf Binary files /dev/null and b/docs/documentation/docs/assets/OfficeMono1.png differ diff --git a/docs/documentation/docs/assets/OfficeMono10.png b/docs/documentation/docs/assets/OfficeMono10.png new file mode 100644 index 000000000..6fd49280c Binary files /dev/null and b/docs/documentation/docs/assets/OfficeMono10.png differ diff --git a/docs/documentation/docs/assets/OfficeMono11.png b/docs/documentation/docs/assets/OfficeMono11.png new file mode 100644 index 000000000..68ca0e006 Binary files /dev/null and b/docs/documentation/docs/assets/OfficeMono11.png differ diff --git a/docs/documentation/docs/assets/OfficeMono12.png b/docs/documentation/docs/assets/OfficeMono12.png new file mode 100644 index 000000000..c4a6fb3da Binary files /dev/null and b/docs/documentation/docs/assets/OfficeMono12.png differ diff --git a/docs/documentation/docs/assets/OfficeMono13.png b/docs/documentation/docs/assets/OfficeMono13.png new file mode 100644 index 000000000..85e76af14 Binary files /dev/null and b/docs/documentation/docs/assets/OfficeMono13.png differ diff --git a/docs/documentation/docs/assets/OfficeMono2.png b/docs/documentation/docs/assets/OfficeMono2.png new file mode 100644 index 000000000..5c3579a0c Binary files /dev/null and b/docs/documentation/docs/assets/OfficeMono2.png differ diff --git a/docs/documentation/docs/assets/OfficeMono3.png b/docs/documentation/docs/assets/OfficeMono3.png new file mode 100644 index 000000000..f852a2904 Binary files /dev/null and b/docs/documentation/docs/assets/OfficeMono3.png differ diff --git a/docs/documentation/docs/assets/OfficeMono4.png b/docs/documentation/docs/assets/OfficeMono4.png new file mode 100644 index 000000000..9ef79af4c Binary files /dev/null and b/docs/documentation/docs/assets/OfficeMono4.png differ diff --git a/docs/documentation/docs/assets/OfficeMono5.png b/docs/documentation/docs/assets/OfficeMono5.png new file mode 100644 index 000000000..801c3b296 Binary files /dev/null and b/docs/documentation/docs/assets/OfficeMono5.png differ diff --git a/docs/documentation/docs/assets/OfficeMono6.png b/docs/documentation/docs/assets/OfficeMono6.png new file mode 100644 index 000000000..0d8c19029 Binary files /dev/null and b/docs/documentation/docs/assets/OfficeMono6.png differ diff --git a/docs/documentation/docs/assets/OfficeMono7.png b/docs/documentation/docs/assets/OfficeMono7.png new file mode 100644 index 000000000..66561bd13 Binary files /dev/null and b/docs/documentation/docs/assets/OfficeMono7.png differ diff --git a/docs/documentation/docs/assets/OfficeMono8.png b/docs/documentation/docs/assets/OfficeMono8.png new file mode 100644 index 000000000..f60e54e6e Binary files /dev/null and b/docs/documentation/docs/assets/OfficeMono8.png differ diff --git a/docs/documentation/docs/assets/OfficeMono9.png b/docs/documentation/docs/assets/OfficeMono9.png new file mode 100644 index 000000000..8ace6f741 Binary files /dev/null and b/docs/documentation/docs/assets/OfficeMono9.png differ diff --git a/docs/documentation/docs/assets/PieChart.png b/docs/documentation/docs/assets/PieChart.png new file mode 100644 index 000000000..c1c038104 Binary files /dev/null and b/docs/documentation/docs/assets/PieChart.png differ diff --git a/docs/documentation/docs/assets/PieChartFuelGage.png b/docs/documentation/docs/assets/PieChartFuelGage.png new file mode 100644 index 000000000..49f071748 Binary files /dev/null and b/docs/documentation/docs/assets/PieChartFuelGage.png differ diff --git a/docs/documentation/docs/assets/PieChartHalfMoon.png b/docs/documentation/docs/assets/PieChartHalfMoon.png new file mode 100644 index 000000000..28e22f124 Binary files /dev/null and b/docs/documentation/docs/assets/PieChartHalfMoon.png differ diff --git a/docs/documentation/docs/assets/PieChartHalfMoonSideway.png b/docs/documentation/docs/assets/PieChartHalfMoonSideway.png new file mode 100644 index 000000000..edb353454 Binary files /dev/null and b/docs/documentation/docs/assets/PieChartHalfMoonSideway.png differ diff --git a/docs/documentation/docs/assets/PolarAreaChart.png b/docs/documentation/docs/assets/PolarAreaChart.png new file mode 100644 index 000000000..ea9f3da12 Binary files /dev/null and b/docs/documentation/docs/assets/PolarAreaChart.png differ diff --git a/docs/documentation/docs/assets/RadarChart.png b/docs/documentation/docs/assets/RadarChart.png new file mode 100644 index 000000000..e0312ec0b Binary files /dev/null and b/docs/documentation/docs/assets/RadarChart.png differ diff --git a/docs/documentation/docs/assets/ScatterChart.png b/docs/documentation/docs/assets/ScatterChart.png new file mode 100644 index 000000000..a0e17b347 Binary files /dev/null and b/docs/documentation/docs/assets/ScatterChart.png differ diff --git a/docs/documentation/docs/assets/ScatterDatasetStyle.png b/docs/documentation/docs/assets/ScatterDatasetStyle.png new file mode 100644 index 000000000..b99de1dc2 Binary files /dev/null and b/docs/documentation/docs/assets/ScatterDatasetStyle.png differ diff --git a/docs/documentation/docs/assets/ScatterPointStyle.png b/docs/documentation/docs/assets/ScatterPointStyle.png new file mode 100644 index 000000000..54a68349a Binary files /dev/null and b/docs/documentation/docs/assets/ScatterPointStyle.png differ diff --git a/docs/documentation/docs/assets/StackedAreaChart.png b/docs/documentation/docs/assets/StackedAreaChart.png new file mode 100644 index 000000000..2490bc9fe Binary files /dev/null and b/docs/documentation/docs/assets/StackedAreaChart.png differ diff --git a/docs/documentation/docs/assets/StackedAreaChartFillStartMinus1.png b/docs/documentation/docs/assets/StackedAreaChartFillStartMinus1.png new file mode 100644 index 000000000..ee7f28d4a Binary files /dev/null and b/docs/documentation/docs/assets/StackedAreaChartFillStartMinus1.png differ diff --git a/docs/documentation/docs/assets/StackedBarChart.png b/docs/documentation/docs/assets/StackedBarChart.png new file mode 100644 index 000000000..8ffdcbfb4 Binary files /dev/null and b/docs/documentation/docs/assets/StackedBarChart.png differ diff --git a/docs/documentation/docs/assets/map-control.gif b/docs/documentation/docs/assets/map-control.gif new file mode 100644 index 000000000..a0c7e426a Binary files /dev/null and b/docs/documentation/docs/assets/map-control.gif differ diff --git a/docs/documentation/docs/controls/ChartControl.md b/docs/documentation/docs/controls/ChartControl.md new file mode 100644 index 000000000..5455b9990 --- /dev/null +++ b/docs/documentation/docs/controls/ChartControl.md @@ -0,0 +1,415 @@ +# ChartControl control + +This control makes it easy to integrate [Chart.js](https://www.chartjs.org) charts into your web parts. It offers most of the functionality available with Chart.js. + +The control automatically renders responsive charts, uses the environment's theme colors, and renders a hidden table for users with impaired vision. + +Here is an example of the control in action: + +![ChartControl control](../assets/ChartControl.gif) + +## How to use this control in your solutions + +- Check that you installed the `@pnp/spfx-controls-react` dependency. Check out the [getting started](../#getting-started) page for more information about installing the dependency. +- Import the following module to your component: + +```TypeScript +import { ChartControl, ChartType } from '@pnp/spfx-controls-react/lib/ChartControl'; +``` + +- Use the `ChartControl` control in your code as follows: + +```TypeScript + +``` + +### Compatibility with Chart.js + +The majority of Chart.js its options like `data`, `options`, `type`, and `plugins` will work the same way as is -- except that you use TypeScript syntax. + +To find sample code that you can use, visit the [Chart.Js documentation](https://www.chartjs.org/docs/latest/). + +For example, to reproduce following Javascript code sample from [Chart.js](https://www.chartjs.org): + +```Html + + +``` + +You would use the following Typescript code: + +```TypeScript + +``` + +The code above will produce the following chart: + +![ChartControl using code from the Chart.js site](../assets/ChartControlSample1.png) + +### Specifying Data + +The `data` property typically consist of: + +- `labels`: (Optional) An array of strings providing the data labels (e.g.: `['January', 'February', 'March', 'April', 'May', 'June', 'July']`) +- `datasets`: At least one dataset, which contains: + - `label`: (Optional) A label for the data (e.g.: `'My First Dataset'`) + - `data`: An array of numbers (e.g.: `[65, 59, 80, 81, 56, 55, 40]`) + +See [below](#charttype) for more information on what types of data are required for each type of chart. + +### Specifying Data Promises + +The ChartControl makes it easy to retrieve data asynchronously with the `datapromise` property. + +To use `datapromise`, add a function to your web part that returns a `Promise` as follows: + +```TypeScript + private _loadAsyncData(): Promise { + return new Promise((resolve, reject) => { + // Call your own service -- this example returns an array of numbers + // but you could call + const dataProvider: IChartDataProvider = new MockChartDataProvider(); + dataProvider.getNumberArray().then((numbers: number[]) => { + // format your response to ChartData + const data: Chart.ChartData = + { + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'] + datasets: [ + { + label: 'My First dataset', + data: numbers + } + ] + }; + + // resolve the promise + resolve(data); + }); + }); + } +``` + +Then, instead of passing a `data` property, pass your function to the `datapromise` property, as follows: + +```TypeScript + +``` + +If you want, you provide a template to display until the `datapromise` is resolved, as follows: + +```TypeScript +
Please wait...
} +/> +``` + +![Data Promise with Loading Template](../assets/ChartControlDataPromise.gif) + +You can provide full React controls within the `loadingtemplate`. For example, to use the Office UI Fabric `Spinner` control, you would use the following code: + +```TypeScript +import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner'; +... + } +/> +``` + +![Chart Control Data Promise with Spinner](../assets/ChartControlDataPromiseSpinner.gif) + +You can also provide another template to display when the `datapromise` is rejected, as follows: + +```TypeScript + } + rejectedtemplate={(error: string) =>
Something went wrong: {error}
} +/> +``` + +![Chart Control Promise with Reject Template](../assets/ChartControlDataPromiseError.gif) + +### Theme Color Support + +By default, the ChartControl will attempt to use the environment theme colors and fonts for elements such as the chart background color, grid lines, titles, labels, legends, and tooltips. This includes support for dark themes and high contrast themes. + +If you wish, you can disable the use of themes by setting the `useTheme` property to `false`. Doing so will use the standard Chart.js colors and fonts. + +### Office Color Palettes + +You can also simplify the majority of code samples by omitting the `color` properties; the ChartControl will automatically reproduce the color palette that you would get if you used Office to create the chart. + +```TypeScript + +``` + +![ChartControl using the default color palette](../assets/ChartControlSample2.png) + +You can also set the `palette` property to choose one of the Office color palettes. For example, Specifying `ChartPalette.OfficeMonochromatic1` will produce the following chart: + +![ChartControl using the Office Monochromatic 1 color palette](../assets/ChartControlSample3.png) + +### Responsiveness + +The ChartControl will automatically expand to fit its container. If you wish to control the size of the chart, set its parent container size, or use the `className` property to pass your own CSS class and override the dimensions within that class. + +### Accessibility + +As long as you provide labels for all your data elements, the ChartControl will render a hidden table. Users who are visually impaired and use a screen reader will hear a description of the data in the chart. + +You can improve the accessible table by adding an `alternateText`, a `caption` and a `summary`. If you do not provide a `caption`, the control will attempt to use the chart's title. + +For example: + +```TypeScript + +``` + +## Implementation + +### ChartControl Properties + +The ChartControl can be configured with the following properties: + +| Property | Type | Required | Description | +| ---- | ---- | ---- | ---- | +| accessibility | [IChartAccessibility](#ichartaccessibility) | no | Optional property to specify the accessibility options. | +| className | string | no | Optional property to specify a custom class that allows you to change the chart's styles. | +| data | ChartData | no | The data you wish to display. | +| datapromise | Promise | no | The promise to load data asynchronously. Use with `loadingtemplate` and `rejectedtemplate` | +| loadingtemplate | JSX.Element
() => JSX.Element | no | The HTML to display while waiting to resolve the `datapromise` | +| options | ChartOptions | no | Optional property to set the chart's additional options. | +| palette | [ChartPalette](#chartpalette) | no | Optional property to set the desired Office color paette | +| plugins | object[] | no | Optional property to set an array of objects implementing the IChartPlugin interface | +| rejectedtemplate | JSX.Element
() => JSX.Element | no | The HTML to display if the `datapromise` promise returns an error. +| useTheme | boolean | no | Optional property to set whether the ChartControl should attempt to use theme colors. Setting it to false will use the startard Chart.js colors and fonts. | +| type | [ChartType](#charttype) or string | yes | The type of chart you wish to render. You can also use the string equivalent. | +| onClick | (event?: MouseEvent, activeElements?: Array<{}>) => void | no | Optional callback method that get called when a user clicks on the chart | +| onHover | (chart: Chart, event: MouseEvent, activeElements: Array<{}>) => void | no | Optional callback method that get called when a user hovers the chart | +| onResize | (chart: Chart, newSize: ChartSize) => void | no | Optional callback method that get called when the window containing the ChartXontrol resizes | + +You can call the following methods to interact with the chart after it has been initialized: + +| Method | Type | Description | +| ---- | ---- | ---- | +| clear | void | Will clear the chart canvas. Used extensively internally between animation frames, but you might find it useful. | +| getCanvas | () => HTMLCanvasElement | Return the canvass element that contains the chart | +| getChart | () => Chart | Returns the Chart.js instance | +| getDatasetAtEvent | (e: MouseEvent) => Array<{}> | Looks for the element under the event point, then returns all elements from that dataset. This is used internally for 'dataset' mode highlighting | +| getElementAtEvent | (e: MouseEvent) => {} | Calling getElementAtEvent(event) passing an argument of an event will return the single element at the event position. For example, you can use with `onClick` event handlers. | +| getElementsAtEvent | (e: MouseEvent) => Array<{}> | Looks for the element under the event point, then returns all elements at the same data index. This is used internally for 'label' mode highlighting. Calling `getElementsAtEvent(event)` passing an argument of an event will return the point elements that are at that the same position of that event. | +| renderChart | (config: {}) => void | Triggers a redraw of all chart elements. Note, this does not update elements for new data. Use .update() in that case. | +| stop | void | Use this to stop any current animation loop. This will pause the chart during any current animation frame. | +| toBase64Image | () => string | Returns a base 64 encoded string of the chart in it's current state. | +| update | (config?: number \| boolean \| string) => void | Triggers an update of the chart. This can be safely called after updating the data object. This will update all scales, legends, and then re-render the chart. | + +### ChartType + +Defines the type of chart that will be rendered. For more information what data structure is required for each type of chart, review the Chart.js documentation ( links below ). + +| Name | Chart.js Equivalent | Description | +| ---- | ---- | ---- | +| [Bar](./charts/BarChart.md) | [bar](https://www.chartjs.org/docs/latest/charts/bar.html) | Vertical bar chart | +| [Bubble](./charts/BubbleChart.md) | [bubble](https://www.chartjs.org/docs/latest/charts/bubble.html) | Bubble chart | +| [Doughnut](./charts/DoughnutChart.md) | [doughnut](https://www.chartjs.org/docs/latest/charts/doughnut.html) | Doughnut chart | +| [HorizontalBar](./charts/BarChart.md#horizontalbarchart) | [horizontalBar](https://www.chartjs.org/docs/latest/charts/bar.html#horizontal-bar-chart) | Horizontal bar chart | +| [Line](./charts/LineChart.md) | [line](https://www.chartjs.org/docs/latest/charts/line.html) | Line chart | +| [Pie](./charts/PieChart.md) | [pie](https://www.chartjs.org/docs/latest/charts/doughnut.html) | Pie chart | +| [PolarArea](./charts/PolarAreaChart.md) | [polarArea](https://www.chartjs.org/docs/latest/charts/polar.html) | Polar area chart | +| [Radar](./charts/RadarChart.md) | [radar](https://www.chartjs.org/docs/latest/charts/radar.html) | Radar chart | +| [Scatter](./charts/ScatterChart.md) | [scatter](https://www.chartjs.org/docs/latest/charts/scatter.html) | Scatter graph | + +### IChartAccessibility + +The `IChartAccessibility` interface implements the following properties: + +| Property | Type | Required | Description | +| ---- | ---- | ---- | ---- | +| alternateText | string | no | Optional property to provide an accessible alternate text for the chart. We recommend that you use this property with `summary` | +| className| string | no | Optional property to specify a custom CSS class for the accessible table. | +| caption| string | no | Optional property to provide a caption for the accessible table. | +| enable | boolean | no | Optional property to turn on or off the rendering of the accessible table. | +| summary | string | no | Optional property to specify the chart's summary. We recommend that you use this property with `alternateText` | +| onRenderTable | () => JSX.Element | no | Options callback method that allows you to override the accessible table. | + +### ChartPalette + +Defines one of the possible Office color palette to use in a chart. The color palettes are the same that you find within Office. + +| Name | Office Name | Description | Example | +| ---- | ---- | ---- | ---- | +| OfficeColorful1 | Office Colorful Palette 1 | Blue, Orange, Grey, Gold, Blue, Green | ![Office Colorful 1](../assets/OfficeColorful1.png) | +| OfficeColorful2 | Office Colorful Palette 2 | Blue, Grey, Blue, Dark Blue, Dark Grey, Dark Blue | ![Office Colorful 2](../assets/OfficeColorful2.png) | +| OfficeColorful3 | Office Colorful Palette 3 | Orange, Gold, Green, Brown, Dark Yellow, Dark Green | ![Office Colorful 3](../assets/OfficeColorful3.png) | +| OfficeColorful4 | Office Colorful Palette 4 | Green, Blue, Gold, Dark Green, Dark Blue, Dark Yellow | ![Office Colorful 4](../assets/OfficeColorful4.png) | +| OfficeMonochromatic1 | Monochromatic Palette 1 | Blue gradient, dark to light | ![Office Monochromatic 1](../assets/OfficeMono1.png) | +| OfficeMonochromatic2 | Monochromatic Palette 2 | Orange gradient, dark to light | ![Office Monochromatic 2](../assets/OfficeMono2.png) | +| OfficeMonochromatic3 | Monochromatic Palette 3 | Grey gradient, dark to light | ![Office Monochromatic 3](../assets/OfficeMono3.png) | +| OfficeMonochromatic4 | Monochromatic Palette 4 | Gold gradient, dark to light | ![Office Monochromatic 4](../assets/OfficeMono4.png) | +| OfficeMonochromatic5 | Monochromatic Palette 5 | Blue gradient, dark to light | ![Office Monochromatic 5](../assets/OfficeMono5.png) | +| OfficeMonochromatic6 | Monochromatic Palette 6 | Green gradient, dark to light | ![Office Monochromatic 6](../assets/OfficeMono6.png) | +| OfficeMonochromatic7 | Monochromatic Palette 7 | Dark Grey, Light Grey, Grey, Dark Grey, Light Grey, Grey | ![Office Monochromatic 7](../assets/OfficeMono7.png) | +| OfficeMonochromatic8 | Monochromatic Palette 8 | Blue gradient, light to dark | ![Office Monochromatic 8](../assets/OfficeMono8.png) | +| OfficeMonochromatic9 | Monochromatic Palette 9 | Orange gradient, light to dark | ![Office Monochromatic 9](../assets/OfficeMono9.png) | +| OfficeMonochromatic10 | Monochromatic Palette 10 | Grey gradient, light to dark | ![Office Monochromatic 10](../assets/OfficeMono10.png) | +| OfficeMonochromatic11 | Monochromatic Palette 11 | Gold gradient, light to dark | ![Office Monochromatic 11](../assets/OfficeMono11.png) | +| OfficeMonochromatic12 | Monochromatic Palette 12 | Blue gradient, light to dark | ![Office Monochromatic 12](../assets/OfficeMono12.png) | +| OfficeMonochromatic13 | Monochromatic Palette 13 | Green gradient, light to dark | ![Office Monochromatic 13](../assets/OfficeMono13.png) | + +### IChartPlugin + +The easiest way to customize a chart is to use the [plugin functionality](https://www.chartjs.org/docs/latest/notes/extensions.html) provided by [Chart.js](https://www.chartjs.org/). In order to use a plugin, simply pass an array of objects that implement the `IChartPlugin` interface to the `plugins` property of the ChartControl. + +If a hook is listed as cancellable, you can return `false` to cancel the event. + +| Property | Type | Required | Description | +| ---- | ---- | ---- | ---- | +| afterDatasetsDraw | (chartInstance: Chart, easing: string, options?: {}) => void | no | Called after the datasets are drawn but after scales are drawn. | +| afterDatasetUpdate | (chartInstance: Chart, options?: {}) => void | no | Called after a dataset was updated. | +| afterDraw | (chartInstance: Chart, easing: string, options?: {}) => void | no | Called after an animation frame was drawn. | +| afterEvent | (chartInstance: Chart, event: Event, options?: {}) => void | no | Called after an event occurs on the chart. | +| afterInit | (chartInstance: Chart, options?: {}) => void | no | Called after a chart initializes | +| afterLayout | (chartInstance: Chart, options?: {}) => void | no | Called after the chart layout was rendered. | +| afterRender | (chartInstance: Chart, options?: {}) => void | no | Called after a rander. | +| afterTooltipDraw | (chartInstance: Chart, tooltipData?: {}, options?: {}) => void | no | Called after drawing the `tooltip`. Note that this hook will not be called if the tooltip drawing has been previously cancelled. | +| afterUpdate | (chartInstance: Chart, options?: {}) => void | no | Called after a chart updates | +| beforeDatasetsDraw | (chartInstance: Chart, easing: string, options?: {}) => void | no | Called before the datasets are drawn but after scales are drawn. Cancellable. | +| beforeDatasetUpdate| (chartInstance: Chart, options?: {}) => void | no | Called before a dataset is updated. Cancellable. | +| beforeDraw | (chartInstance: Chart, easing: string, options?: {}) => void | no | Called before an animation frame is drawn. | +| beforeEvent | (chartInstance: Chart, event: Event, options?: {}) => void | no | Called when an event occurs on the chart. Cancellable. | +| beforeInit | (chartInstance: Chart, options?: {}) => void | no | Called before a chart initializes | +| beforeLayout | (chartInstance: Chart, options?: {}) => void | no | Called before rendering the chart's layout. Cancellable. | +| beforeRender | (chartInstance: Chart, options?: {}) => void | no | Called at the start of a render. It is only called once, even if the animation will run for a number of frames. Use beforeDraw or afterDraw to do something on each animation frame. Cancellable. | +| beforeTooltipDraw | (chartInstance: Chart, tooltipData?: {}, options?: {}) => void | no | Called before drawing the `tooltip`. Cancellable. If it returns `false`, tooltip drawing is cancelled until another `render` is triggered. | +| beforeUpdate | (chartInstance: Chart, options?: {}) => void | no | Called before updating the chart. Cancellable. | +| destroy | (chartInstance: Chart) => void | no | Called when a chart is destroyed. | +| resize | (chartInstance: Chart, newChartSize: Chart.ChartSize, options?: {}) => void | no | Called when a chart resizes. Cancellable. | + +![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/ChartControl) diff --git a/docs/documentation/docs/controls/IFrameDialog.md b/docs/documentation/docs/controls/IFrameDialog.md index bb686cce1..669764444 100644 --- a/docs/documentation/docs/controls/IFrameDialog.md +++ b/docs/documentation/docs/controls/IFrameDialog.md @@ -49,5 +49,13 @@ The IFrameDialog component can be configured with the following properties: | iframeOnload | iframeOnLoad?: (iframe: any) => {} | no | iframe's onload event handler | | width | string | yes | iframe's width | | heigth | string | yes | iframe's height | +| allowFullScreen | boolean | no | Specifies if iframe content can be displayed in a full screen | +| allowTransparency | boolean | no | Specifies if transparency is allowed in iframe | +| marginHeight | number | no | Specifies the top and bottom margins of the content of an iframe | +| marginWidth | number | no | Specifies the left and right margins of the content of an iframe | +| name | string | no | Specifies the name of an iframe | +| sandbox | string | no | Enables an extra set of restrictions for the content in an iframe | +| scrolling | string | no | Specifies whether or not to display scrollbars in an iframe | +| seamless | string | no | When present, it specifies that the iframe should look like it is a part of the containing document (no borders or scrollbars) | ![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/FileTypeIcon) diff --git a/docs/documentation/docs/controls/ListView.md b/docs/documentation/docs/controls/ListView.md index 6ee114f25..c33154438 100644 --- a/docs/documentation/docs/controls/ListView.md +++ b/docs/documentation/docs/controls/ListView.md @@ -27,8 +27,12 @@ import { ListView, IViewField, SelectionMode, GroupOrder, IGrouping } from "@pnp compact={true} selectionMode={SelectionMode.multiple} selection={this._getSelection} + showFilter={true} + defaultFilter="John" + filterPlaceHolder="Search..." groupByFields={groupByFields} /> ``` +- The control provides full text filtering through all the columns. If you want to exectue filtering on the specified columns, you can use syntax : ``:``. Use `':'` as a separator between column name and value. Control support both `'fieldName'` and `'name'` properties of IColumn interface. - With the `selection` property you can define a method that which gets called when the user selects one or more items in the list view: @@ -71,6 +75,9 @@ The ListView control can be configured with the following properties: | selection | function | no | Selection event that passes the selected item(s) from the list view. | | groupByFields | IGrouping[] | no | Defines the field on which you want to group the items in the list view. | | defaultSelection | number[] | no | The index of the items to be select by default | +| filterPlaceHolder | string | no | Specify the placeholder for the filter text box. Default 'Search' | +| showFilter | boolean | no | Specify if the filter text box should be rendered. | +| defaultFilter | string | no | Specify the initial filter to be applied to the list. | The `IViewField` has the following implementation: diff --git a/docs/documentation/docs/controls/Map.md b/docs/documentation/docs/controls/Map.md new file mode 100644 index 000000000..c1cd3a0ea --- /dev/null +++ b/docs/documentation/docs/controls/Map.md @@ -0,0 +1,62 @@ +# Map control + +This control renders can be used to render a map in your solution. The control has also the ability to search for new locations. + +Here is an example of the control in action: + +![Map control](../assets/map-control.gif) + +## How to use this control in your solutions + +- Check that you installed the `@pnp/spfx-controls-react` dependency. Check out the [getting started](../#getting-started) page for more information about installing the dependency. +- In your component file, import the `Map` control as follows: + +```TypeScript +import { Map, ICoordinates, MapType } from "@pnp/spfx-controls-react/lib/Map"; +``` + +- Use the `Map` control in your code as follows: + +```TypeScript + +``` + +## Implementation + +The `Map` control can be configured with the following properties: + +| Property | Type | Required | Description | Default | +| ---- | ---- | ---- | ---- | ---- | +| titleText | string | no | Title label to show above the control. | | +| coordinates | ICoordinates | yes | Coordinates required for rendering the control. | | +| enableSearch | boolean | no | Allow the user to search for new locations. | | +| zoom | number | no | Zoom level for the maps on display (range 1-15). | 10 | +| width | number | no | Width of the maps area in percentage. | 100% | +| height | number | no | Height of the maps area. | 300px | +| mapType | MapType | no | Type of the map to render. | standard | +| loadingMessage | string | no | Custom loading message. | | +| errorMessage | string | no | Custom error message. | | +| mapsClassName | string | no | Custom CSS class name. | | +| errorMessageClassName | string | no | Custom CSS error class name. | | +| onUpdateCoordinates | (coordinates: ICoordinates) => void | no | Callback to let your solution knows the new coordinates when a search was performed. | | + +`ICoordinates` interface: + +| Property | Type | Required | Description | Default | +| ---- | ---- | ---- | ---- | ---- | +| latitude | number | yes | Latitude of the map to display. | | +| longitude | number | yes | Longitude of the map to display. | | + +`MapType` enum: + +| Name | +| ---- | +| standard | +| cycle | +| normal | +| transport | + + +![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/Map) diff --git a/docs/documentation/docs/controls/PeoplePicker.md b/docs/documentation/docs/controls/PeoplePicker.md index 8275b0568..9ed2337a9 100644 --- a/docs/documentation/docs/controls/PeoplePicker.md +++ b/docs/documentation/docs/controls/PeoplePicker.md @@ -37,7 +37,8 @@ import { PeoplePicker, PrincipalType } from "@pnp/spfx-controls-react/lib/People disabled={true} selectedItems={this._getPeoplePickerItems} showHiddenInUI={false} - principleTypes={[PrincipalType.User]} /> + principalTypes={[PrincipalType.User]} + resolveDelay={1000} /> ``` - With the `selectedItems` property you can get the selected People in the Peoplepicker : @@ -55,34 +56,35 @@ The People picker control can be configured with the following properties: | Property | Type | Required | Description | Default | | ---- | ---- | ---- | ---- | ---- | | context | WebPartContext | yes | Context of the current web part. | | -| titleText | string | yes | Text to be displayed on the control | | +| titleText | string | no | Text to be displayed on the control | | | groupName | string | no | group from which users are fetched. Leave it blank if need to filter all users | _none_ | -| personSelectionLimit | number | no | Defines the limit of people that can be selected in the control | | +| personSelectionLimit | number | no | Defines the limit of people that can be selected in the control | 1 | | isRequired | boolean | no | Set if the control is required or not | false | | disabled | boolean | no | Set if the control is disabled or not | false | | errorMessage | string | no | Specify the error message to display | | | errorMessageClassName | string | no | applies custom styling to the error message section | | | showtooltip | boolean | no | Defines if need a tooltip or not | false | -| tooltip | string | no | Specify the tooltip message to display | | +| tooltipMessage | string | no | Specify the tooltip message to display | | | tooltipDirectional | DirectionalHint | no | Direction where the tooltip would be shown | | -| selectedItems | function | no | get the selected users in the control | | +| selectedItems | (items: IPersonaProps[]) => void | no | Get the selected users in the control. | | | peoplePickerWPclassName | string | no | applies custom styling to the people picker element | | | peoplePickerCntrlclassName | string | no | applies custom styling to the people picker control only | | -| defaultSelectedUsers | string[] | no | Default selected user emails | | -| webAbsoluteUrl | string | no | Specify the site URL on which you want to perform the user query call. | Current site URL | -| showHiddenInUI | boolean | no | Show users which are hidden from the UI. By default these users/groups hidden for the UI will not be shown. | false | -| principleTypes | PrincipleType[] | no | Define which type of data you want to retrieve: User, SharePoint groups, Security groups. Multiple are possible. | | +| defaultSelectedUsers | string[] | no | Default selected user emails or login names | | +| webAbsoluteUrl | string | no | Specify the site URL on which you want to perform the user query call. If not provided, the people picker will perform a tenant wide people/group search. When provided it will search users/groups on the provided site. | | +| principalTypes | PrincipalType[] | no | Define which type of data you want to retrieve: User, SharePoint groups, Security groups. Multiple are possible. | | +| ensureUser | boolean | no | When ensure user property is true, it will return the local user ID on the current site when doing a tenant wide search. | false | | suggestionsLimit | number | no | Maximum number of suggestions to show in the full suggestion list. | 5 | +| resolveDelay | number | no | Add delay to resolve and search users | 200 | Enum `PrincipalType` The `PrincipalType` enum can be used to specify the types of information you want to query: User, Security groups, and/or SharePoint groups. -| Name | -| ---- | -| User | -| DistributionList | -| SecurityGroup | -| SharePointGroup | +| Name | Value | +| ---- | ---- | +| User | 1 | +| DistributionList | 2 | +| SecurityGroup | 4 | +| SharePointGroup | 8 | ![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/PeoplePicker) diff --git a/docs/documentation/docs/controls/Placeholder.md b/docs/documentation/docs/controls/Placeholder.md index eecfcd3af..250e7733e 100644 --- a/docs/documentation/docs/controls/Placeholder.md +++ b/docs/documentation/docs/controls/Placeholder.md @@ -16,12 +16,11 @@ import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder"; - Use the `Placeholder` control in your code as follows: ```TypeScript - + ``` - With the `onConfigure` property you can define what it needs to do when you click on the button. Like for example opening the property pane: @@ -33,6 +32,17 @@ private _onConfigure() { } ``` +Sample of using the `hideButton` functionality for hiding the button when page is in read mode: + +```TypeScript + +``` + ## Implementation The placeholder control can be configured with the following properties: @@ -44,6 +54,7 @@ The placeholder control can be configured with the following properties: | description | string | yes | Text description for the placeholder. This appears bellow the Icon and IconText. | | iconName | string | yes | The name of the icon that will be used in the placeholder. This is the same name as you can find on the Office UI Fabric icons page: [Office UI Fabric icons](https://dev.office.com/fabric#/styles/icons). For example: `Page` or `Add`. | | iconText | string | yes | Heading text which is displayed next to the icon. | +| hideButton | boolean | no | Specify if you want to hide the button. Default is `false`. | | onConfigure | function | no | onConfigure handler for the button. The button is optional. | ![](https://telemetry.sharepointpnp.com/sp-dev-fx-controls-react/wiki/controls/Placeholder) diff --git a/docs/documentation/docs/controls/charts/BarChart.md b/docs/documentation/docs/controls/charts/BarChart.md new file mode 100644 index 000000000..b57e263bd --- /dev/null +++ b/docs/documentation/docs/controls/charts/BarChart.md @@ -0,0 +1,382 @@ +# ChartControl - Bar Chart + +Bar charts represent data values as vertical bars. + +![Default Bar Chart](../../assets/BarChart.png) + +## Example Usage + +To create a bar chart, add the ChartControl import: + +```TypeScript +import { ChartControl, ChartType } from '@pnp/spfx-controls-react/lib/ChartControl'; +``` + +Then render the ChartControl: + +```TypeScript + +``` + +For example, to render the chart above, use the following code: + +```TypeScript +// set the data +const data: Chart.ChartData = { + labels: + [ + 'January', 'February', 'March', 'April', 'May', 'June', 'July' + ], + datasets: + [{ + label: 'My First Dataset', + data: + [ + 65, 59, 80, 81, 56, 55, 40 + ], + backgroundColor: + [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(255, 159, 64, 0.2)', + 'rgba(255, 205, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(201, 203, 207, 0.2)' + ], + borderColor: + [ + 'rgb(255, 99, 132)', + 'rgb(255, 159, 64)', + 'rgb(255, 205, 86)', + 'rgb(75, 192, 192)', + 'rgb(54, 162, 235)', + 'rgb(153, 102, 255)', + 'rgb(201, 203, 207)' + ], + borderWidth: 1 + }] + }; + +// set the options +const options: Chart.ChartOptions = { + scales: + { + yAxes: + [ + { + ticks: + { + beginAtZero: true + } + } + ] + } +}; + +return ( + ); +``` + +You can omit the `backgroundColor`, `borderColor`, and `borderWidth` values from the code above to render a chart using the default Office palette: + +```TypeScript +// set the data +const data: Chart.ChartData = { + labels: + [ + 'January', 'February', 'March', 'April', 'May', 'June', 'July' + ], + datasets: + [{ + label: 'My First Dataset', + data: + [ + 65, 59, 80, 81, 56, 55, 40 + ] + }] + }; + +// set the options +const options: Chart.ChartOptions = { + scales: + { + yAxes: + [ + { + ticks: + { + beginAtZero: true + } + } + ] + } +}; + +return ( + ); +``` + +Which will produce the following chart: + +![Default Chart Colors](../../assets/BarChartDefaultColors.png) + +As with all charts, the `backgroundColor` and `borderColor` values can be one of the following: + +* Array of colors (`string[]`): each data element in the dataset will be assigned a different color in the same order as they are listed in the array. If there are more data elements than colors, the remaining data elements will have a grey color. +* A single color (`string`): every data element in the dataset will use the same color. + +## Variations + +### Stacked Bar Chart + +![Stacked Bar Chart](../../assets/StackedBarChart.png) + +If your bar chart has multiple datasets, you can render it as a stacked bar chart by changing the settings on the X and Y axes to enable stacking, as follows: + +```TypeScript +const options: Chart.ChartOptions = { + scales: { + xAxes: [{ + stacked: true + }], + yAxes: [{ + stacked: true + }] + } +}; +``` + +In order to render each dataset with a different color, make sure to specify the `backgroundColor` and `borderColor` settings for each dataset: + +```TypeScript +const data: Chart.ChartData = { + labels: + [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July' + ], + datasets: + [ + { + label: 'My First Dataset', + data: + [ + 65, + 59, + 80, + 81, + 56, + 55, + 40 + ], + fill: false, + backgroundColor: 'rgba(255, 99, 132, 0.2)', // same color for all data elements + borderColor: 'rgb(255, 99, 132)', // same color for all data elements + borderWidth: 1 + }, + { + label: 'My Second Dataset', + data: + [ + 15, + 29, + 30, + 8, + 26, + 35, + 20 + ], + fill: false, + backgroundColor: 'rgba(255, 159, 64, 0.2)', // same color for all data elements + borderColor: 'rgb(255, 159, 64)', // same color for all data elements + borderWidth: 1 + } + ] + }; +``` + +### Horizontal Bar Chart + +![Horizontal Bar Chart](../../assets/HorizontalBarChart.png) + +To render a horizontal bar, use the following code: + +```TypeScript + +``` + +Using the same options as above. Note that horizontal bar charts can also be stacked using the same approach as above. + +## Dataset Properties + +Bar charts allow each dataset to have different configuration properties. + +Some properties can be provided as arrays. When arrays are provided, the settings in the array will be applied to each data element in the same order (e.g.: first value applies to first element, second value to second element, etc.) + +For example, the following code will apply the value `'red:'` to every element in the dataset. + +```TypeScript +const data: Chart.ChartData = { + datasets: + [ + { + label: 'My First Dataset', + data: + [ + 10, + 20, + 30, + ], + backgroundColor: 'red' + } + }; +``` + +Whereas the following code will set `10` to `backgroundColor` `'red'`, `20` to `'green'`, and `30` to `'blue'`. + +```TypeScript +const data: Chart.ChartData = { + datasets: + [ + { + label: 'My First Dataset', + data: + [ + 10, + 20, + 30, + ], + backgroundColor: ['red', 'green', 'blue'] + } + }; +``` + +| Name | Type | Description | +| ---- | ---- | ---- | +| label | string | Dataset label. Appears in the legend and tooltips. | +| xAxisID | string | The axis ID for the X axis. If not specified, the dataset will be rendered on the first available X axis. If an ID is specified, the dataset will be rendered on that axis | +| yAxisID | string | The axis ID for the Y axis. If not specified, the dataset will be rendered on the first available Y axis. If an ID is specified, the dataset will be rendered on that axis | +| backgroundColor | Color OR Color[] | The bar fill color. | +| borderColor | Color OR Color[] | The bar border color. | +| borderWidth | number OR number[] | The bar's border width. Measured in pixels. | +| borderSkipped | `'bottom'`
`'left'`
`'top'`
`'right'` | Specifies which border should be hidden when rendering bars. This option is useful when custom-rendering charts. | +| data | number[]
{ t: Date, y: number}| The chart's data. Required. | +| hoverBackgroundColor | Color OR Color[] | The bar's fill color when a mouse hovers over it | +| hoverBorderColor | Color OR Color[] | The bar's border color when a mouse hovers over it. | +| hoverBorderWidth | number OR number[] | The bar's border width when a mouse hovers over it. | + +## Data Structure + +### number[] + +The `data` property of each dataset item consists of an array of numbers. Each point in the array corresponds to the matching label on the x axis: + +```TypeScript +data: [20, 10, 33, 47] +``` + +The chart elements will be rendered in the same order as found in the array. + +### Time Scales + +You can also provide dates and times instead of labels for each data element. Time scales should be provided as `t` (as in `ticks`) and `y` coordinates: + +```TypeScript +data: [ + { "t": new Date('December 1 2018'), "y": 46 }, + { "t": new Date('December 3 2018'), "y": 9 }, + { "t": new Date('December 7 2018'), "y": 48 }, + { "t": new Date('December 10 2018'), "y": 13 }, + { "t": new Date('December 20 2018'), "y": 12 }, + ] +``` + +Or, if you prefer using [moment.js](http://momentjs.com/): + +```TypeScript + +data: [ + { "t": moment('December 1 2018').valueOf(), "y": 46 }, + { "t": moment('December 3 2018').valueOf(), "y": 9 }, + { "t": moment('December 7 2018').valueOf(), "y": 48 }, + { "t": moment('December 10 2018').valueOf(), "y": 13 }, + { "t": moment('December 20 2018').valueOf(), "y": 12 }, +] +``` + +(add the following to your imports:) + +```TypeScript +import * as moment from 'moment'; +``` + +To render horizontal axis as a time series, use the following options: + +```TypeScript +options={{ + scales: { + xAxes: [{ + type: 'time', + time: { + displayFormats: { + // choose the scale that applies to your data, + // or pass all the scales + 'millisecond': 'MMM DD YYYY', // use your own date format + 'second': 'MMM DD YYYY', + 'minute': 'MMM DD YYYY', + 'hour': 'MMM DD YYYY', + 'day': 'MMM DD YYYY', + 'week': 'MMM DD YYYY', + 'month': 'MMM DD YYYY', + 'quarter': 'MMM DD YYYY', + 'year': 'MMM DD YYYY', + } + } + }] + } + }} +``` + +Which will produce the following chart: + +![Bar Chart Time Series](../../assets/BarTimeScale.png) + +> **NOTE:** As with regular data elements, you should pass the time scale array in the order that you want them to appear. Otherwise, you will get disappointing results. + +## Configuration + +The following configuration options are specific to bar charts: + +| Name | Type | Default | Description | +| ---- | ---- | ---- | ---- | +| barPercentage | number | 0.9 | How much of the category should each bar occupy, in percent. Value should be between 0 and 1. | +| categoryPercentage | number | 0.8 | How much each category should occupy, in percent. Value should be between 0 and 1. | +| barThickness | number OR `"flex"` | | Sets the thickness of each bar. If the default value is used, the bars will be equally sized to match the smallest interval.
If `number` value is provided, the bar width will be set in pixels and `barPercentage` and `categoryPercentage` will be ignored.
If `"flex"` is used, the widths are calculated automatically based on the previous and following samples so that they take the full available widths without overlap | +| maxBarThickness | number | | Sets the maximum width of every bar. | +| gridLines.offsetGridLines | boolean | true | `true`, bars will fall between the grid lines; grid lines will shift to the left by half a tick interval.
If `false`, grid line will align with the middle of the bars. | + +## For More Information + +For more information on what options are available with Bar charts, refer to the [Bar Chart documentation](https://www.chartjs.org/docs/latest/charts/bar.html) on [Chart.js](https://www.chartjs.org). diff --git a/docs/documentation/docs/controls/charts/BubbleChart.md b/docs/documentation/docs/controls/charts/BubbleChart.md new file mode 100644 index 000000000..e13911125 --- /dev/null +++ b/docs/documentation/docs/controls/charts/BubbleChart.md @@ -0,0 +1,109 @@ +# ChartControl - Bubble Chart + +Bubble chart show elements across three dimensions. Each bubble in the chart is located according to the first two dimensions. The size of each bubble represents the thid dimension. + +![Default Bubble Chart](../../assets/BubbleChart.png) + +## Example Usage + +To create a bubble chart, add the ChartControl import: + +```TypeScript +import { ChartControl, ChartType } from '@pnp/spfx-controls-react/lib/ChartControl'; +``` + +Then render the ChartControl: + +```TypeScript + +``` + +For example, to render the chart above, use the following code: + +```TypeScript +// set the data +const data: Chart.ChartData = { + datasets: [ + { + label: "Bubble", + data: + [ + { x: 10, y: 20, r: 20 }, + { x: 85, y: 50, r: 35 }, + { x: 70, y: 70, r: 5 }, + { x: 40, y: 100, r: 5 }, + { x: 50, y: 50, r: 12 }, + { x: 30, y: 80, r: 15 }, + { x: 20, y: 30, r: 15 }, + { x: 40, y: 10, r: 10 } + ] + }] +}; + +// set the options +const options: Chart.ChartOptions = { + legend: { + display: false + }, + title: { + display: true, + text: "My First Bubbles" + } +}; + +return ( + ); +``` + +## Dataset Properties + +Bubble charts allow each dataset to have different configuration properties. + +Some properties can be provided as arrays. When arrays are provided, the settings in the array will be applied to each data element in the same order (e.g.: first value applies to first element, second value to second element, etc.) + +| Name | Type | Description | +| ---- | ---- | ---- | +| backgroundColor | Color OR Color[] | The bubble's fill color. Default is `'rgba(0,0,0,0.1)'`. | +| borderColor | Color OR Color[] | The bubble's border color. Default is `'rgba(0,0,0,0.1)'`. | +| borderWidth | number OR number[] | The width of the bubble's border. Measured in pixels. Default is `3`.| +| data | { x: number, y:number, r: number}[] | The data to render. Required. | +| hoverBackgroundColor | Color OR Color[] | The bubble's background color when a mouse hovers over it. | +| hoverBorderColor | Color OR Color[] | The bubble's border color when a mouse hovers over it. | +| hoverBorderWidth | number OR number[] | The bubble's border width when a mouse hovers over it. Default is `1`. | +| hoverRadius | number OR number[] | The bubble's radius when a mouse hovers over it. Default is `4`. | +| hitRadius | number OR number[] | The bubble's radius when a mouse click event occurs. Default is `1`. | +| label | string | The dataset's label | +| pointStyle | `'circle'`
`'cross'`
`'crossRot'`
`'dash'`
`'line'`
`'rect'`
`'rectRounded'`
`'rectRot'`
`'star'`
`'triangle'`
HTMLImageElement
HTMLCanvasElement
HTMLImageElement[]
HTMLCanvasElement[] | Style of bubble. Default is `'circle'` | +| rotation | number OR number[] | The bubble's rotation, in degrees. Default is `0`. | +| radius | number OR number[] | The bubble's radius. Default is `3`. | + +## Data Structure + +The `data` property of each dataset item consists of an `x`, `y`, and `r` coordinate. + +```TypeScript +{ + // X Value + x: number, + + // Y Value + y: number, + + // Bubble radius in pixels + r: number +} +``` + +> **NOTE:** Unlike the `x` and `y`, the `r` value is measured in pixels and does not scale with the chart. + +## For More Information + +For more information on what options are available with Bubble charts, refer to the [Bubble Chart documentation](https://www.chartjs.org/docs/latest/charts/bubble.html) on [Chart.js](https://www.chartjs.org). diff --git a/docs/documentation/docs/controls/charts/DoughnutChart.md b/docs/documentation/docs/controls/charts/DoughnutChart.md new file mode 100644 index 000000000..59a3fe0dd --- /dev/null +++ b/docs/documentation/docs/controls/charts/DoughnutChart.md @@ -0,0 +1,103 @@ +# ChartControl - Doughnut Chart + +Doughnut charts are divided into segments, each of which shows the proportional value of the data. + +![Default Donut Chart](../../assets/DoughnutChart.png) + +## Example Usage + +To create a donut chart, add the ChartControl import: + +```TypeScript +import { ChartControl, ChartType } from '@pnp/spfx-controls-react/lib/ChartControl'; +``` + +Then render the ChartControl: + +```TypeScript + +``` + +For example, to render the chart above, use the following code: + +```TypeScript +// set the data +const data: Chart.ChartData = { + labels: + [ + 'January', 'February', 'March', 'April', 'May', 'June', 'July' + ], + datasets: [ + { + label: 'My First Dataset', + data: + [ + 65, 59, 80, 81, 56, 55, 40 + ] + } + ] + }; + +// set the options +const options: Chart.ChartOptions = { + legend: { + display: true, + position: "left" + }, + title: { + display: true, + text: "My First Donut" + } + }; + +return ( + ); +``` + +## Dataset Properties + +Doughnut charts allow each dataset to have different configuration properties. + +Properties are provided as arrays. Settings in the array will be applied to each data element in the same order (e.g.: first value applies to first element, second value to second element, etc.) + +| Name | Type | Description | +| ---- | ---- | ---- | +| backgroundColor | Color[] | The segment's fill color. | +| borderColor | Color[] | The segment's border color. | +| borderWidth | number[] | The segment's border width. Measured in pixels. | +| data | number[] | The chart's data. Required. | +| hoverBackgroundColor | Color[] | The segment's fill color when a mouse hovers over it | +| hoverBorderColor | Color[] | The segment's border color when a mouse hovers over it. | +| hoverBorderWidth | number[] | The segment's border width when a mouse hovers over it. | + +## Data Structure + +The `data` property of each dataset item consists of an array of numbers. Each point in the array corresponds to the matching label on the x axis: + +```TypeScript +data: [20, 10, 33, 47] +``` + +## Configuration + +The following configuration options are specific to doughnut charts: + +| Name | Type | Default | Description | +| ---- | ---- | ---- | ---- | +| cutoutPercentage | number | 50 | The percentage of the chart that is cut out of the middle. | +| rotation | number | -0.5 * Math.PI | The angle at which the doughtnut segments start | +| circumference | number | 2 * Math.PI | The total circumference of the donut chart. | +| animation.animateRotate | boolean | true | `true` will animate the chart while rotating it. | +| animation.animateScale | boolean | false | `true` will animate the chart while scaling it. | + +## For More Information + +For more information on what options are available with Doughnut charts, refer to the [Doughnut and Pie documentation](https://www.chartjs.org/docs/latest/charts/doughnut.html) on [Chart.js](https://www.chartjs.org). diff --git a/docs/documentation/docs/controls/charts/LineChart.md b/docs/documentation/docs/controls/charts/LineChart.md new file mode 100644 index 000000000..20e9fdf7c --- /dev/null +++ b/docs/documentation/docs/controls/charts/LineChart.md @@ -0,0 +1,454 @@ +# ChartControl - Line Chart + +Line charts represent data values as plotted points on a line. + +![Default Line Chart](../../assets/LineChart.png) + +## Example Usage + +To create a line chart, add the ChartControl import: + +```TypeScript +import { ChartControl } from "@pnp/spfx-controls-react/lib/ChartControl"; +``` + +Then render the ChartControl: + +```TypeScript + +``` + +Alternatively, you can use the following import: + +```TypeScript +import { ChartControl, ChartType } from '@pnp/spfx-controls-react/lib/ChartControl'; +``` + +Followed by: + +```TypeScript + +``` + +For example, to render the chart above, use the following code: + +```TypeScript +// set the data +const data: Chart.ChartData = { + labels: + [ + 'January', 'February', 'March', 'April', 'May', 'June', 'July' + ], + datasets: [ + { + label: 'My First Dataset', + fill: false, + lineTension: 0, + data: + [ + 65, 59, 80, 81, 56, 55, 40 + ] + } + ] +}; + +// set the options +const options: Chart.ChartOptions = { + legend: { + display: false, + }, + title: { + display: true, + text: "My First Line Chart" + } +}; + +return ( + ); +``` + +## Variations + +### Curved lines + +![Curved Line Chart](../../assets/LineChartCurved.png) + +You can render curved lines instead of straight lines by removing the `lineTension` setting from each dataset, or by setting it to a value other than `0`. + +For example, to render the above chart, use the following code: + +```TypeScript +// set the data +const data: Chart.ChartData = { + labels: + [ + 'January', 'February', 'March', 'April', 'May', 'June', 'July' + ], + datasets: [ + { + label: 'My First Dataset', + fill: false, + //lineTension: 0, -- removed + data: + [ + -65, -59, 80, 81, -56, 55, 40 + ], + backgroundColor: "rgba(255, 99, 132, 0.2)", + borderColor: "rgb(255, 99, 132)", + borderWidth: 1 + }, + ] +}; + +// set the options +const options: Chart.ChartOptions = { + legend: { + display: false, + }, + title: { + display: true, + text: "My First Curved Line Chart" + } +}; + +return ( + ); +``` + +### Area Chart + +![Area Chart](../../assets/AreaChartDefault.png) + +To render an area chart, change the `fill` setting of the dataset to `true`. + +```TypeScript +// set the data +const data: Chart.ChartData = { + labels: + [ + 'January', 'February', 'March', 'April', 'May', 'June', 'July' + ], + datasets: [ + { + label: 'My First Dataset', + fill: true, + lineTension: 0, + data: + [ + 65, 59, 80, 81, 56, 55, 40 + ] + } + ] +}; + +// set the options +const options: Chart.ChartOptions = { + legend: { + display: false, + }, + title: { + display: true, + text: "My First Area Chart" + } +}; + +return ( + ); +``` + +If your chart has negative and positive values, you can control where the filled area by setting the `fill` setting to one of the following value: + +| `fill` Value | Description | Sample | +| ---- | ---- | ---- | +| `'start'` | Fill from the bottom of the chart | ![start fill](../../assets/AreaChartFillStart.png) | +| `'end'` | Fill from the top of the chart | ![end fill](../../assets/AreaChartFillEnd.png) | +| `'origin'` | Fill from the 'zero' line Same as `true` | ![origin fill](../../assets/AreaChart.png) | + +For example, the code below will set the `fill` value to `start`: + +```TypeScript +// set the data +const data: Chart.ChartData = { + labels: + [ + 'January', 'February', 'March', 'April', 'May', 'June', 'July' + ], + datasets: [ + { + label: 'My First Dataset', + fill: 'start', + lineTension: 0, + data: + [ + -65, -59, 80, 81, -56, 55, 40 + ] + } + ] +}; + +// set the options +const options: Chart.ChartOptions = { + legend: { + display: false, + }, + title: { + display: true, + text: "My First Area Chart" + } +}; + +return ( + ); +``` + +Which renders the following chart: + +![Area Chart with Fill = 'start'](../../assets/AreaChartFillStart.png) + +### Stacked Area Chart + +![Stacked Area Chart](../../assets/StackedAreaChart.png) + +If your bar chart has multiple datasets, you can render it as a stacked area chart by changing the settings on the Y axis to enable stacking, as follows: + +```TypeScript +const options: Chart.ChartOptions = { + scales: { + yAxes: [{ + stacked: true + }] + } +}; +``` + +In order to render each dataset with a different color, make sure to specify the `backgroundColor` and `borderColor` settings for each dataset. + +For example, to render the above chart, use the following code: + +```TypeScript +const data: Chart.ChartData = { + labels: + [ + 'January', 'February', 'March', 'April', 'May', 'June', 'July' + ], + datasets: [ + { + label: 'My First Dataset', + fill: true, + lineTension: 0, + data: + [ + -65, -59, 80, 81, -56, 55, 40 + ], + backgroundColor: "rgba(255, 99, 132, 0.2)", + borderColor: "rgb(255, 99, 132)", + borderWidth: 1 + }, + { + label: 'My Second Dataset', + fill: true, + lineTension: 0, + data: + [ + 45, 49, 88, 71, -36, 35, 60 + ], + backgroundColor: 'rgba(255, 159, 64, 0.2)', + borderColor: 'rgb(255, 159, 64)', + borderWidth: 1 + } + + ] +}; + +const options: Chart.ChartOptions = { + legend: { + display: false, + }, + title: { + display: true, + text: "My First Stacked Area Chart" + }, + scales: { + yAxes: [{ + stacked: true + }] + } +}; + +return ( + ); +``` + +As with lines, you can set the `lineTension` value to render curved lines instead of straight lines: + +![Curved Area Chart](../../assets/AreaChartCurved.png) + +In addition to the `fill` values listed above, you can specify how each dataset fill: + +| `fill` Value Type | Description | Sample Values | +| ---- | ---- | ---- | +| number | Fill to dataset by its absolute index | `1`, `2`, `3`, ... | +| string | Fill to dataset by its relative index | `'-3'`, `'-2'`, `'-1'`, `'+1'`, `'+2'`, `'+3'`, ... | + +For example, if you use declare your datasets with the following fill values: + +```TypeScript +const data: Chart.ChartData = { + labels: + [ + 'January', 'February', 'March', 'April', 'May', 'June', 'July' + ], + datasets: [ + { + label: 'My First Dataset', + fill: "start", + lineTension: 0, + data: + [ + -65, -59, 80, 81, -56, 55, 40 + ], + backgroundColor: "rgba(255, 99, 132, 0.2)", + borderColor: "rgb(255, 99, 132)", + borderWidth: 1 + }, + { + label: 'My Second Dataset', + fill: '-1', + lineTension: 0, + data: + [ + 45, 49, 88, 71, -36, 35, 60 + ], + backgroundColor: 'rgba(255, 159, 64, 0.2)', + borderColor: 'rgb(255, 159, 64)', + borderWidth: 1 + } + + ] + }; +``` + +Will cause the first dataset to fill from the bottom of the chart, while the second dataset will fill to the previous dataset (by it's relative index of `-1`) + +![Stacked Area Chart with fill values `start` and `'-1'`](../../assets/StackedAreaChartFillStartMinus1.png) + +## Dataset Properties + +Line charts allow each dataset to have different configuration properties. + +Some properties can be provided as arrays. When arrays are provided, the settings in the array will be applied to each data element in the same order (e.g.: first value applies to first element, second value to second element, etc.) + +| Name | Type | Description | +| ---- | ---- | ---- | +| label | string | Dataset label. Appears in the legend and tooltips. | +| xAxisID | string | The axis ID for the X axis. If not specified, the dataset will be rendered on the first available X axis. If an ID is specified, the dataset will be rendered on that axis | +| yAxisID | string | The axis ID for the Y axis. If not specified, the dataset will be rendered on the first available Y axis. If an ID is specified, the dataset will be rendered on that axis | +| backgroundColor | Color OR Color[] | The fill color under the line. | +| borderColor | Color OR Color[] | The color of the line. | +| borderWidth | number OR number[] | The width of the line. Measured in pixels. | +| borderDash | number[] | The length and spacing of dashes. Consist of an array of numbers that specify distances to alternately draw a line and a gap. If array length is odd, elements of the array will be repeated. If an empty array is provided, lines will be solid. | +| borderDashOffset | number | The distance to offset dashes. | +| borderCapStyle | `'butt'`
`'round'`
`'square'` | Specifies the end of the lines. Default is `'butt`'. | +| borderJoinStyle | `'bevel'`
`'round'`
`'miter'` | Determines the shape used to join two line segments where they meet. Default is `'miter'`. | +| cubicInterpolationMode | `'default'`
`'monotone'` | Determins which algorithm is used to interpolate a smooth curve between data points. | +| data | number[]
Point[] | The chart's data. Required. | +| fill | `false`
number
string
`'start'`
`'end'`
`'origin'` | Controls how the dataset's area is filled. | +| lineTension | number | Ttension of the Bezier curve line. `0` renders straight lines. Ignored if `cubicInterpolationMode` is set to `monotone`. | +| pointBackgroundColor | Color OR Color[] | The point's fill color. | +| pointBorderColor | Color OR Color[] | The point's border color. | +| pointBorderWidth | number OR number[] | The point's border width. | +| pointRadius | number OR number[] | The point's fill color. | +| pointStyle | `'circle'`
`'cross'`
`'crossRot'`
`'dash'`
`'line'`
`'rect'`
`'rectRounded'`
`'rectRot'`
`'star'`
`'triangle'`
HTMLImageElement
HTMLCanvasElement
HTMLImageElement[]
HTMLCanvasElement[] | Style of point. | +| pointRotation | number OR number[] | The point's roation, in degrees. | +| pointHitRadius | number OR number[] | The point's border width. | +| pointHoverBackgroundColor | Color OR Color[] | The point's background color when a mouse hovers over it. | +| pointHoverBorderColor | Color OR Color[] | The point's border color when a mouse hovers over it. | +| pointHoverBorderWidth | number OR number[] | The point's border width when a mouse hovers over it. | +| pointHoverRadius | number OR number[] | The point's radius width when a mouse hovers over it. | +| showLine | boolean | The point's radius width when a mouse hovers over it. | +| spanGaps | boolean | The point's radius width when a mouse hovers over it. | +| steppedLine | boolean
`'before'`
`'after'`| Determines whether the line is shown as a stepped line. Any value but `false` overrides the `lineTension` setting. | + +## Data Structure + +### number[] + +The `data` property of each dataset item consists of an array of numbers. Each point in the array corresponds to the matching label on the x axis: + +```TypeScript +data: [20, -10, 33, -47] +``` + +The chart elements will be rendered in the same order as found in the array. + +### Point[] + +You can also provide data elements with `x` and `y` coordinates: + +```TypeScript +data: [{ + x: 10, + y: 20 +}, { + x: 15, + y: 10 +}] + +``` + +#### Point Configuration + +Point elements can be configured to change their appearance using the following configuration options: + +| Name | Type | Default | Description | +| ---- | ---- | ---- | ---- | +| radius | number | 3 | Point radius. | +| pointStyle | `'circle'`
`'cross'`
`'crossRot'`
`'dash'`
`'line'`
`'rect'`
`'rectRounded'`
`'rectRot'`
`'star'`
`'triangle'` | `'circle'` | Style of point. | +| rotation | number | 0 | Rotation of the point, in degrees. | +| backgroundColor | Color | `'rgba(0,0,0,0.1)` | Fill color. | +| borderWidth | number | 1 | Stroke width. | +| borderColor | Color | `'rgba(0,0,0,0.1)` | Stroke color. | +| hitRadius | number | 1 | Extra radius added around the point to make it easier to detect mouse events. | +| hoverRadius | number | 4 | Point radius, when mouse hovers over point. | +| hoverBorderWidth | number | 1 | Stroke width, when mouse hovers over point. | + +## Configuration + +The following configuration options are specific to line charts: + +| Name | Type | Default | Description | +| ---- | ---- | ---- | ---- | +| showLines | boolean | true | Indicates whether a line will be drawn between each data point. A value of `false` will not render lines. | +| spanGaps | boolean | false | Indicates whether invalid number values (`NaN`) will cause a break in the line . A value of `false` will not span data gaps and cause a break in the line. | + +## For More Information + +For more information on what options are available with Line charts, refer to the [Line Chart documentation](https://www.chartjs.org/docs/latest/charts/line.html) on [Chart.js](https://www.chartjs.org). diff --git a/docs/documentation/docs/controls/charts/PieChart.md b/docs/documentation/docs/controls/charts/PieChart.md new file mode 100644 index 000000000..8215eab0f --- /dev/null +++ b/docs/documentation/docs/controls/charts/PieChart.md @@ -0,0 +1,179 @@ +# ChartControl - Pie Chart + +Pie charts are divided into segments, each of which shows the proportional value of the data. + +![Default Pie Chart](../../assets/PieChart.png) + +## Example Usage + +To create a pie chart, add the ChartControl import: + +```TypeScript +import { ChartControl, ChartType } from '@pnp/spfx-controls-react/lib/ChartControl'; +``` + +Then render the ChartControl: + +```TypeScript + +``` + +For example, to render the chart above, use the following code: + +```TypeScript +// set the data + const data: Chart.ChartData = { + labels: + [ + 'January', 'February', 'March', 'April', 'May', 'June', 'July' + ], + datasets: [ + { + label: 'My First Dataset', + data: + [ + 10, 50, 20, 60, 30, 70, 40 + ] + } + ] +}; + +// set the options +const options: Chart.ChartOptions = { + legend: { + display: true, + position: "left" + }, + title: { + display: true, + text: "My First Pie" + } +}; + +return ( + ); + +``` + +## Variations + +### Half-Moon Pie charts + +![Half-Moon Pie Chart](../../assets/PieChartHalfMoon.png) + +By default, pie charts (and doughnut charts) render a whole circle. You can change the chart's `circumference` option to render partial circles. + +The default `circumference` value is `2 * Math.PI`. To render a half-moon, specify a half value (i.e.: `Math.PI`), as follows: + +```TypeScript + const options: Chart.ChartOptions = { + circumference: Math.PI, + legend: { + display: true, + position: "left" + }, + title: { + display: true, + text: "My First Pie" + } + }; +``` + +Which renders the following half-moon: + +![Sideway Half-Moon Pie Chart](../../assets/PieChartHalfMoonSideway.png) + +To rotate the pie chart 90 degrees to the left, specify a `rotation` value in the chart's options. For example, to render the horizontal half-moon chart shown at the top of this section, use the following options: + +```TypeScript + const options: Chart.ChartOptions = { + circumference: Math.PI, + rotation: -1 * Math.PI, + legend: { + display: true, + position: "left" + }, + title: { + display: true, + text: "My First Pie" + } + }; +``` + +### Doughnut charts + +Technically, doughnut charts and pie charts are derived from the same class in [Chart.js](https://github.com/), where a doughnut chart's `cutoutPercentage` is set to 50. + +If you wish to render simple doughnut charts, use the [Doughnut Chart type](./DoughnutChart.md). + +However, if you wish to customize how the pie/doughtnut chart is rendered, you can set the cutout percentage to a different value. + +For example, you can use the following code to render a custom "fuel-gauge" chart: + +```TypeScript + const options: Chart.ChartOptions = { + circumference: 1 * Math.PI, + rotation: 1 * Math.PI, + cutoutPercentage: 60, + legend: { + display: true, + position: "left" + }, + title: { + display: true, + text: "My First Pie" + } + }; +``` + +Will produce the following chart: + +![Vroom vroom!](../../assets/PieChartFuelGage.png) + +## Dataset Properties + +Pie charts allow each dataset to have different configuration properties. + +Properties are provided as arrays. Settings in the array will be applied to each data element in the same order (e.g.: first value applies to first element, second value to second element, etc.) + +| Name | Type | Description | +| ---- | ---- | ---- | +| backgroundColor | Color[] | The segment's fill color. | +| borderColor | Color[] | The segment's border color. | +| borderWidth | number[] | The segment's border width. Measured in pixels. | +| data| number[] | The chart's data. Required. | +| hoverBackgroundColor | Color[] | The segment's fill color when a mouse hovers over it | +| hoverBorderColor | Color[] | The segment's border color when a mouse hovers over it. | +| hoverBorderWidth | number[] | The segment's border width when a mouse hovers over it. | + +## Data Structure + +The `data` property of each dataset item consists of an array of numbers. Each point in the array corresponds to the matching label on the x axis: + +```TypeScript +data: [20, 10, 33, 47] +``` + +## Configuration + +The following configuration options are specific to pie charts: + +| Name | Type | Default | Description | +| ---- | ---- | ---- | ---- | +| cutoutPercentage | number | 50 | The percentage of the chart that is cut out of the middle. | +| rotation | number | -0.5 * Math.PI | The angle at which the pie segments start | +| circumference | number | 2 * Math.PI | The total circumference of the donut chart. | +| animation.animateRotate | boolean | true | `true` will animate the chart while rotating it. | +| animation.animateScale | boolean | false | `true` will animate the chart while scaling it. | + +## For More Information + +For more information on what options are available with Pie charts, refer to the [Doughtnut and Pie documentation](https://www.chartjs.org/docs/latest/charts/doughnut.html) on [Chart.js](https://www.chartjs.org). diff --git a/docs/documentation/docs/controls/charts/PolarAreaChart.md b/docs/documentation/docs/controls/charts/PolarAreaChart.md new file mode 100644 index 000000000..63e9dd95c --- /dev/null +++ b/docs/documentation/docs/controls/charts/PolarAreaChart.md @@ -0,0 +1,101 @@ +# ChartControl - Polar Chart + +Polar charts are similar to pie charts, except that each segment has the same angle, and the radius of each segment differs depending on the value. + +![Default Polar Chart](../../assets/PolarAreaChart.png) + +## Example Usage + +To create a polar area chart, add the ChartControl import: + +```TypeScript +import { ChartControl, ChartType } from '@pnp/spfx-controls-react/lib/ChartControl'; +``` + +Then render the ChartControl: + +```TypeScript + +``` + +For example, to render the chart above, use the following code: + +```TypeScript +// set the data +const data: Chart.ChartData = { + labels: + [ + 'January', 'February', 'March', 'April', 'May', 'June', 'July' + ], + datasets: [ + { + label: 'My First Dataset', + data: + [ + 10, 50, 20, 60, 30, 70, 40 + ] + } + ] +}; + +// set the options +const options: Chart.ChartOptions = { + legend: { + display: true, + position: "left" + }, + title: { + display: true, + text: "My First Polar" + } +}; + +return ( + ); +``` + +## Dataset Properties + +Polar area charts allow each dataset to have different configuration properties. + +Properties are provided as arrays. The settings in the array will be applied to each data element in the same order (e.g.: first value applies to first element, second value to second element, etc.) + +| Name | Type | Description | +| ---- | ---- | ---- | +| backgroundColor | Color[] | The segment's fill color. | +| borderColor | Color[] | The segment's border color. | +| borderWidth | number[] | The segment's border width. Measured in pixels. | +| data | number[] | The chart's data. Required. | +| hoverBackgroundColor | Color[] | The segment's fill color when a mouse hovers over it | +| hoverBorderColor | Color[] | The segment's border color when a mouse hovers over it. | +| hoverBorderWidth | number[] | The segment's border width when a mouse hovers over it. | + +## Data Structure + +The `data` property of each dataset item consists of an array of numbers. Each point in the array corresponds to the matching label on the x axis: + +```TypeScript +data: [20, 10, 33, 47] +``` + +## Configuration + +The following configuration options are specific to polar area charts: + +| Name | Type | Default | Description | +| ---- | ---- | ---- | ---- | +| rotation | number | -0.5 * Math.PI | The angle at which the polar segments start | +| animation.animateRotate | boolean | true | `true` will animate the chart while rotating it. | +| animation.animateScale | boolean | false | `true` will animate the chart while scaling it. | + +## For More Information + +For more information on what options are available with Polar Area charts, refer to the [Polar Area documentation](https://www.chartjs.org/docs/latest/charts/polar.html) on [Chart.js](https://www.chartjs.org). diff --git a/docs/documentation/docs/controls/charts/RadarChart.md b/docs/documentation/docs/controls/charts/RadarChart.md new file mode 100644 index 000000000..cdb17babc --- /dev/null +++ b/docs/documentation/docs/controls/charts/RadarChart.md @@ -0,0 +1,121 @@ +# ChartControl - Radar Chart + +Radar charts are best used when comparing points of two or more datasets. + +![Default Radar Chart](../../assets/RadarChart.png) + +## Example Usage + +To create a radar chart, add the ChartControl import: + +```TypeScript +import { ChartControl, ChartType } from '@pnp/spfx-controls-react/lib/ChartControl'; +``` + +Then render the ChartControl: + +```TypeScript + +``` + +For example, to render the chart above, use the following code: + +```TypeScript +// set the data +const data: Chart.ChartData = { + labels: + ["Eating", "Drinking", "Sleeping", "Designing", "Coding", "Cycling", "Running"], + datasets: [ + // It is better to use multiple datasets for radar charts + { + label: "My First Dataset", + data: [65, 59, 90, 81, 56, 55, 40], + fill: true, + // semi-transparent colors are best + backgroundColor: "rgba(255, 99, 132, 0.2)", + borderColor: "rgb(255, 99, 132)", + pointBackgroundColor: "rgb(255, 99, 132)", + pointBorderColor: "#fff", + pointHoverBackgroundColor: "#fff", + pointHoverBorderColor: "rgb(255, 99, 132)" + }, + { + label: "My Second Dataset", + data: [28, 48, 40, 19, 96, 27, 100], + fill: true, + backgroundColor: "rgba(54, 162, 235, 0.2)", + borderColor: "rgb(54, 162, 235)", + pointBackgroundColor: "rgb(54, 162, 235)", + pointBorderColor: "#fff", + pointHoverBackgroundColor: "#fff", + pointHoverBorderColor: "rgb(54, 162, 235)" + }] +}; + +// set the options +const options: Chart.ChartOptions = { + // Using an aspect ratio of 1 will render a square chart area + aspectRatio: 1, + elements: { + line: { + tension: 0, + borderWidth: 3 + } + } +}; + +// render the chart +return ( + ); +``` + +## Dataset Properties + +Radar charts allow each dataset to have different configuration properties. + +Some properties can be provided as arrays. When arrays are provided, the settings in the array will be applied to each data element in the same order (e.g.: first value applies to first element, second value to second element, etc.) + +| Name | Type | Description | +| ---- | ---- | ---- | +| label | string | Dataset label. Appears in the legend and tooltips. | +| backgroundColor | Color OR Color[] | The fill color under the line. | +| borderColor | Color OR Color[] | The color of the line. | +| borderWidth | number OR number[] | The width of the line. Measured in pixels. | +| borderDash | number[] | The length and spacing of dashes. Consist of an array of numbers that specify distances to alternately draw a line and a gap. If array length is odd, elements of the array will be repeated. If an empty array is provided, lines will be solid. | +| borderDashOffset | number | The distance to offset dashes. | +| borderCapStyle | `'butt'`
`'round'`
`'square'` | Specifies the end of the lines. Default is `'butt`'. | +| borderJoinStyle | `'bevel'`
`'round'`
`'miter'` | Determines the shape used to join two line segments where they meet. Default is `'miter'`. | +| data | number[] | The chart's data. Required. | +| fill | `false`
number
string
`'start'`
`'end'`
`'origin'` | Controls how the dataset's area is filled. | +| lineTension | number | Ttension of the Bezier curve line. `0` renders straight lines. Ignored if `cubicInterpolationMode` is set to `monotone`. | +| pointBackgroundColor | Color OR Color[] | The point's fill color. | +| pointBorderColor | Color OR Color[] | The point's border color. | +| pointBorderWidth | number OR number[] | The point's border width. | +| pointRadius | number OR number[] | The point's fill color. | +| pointStyle | `'circle'`
`'cross'`
`'crossRot'`
`'dash'`
`'line'`
`'rect'`
`'rectRounded'`
`'rectRot'`
`'star'`
`'triangle'`
HTMLImageElement
HTMLCanvasElement
HTMLImageElement[]
HTMLCanvasElement[] | Style of point. | +| pointRotation | number OR number[] | The point's roation, in degrees. | +| pointHitRadius | number OR number[] | The point's border width. | +| pointHoverBackgroundColor | Color OR Color[] | The point's background color when a mouse hovers over it. | +| pointHoverBorderColor | Color OR Color[] | The point's border color when a mouse hovers over it. | +| pointHoverBorderWidth | number OR number[] | The point's border width when a mouse hovers over it. | +| pointHoverRadius | number OR number[] | The point's radius width when a mouse hovers over it. | + +## Data Structure + +The `data` property of each dataset item consists of an array of numbers. Each point in the array corresponds to the matching label on the x axis: + +```TypeScript +data: [20, 10, 33, 47] +``` + +## For More Information + +For more information on what options are available with Radar charts, refer to the [Radar documentation](https://www.chartjs.org/docs/latest/charts/radar.html) on [Chart.js](https://www.chartjs.org). diff --git a/docs/documentation/docs/controls/charts/ScatterChart.md b/docs/documentation/docs/controls/charts/ScatterChart.md new file mode 100644 index 000000000..f17c0c529 --- /dev/null +++ b/docs/documentation/docs/controls/charts/ScatterChart.md @@ -0,0 +1,231 @@ +# ChartControl - Scatter Chart + +Scatter charts are similar to line charts, except that the X axis (the horizontal axis) uses data values. + +Unlike other charts, scatter charts use `x` and `y` coordinates. + +![Default Scatter Chart](../../assets/ScatterChart.png) + +## Example Usage + +To create a scatter chart, add the ChartControl import: + +```TypeScript +import { ChartControl, ChartType } from '@pnp/spfx-controls-react/lib/ChartControl'; +``` + +Then render the ChartControl: + +```TypeScript + +``` + +For example, to render the chart above, use the following code: + +```TypeScript +// set the data +const data: Chart.ChartData = { + datasets: [{ + label: 'Scatter Dataset', + data: [ + { + x: -10, + y: 0 + }, + { + x: 0, + y: 10 + }, + { + x: 6, + y: 4 + }, + { + x: 2, + y: 6 + }, + { + x: -4, + y: 7 + }, + { + x: -8, + y: 5 + }, + { + x: 10, + y: 5 + }] + }] +}; + +// set the options +const options: Chart.ChartOptions = { + scales: { + xAxes: [{ + type: 'linear', + position: 'bottom' + }] + } +}; + +return ( + ); +``` +## Dataset Properties + +Scatter charts allow each dataset to have different configuration properties. + +Some properties can be provided as arrays. When arrays are provided, the settings in the array will be applied to each data element in the same order (e.g.: first value applies to first element, second value to second element, etc.) + +| Name | Type | Description | +| ---- | ---- | ---- | +| label | string | Dataset label. Appears in the legend and tooltips. | +| xAxisID | string | The axis ID for the X axis. If not specified, the dataset will be rendered on the first available X axis. If an ID is specified, the dataset will be rendered on that axis | +| yAxisID | string | The axis ID for the Y axis. If not specified, the dataset will be rendered on the first available Y axis. If an ID is specified, the dataset will be rendered on that axis | +| backgroundColor | Color OR Color[] | The fill color under the line. | +| borderColor | Color OR Color[] | The color of the line. | +| borderWidth | number OR number[] | The width of the line. Measured in pixels. | +| borderDash | number[] | The length and spacing of dashes. Consist of an array of numbers that specify distances to alternately draw a line and a gap. If array length is odd, elements of the array will be repeated. If an empty array is provided, lines will be solid. | +| borderDashOffset | number | The distance to offset dashes. | +| borderCapStyle | `'butt'`
`'round'`
`'square'` | Specifies the end of the lines. Default is `'butt`'. | +| borderJoinStyle | `'bevel'`
`'round'`
`'miter'` | Determines the shape used to join two line segments where they meet. Default is `'miter'`. | +| cubicInterpolationMode | `'default'`
`'monotone'` | Determins which algorithm is used to interpolate a smooth curve between data points. | +| data | Point[] | The chart's data. Required. | +| fill | `false`
number
string
`'start'`
`'end'`
`'origin'` | Controls how the dataset's area is filled. | +| lineTension | number | Ttension of the Bezier curve line. `0` renders straight lines. Ignored if `cubicInterpolationMode` is set to `monotone`. | +| pointBackgroundColor | Color OR Color[] | The point's fill color. | +| pointBorderColor | Color OR Color[] | The point's border color. | +| pointBorderWidth | number OR number[] | The point's border width. | +| pointRadius | number OR number[] | The point's fill color. | +| pointStyle | `'circle'`
`'cross'`
`'crossRot'`
`'dash'`
`'line'`
`'rect'`
`'rectRounded'`
`'rectRot'`
`'star'`
`'triangle'`
HTMLImageElement
HTMLCanvasElement
HTMLImageElement[]
HTMLCanvasElement[] | Style of point. | +| pointRotation | number OR number[] | The point's roation, in degrees. | +| pointHitRadius | number OR number[] | The point's border width. | +| pointHoverBackgroundColor | Color OR Color[] | The point's background color when a mouse hovers over it. | +| pointHoverBorderColor | Color OR Color[] | The point's border color when a mouse hovers over it. | +| pointHoverBorderWidth | number OR number[] | The point's border width when a mouse hovers over it. | +| pointHoverRadius | number OR number[] | The point's radius width when a mouse hovers over it. | +| showLine | boolean | The point's radius width when a mouse hovers over it. | +| spanGaps | boolean | The point's radius width when a mouse hovers over it. | +| steppedLine | boolean
`'before'`
`'after'`| Determines whether the line is shown as a stepped line. Any value but `false` overrides the `lineTension` setting. | + + +## Data Structure + +The `data` property of each dataset item consists of an array of points. Each point in the array consist of an `x` and `y` coordinate. + +```TypeScript +data: +[{ + x: 10, + y: 20 +}, { + x: 15, + y: 10 +}] +``` + +#### Point Configuration + +Point elements can be configured to change their appearance using the following configuration options: + +| Name | Type | Default | Description | +| ---- | ---- | ---- | ---- | +| radius | number | 3 | Point radius. | +| pointStyle | `'circle'`
`'cross'`
`'crossRot'`
`'dash'`
`'line'`
`'rect'`
`'rectRounded'`
`'rectRot'`
`'star'`
`'triangle'` | `'circle'` | Style of point. | +| rotation | number | 0 | Rotation of the point, in degrees. | +| backgroundColor | Color | `'rgba(0,0,0,0.1)` | Fill color. | +| borderWidth | number | 1 | Stroke width. | +| borderColor | Color | `'rgba(0,0,0,0.1)` | Stroke color. | +| hitRadius | number | 1 | Extra radius added around the point to make it easier to detect mouse events. | +| hoverRadius | number | 4 | Point radius, when mouse hovers over point. | +| hoverBorderWidth | number | 1 | Stroke width, when mouse hovers over point. | + +You can change the point configuration in the chart via the `options.elements.point` configuration. + +![Scatter with Point Styles](../../assets/ScatterPointStyle.png) + +For example, to render the above chart, use the following code: + +```TypeScript + const options: Chart.ChartOptions = { + elements: { + point: { + pointStyle: "triangle", + radius: 10, + hoverRadius: 15 + } + }, + scales: { + xAxes: [{ + type: 'linear', + position: 'bottom' + }] + } + }; +``` + +You can also control point configurations at the dataset level. + ```TypeScript + const data: Chart.ChartData = { + datasets: [{ + label: 'Triangle', + data: [ + { + x: -10, + y: 0 + }, + { + x: 0, + y: 10 + }], + pointStyle: "triangle", + backgroundColor: 'red' + }, + { + label: 'Rectangle', + data: [ + { + x: -6, + y: 6 + }, + { + x: -4, + y: 4 + }], + pointStyle: "rectRounded", + backgroundColor: 'green' + }, + { + label: 'Circle', + data: [ + { + x: 2, + y: 6 + }, + { + x: 8, + y: 2 + } + ], + pointStyle: "circle", + backgroundColor: 'blue' + } + ] + }; + ``` + + Which renders the following chart: + ![Dataset Styles](../../assets/ScatterDatasetStyle.png) + +## For More Information + +For more information on what options are available with Scatter charts, refer to the [Scatter documentation](https://www.chartjs.org/docs/latest/charts/scatter.html) on [Chart.js](https://www.chartjs.org). diff --git a/docs/documentation/mkdocs.yml b/docs/documentation/mkdocs.yml index 861e8bef5..c90672681 100644 --- a/docs/documentation/mkdocs.yml +++ b/docs/documentation/mkdocs.yml @@ -2,10 +2,22 @@ site_name: '@pnp/spfx-controls-react' nav: - Home: 'index.md' - Controls: + - Charts: + - "ChartControl": 'controls/ChartControl.md' + - "Bar Chart": 'controls/charts/BarChart.md' + - "Bubble Chart": 'controls/charts/BubbleChart.md' + - "Doughnut Chart": 'controls/charts/DoughnutChart.md' + - "Line Chart": 'controls/charts/LineChart.md' + - "Pie Chart": 'controls/charts/PieChart.md' + - "Polar Area Chart": 'controls/charts/PolarAreaChart.md' + - "Radar Chart": 'controls/charts/RadarChart.md' + - "Scatter Chart": 'controls/charts/ScatterChart.md' - FileTypeIcon: 'controls/FileTypeIcon.md' + - ListItemPicker: 'controls/ListItemPicker.md' - ListPicker: 'controls/ListPicker.md' - ListView: 'controls/ListView.md' - "ListView: add a contextual menu": 'controls/ListView.ContextualMenu.md' + - Map: 'controls/Map.md' - Placeholder: 'controls/Placeholder.md' - SiteBreadcrumb: 'controls/SiteBreadcrumb.md' - WebPartTitle: 'controls/WebPartTitle.md' diff --git a/package-lock.json b/package-lock.json index 7037c03c9..e97c55099 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@pnp/spfx-controls-react", - "version": "1.10.0", + "version": "1.11.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1400,6 +1400,11 @@ "chalk": "*" } }, + "@types/chart.js": { + "version": "2.7.40", + "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.7.40.tgz", + "integrity": "sha512-yC8Ff5vsHFTClGCWXoAmNCh33cNYfP2/yFANBLjLiso4jTKsLfQ0KQuBEuKxOWTRoOSLyT6v+ZYcvz0uonvvsA==" + }, "@types/cheerio": { "version": "0.22.7", "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.7.tgz", @@ -3273,6 +3278,39 @@ "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", "dev": true }, + "chart.js": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.7.3.tgz", + "integrity": "sha512-3+7k/DbR92m6BsMUYP6M0dMsMVZpMnwkUyNSAbqolHKsbIzH2Q4LWVEHHYq7v0fmEV8whXE0DrjANulw9j2K5g==", + "requires": { + "chartjs-color": "^2.1.0", + "moment": "^2.10.2" + } + }, + "chartjs-color": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.2.0.tgz", + "integrity": "sha1-hKL7dVeH7YXDndbdjHsdiEKbrq4=", + "requires": { + "chartjs-color-string": "^0.5.0", + "color-convert": "^0.5.3" + }, + "dependencies": { + "color-convert": { + "version": "0.5.3", + "resolved": "http://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", + "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=" + } + } + }, + "chartjs-color-string": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.5.0.tgz", + "integrity": "sha512-amWNvCOXlOUYxZVDSa0YOab5K/lmEhbFNKI55PWc4mlv28BDzA7zaoQTGxSBgJMHIW+hGX8YUrvw/FH4LyhwSQ==", + "requires": { + "color-name": "^1.0.0" + } + }, "cheerio": { "version": "0.22.0", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", @@ -3727,21 +3765,18 @@ } }, "color": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz", - "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=", - "dev": true, + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.0.tgz", + "integrity": "sha512-CwyopLkuRYO5ei2EpzpIh6LqJMt6Mt+jZhO5VI5f/wJLZriXQE32/SSqzmrh+QB+AZT81Cj8yv+7zwToW8ahZg==", "requires": { - "clone": "^1.0.2", - "color-convert": "^1.3.0", - "color-string": "^0.3.0" + "color-convert": "^1.9.1", + "color-string": "^1.5.2" } }, "color-convert": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", - "dev": true, "requires": { "color-name": "^1.1.1" } @@ -3749,16 +3784,15 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "color-string": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", - "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", - "dev": true, + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", "requires": { - "color-name": "^1.0.0" + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" } }, "color-support": { @@ -3776,6 +3810,28 @@ "color": "^0.11.0", "css-color-names": "0.0.4", "has": "^1.0.1" + }, + "dependencies": { + "color": { + "version": "0.11.4", + "resolved": "http://registry.npmjs.org/color/-/color-0.11.4.tgz", + "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=", + "dev": true, + "requires": { + "clone": "^1.0.2", + "color-convert": "^1.3.0", + "color-string": "^0.3.0" + } + }, + "color-string": { + "version": "0.3.0", + "resolved": "http://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", + "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", + "dev": true, + "requires": { + "color-name": "^1.0.0" + } + } } }, "colors": { @@ -11391,6 +11447,11 @@ "integrity": "sha1-CbaYXDIYFhQDIeED593ktIdgkhw=", "dev": true }, + "moment": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" + }, "morgan": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.6.1.tgz", @@ -12210,17 +12271,40 @@ "dev": true }, "office-ui-fabric-react": { - "version": "5.120.0", - "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-5.120.0.tgz", - "integrity": "sha512-WUyEExfSROSn5XIQudmVpo8cXV2h7RVvGDZTFIbWAEwh2gyxQjHegzoGDLeoTWy0ulD6+RM3vonrQNE5c629AQ==", + "version": "5.131.0", + "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-5.131.0.tgz", + "integrity": "sha512-QOYu1uf92qhTTIlBAj8teKvRpCmpliRZjynYtgeeUbDm4C4GtXdb/O1rPNFsfT0PNtPC8dCNeQ7/CXjQenUkyw==", "requires": { "@microsoft/load-themed-styles": "^1.7.13", "@uifabric/icons": ">=5.8.0 <6.0.0", "@uifabric/merge-styles": ">=5.17.1 <6.0.0", - "@uifabric/styling": ">=5.32.0 <6.0.0", - "@uifabric/utilities": ">=5.34.1 <6.0.0", + "@uifabric/styling": ">=5.36.0 <6.0.0", + "@uifabric/utilities": ">=5.34.2 <6.0.0", "prop-types": "^15.5.10", "tslib": "^1.7.1" + }, + "dependencies": { + "@uifabric/styling": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/@uifabric/styling/-/styling-5.37.0.tgz", + "integrity": "sha512-3hC0itW/hWSD5J4uANzUKk8XVGWUNkU+VLjEjWsQ6i5lvwFGaanR6Qy0bTkZdFGqFWMXe91CkBHV7HnvEx7tCA==", + "requires": { + "@microsoft/load-themed-styles": "^1.7.13", + "@uifabric/merge-styles": ">=5.17.1 <6.0.0", + "@uifabric/utilities": ">=5.34.2 <6.0.0", + "tslib": "^1.7.1" + } + }, + "@uifabric/utilities": { + "version": "5.34.2", + "resolved": "https://registry.npmjs.org/@uifabric/utilities/-/utilities-5.34.2.tgz", + "integrity": "sha512-7LDHamnrKpY49S49Nzu1YMTuBtZIgTsQd9AuWTvXlUevD67ZyjSRnhCKlKVZHwe/Vi0jWLIodbup4p5IyRGWoQ==", + "requires": { + "@uifabric/merge-styles": ">=5.17.1 <6.0.0", + "prop-types": "^15.5.10", + "tslib": "^1.7.1" + } + } } }, "on-finished": { @@ -14579,6 +14663,21 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + } + } + }, "sinon": { "version": "1.17.7", "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.7.tgz", diff --git a/package.json b/package.json index cffff5e38..a60bd7b84 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@pnp/spfx-controls-react", "description": "Reusable React controls for SharePoint Framework solutions", - "version": "1.10.0", + "version": "1.11.0", "engines": { "node": ">=0.10.0" }, @@ -18,13 +18,16 @@ "sonarcloud:start": "gulp sonarqube --gulpfile gulpfile.sonarqube.js" }, "dependencies": { - "@pnp/common": "^1.0.1", - "@pnp/logging": "^1.0.1", - "@pnp/odata": "^1.0.1", - "@pnp/sp": "^1.0.1", + "@pnp/common": "1.0.1", + "@pnp/logging": "1.0.1", + "@pnp/odata": "1.0.1", + "@pnp/sp": "1.0.1", "@pnp/telemetry-js": "1.0.0", - "lodash": "^4.17.4", - "office-ui-fabric-react": "~5.120.0" + "@types/chart.js": "2.7.40", + "chart.js": "2.7.3", + "color": "^3.1.0", + "lodash": "4.17.4", + "office-ui-fabric-react": "5.131.0" }, "devDependencies": { "@microsoft/decorators": "~1.3.0", diff --git a/scripts/create-changelog.js b/scripts/create-changelog.js index c3a03fd2e..6cea55428 100644 --- a/scripts/create-changelog.js +++ b/scripts/create-changelog.js @@ -18,7 +18,7 @@ if (changelog.versions && changelog.versions.length > 0) { const typeChanges = entry.changes[changeName]; if (typeChanges.length > 0) { let name = changeName === "new" ? "new control(s)" : changeName; - markdown.push(`**${name.charAt(0).toUpperCase() + name.slice(1)}**`); + markdown.push(`### ${name.charAt(0).toUpperCase() + name.slice(1)}`); markdown.push(``); // Add each change text diff --git a/src/ChartControl.ts b/src/ChartControl.ts new file mode 100644 index 000000000..3a6f48699 --- /dev/null +++ b/src/ChartControl.ts @@ -0,0 +1 @@ +export * from './controls/chartControl'; diff --git a/src/Map.ts b/src/Map.ts new file mode 100644 index 000000000..87fd3842c --- /dev/null +++ b/src/Map.ts @@ -0,0 +1 @@ +export * from './controls/map'; diff --git a/src/common/telemetry/version.ts b/src/common/telemetry/version.ts index ab0e836b7..092d558bf 100644 --- a/src/common/telemetry/version.ts +++ b/src/common/telemetry/version.ts @@ -1 +1 @@ -export const version: string = "1.10.0"; \ No newline at end of file +export const version: string = "1.11.0"; \ No newline at end of file diff --git a/src/controls/chartControl/AccessibleChartTable.test.tsx b/src/controls/chartControl/AccessibleChartTable.test.tsx new file mode 100644 index 000000000..012b45f08 --- /dev/null +++ b/src/controls/chartControl/AccessibleChartTable.test.tsx @@ -0,0 +1,197 @@ +/// + +import * as React from 'react'; +import { expect } from 'chai'; +import { mount, ReactWrapper } from 'enzyme'; +import { AccessibleChartTable, ChartType } from './'; +import styles from './ChartControl.module.scss'; + +declare const sinon; + +describe('', () => { + let tableControl: ReactWrapper; + const dummyClass: string = "DummyClass"; + const dummyCaption: string = "Dummy Summary"; + const dummyTitle: string = "Dummy Title"; + const dummySummary: string = "Dummy Summary"; + const dummyLabels: string[] = ['Human', 'Chimp', 'Dolphin', 'Cat']; + const dummyXAxisLabel: string = "X"; + const dummyYAxisLabel: string = "Y"; + const dummyDatasetLabel: string = "Millions"; + const dummyDatasetData: number[] = [11000, 6200, 5800, 300]; + const dummyDatasetLabel2: string = "Minions"; + const dummyDatasetData2: number[] = [12000, 7200, 6800, 400]; + const dummyData: Chart.ChartData = { + labels: dummyLabels, + datasets: [ + { + label: dummyDatasetLabel, + data: dummyDatasetData + } + ] + }; + + const dummyDataNoLabels: Chart.ChartData = { + datasets: [ + { + label: dummyDatasetLabel, + data: dummyDatasetData + } + ] + }; + + const dummyOptions: Chart.ChartOptions = { + title: { + text: dummyTitle + }, + scales: { + xAxes: [ + { + scaleLabel: { + labelString: dummyXAxisLabel + } + } + ], + yAxes: [ + { + scaleLabel: { + labelString: dummyYAxisLabel + } + } + ] + } + }; + + const dummyMultisetData: Chart.ChartData = { + labels: dummyLabels, + datasets: [ + { + label: dummyDatasetLabel, + data: dummyDatasetData + }, + { + label: dummyDatasetLabel2, + data: dummyDatasetData2 + } + ] + }; + + + afterEach(() => { + tableControl.unmount(); + }); + + + it('Should render only one table', (done) => { + tableControl = mount(); + expect(tableControl.find(`div.${styles.accessibleTable}`)).to.have.length(1); + done(); + }); + + it('Should render with a custom className if one is provided', (done) => { + tableControl = mount(); + expect(tableControl.find(`div.${dummyClass}`)).to.have.length(1); + done(); + }); + + it('Should render a caption if one is provided', (done) => { + tableControl = mount(); + expect(tableControl.find(`div.${styles.accessibleTable} table caption`).text()).to.be.equal(dummyCaption); + done(); + }); + + it('Should render a caption if no caption is provided but a title is available', (done) => { + tableControl = mount(); + expect(tableControl.find(`div.${styles.accessibleTable} table caption`).text()).to.be.equal(dummyTitle); + done(); + }); + + it('Should prioritize the caption if both caption and title are provided', (done) => { + tableControl = mount(); + expect(tableControl.find(`div.${styles.accessibleTable} table caption`).text()).to.be.equal(dummyCaption); + done(); + }); + + it('Should render the same number of rows as there are data elements', (done) => { + tableControl = mount(); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`)).to.have.length(4); + done(); + }); + + it('Should render a table matching the data provided', (done) => { + tableControl = mount(); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(0).find('th').at(0).text()).to.equal('Human'); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(1).find('th').at(0).text()).to.equal('Chimp'); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(2).find('th').at(0).text()).to.equal('Dolphin'); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(3).find('th').at(0).text()).to.equal('Cat'); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(0).find('td').at(0).text()).to.equal('11000'); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(1).find('td').at(0).text()).to.equal('6200'); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(2).find('td').at(0).text()).to.equal('5800'); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(3).find('td').at(0).text()).to.equal('300'); + done(); + }); + + it('Should include a summary in the caption if one is provided', (done) => { + tableControl = mount(); + expect(tableControl.find(`div.${styles.accessibleTable} table caption br`)).to.have.length(1); + expect(tableControl.find(`div.${styles.accessibleTable} table caption span`)).to.have.length(1); + expect(tableControl.find(`div.${styles.accessibleTable} table caption span`).text()).to.be.equals(dummySummary); + done(); + }); + + it('Should include a summary in the caption if one is provided -- even if no title is provided', (done) => { + tableControl = mount(); + expect(tableControl.find(`div.${styles.accessibleTable} table caption br`)).to.have.length(0); + expect(tableControl.find(`div.${styles.accessibleTable} table caption span`)).to.have.length(1); + expect(tableControl.find(`div.${styles.accessibleTable} table caption span`).text()).to.be.equals(dummySummary); + done(); + }); + + it('Should do nothing if there are no data labels', (done) => { + tableControl = mount(); + expect(tableControl.find(`div.${styles.accessibleTable} table`)).to.have.length(0); + done(); + }); + + it('Should render an X and Y label if axis labels are provided', (done) => { + tableControl = mount(); + expect(tableControl.find(`div.${styles.accessibleTable} table thead tr`).at(0).find('th').at(0).text()).to.be.equal(""); + expect(tableControl.find(`div.${styles.accessibleTable} table thead tr`).at(0).find('th')).to.have.length(2); + expect(tableControl.find(`div.${styles.accessibleTable} table thead tr`).at(0).find('th').at(1).text()).to.be.equal(dummyYAxisLabel); + expect(tableControl.find(`div.${styles.accessibleTable} table thead tr`).at(1).find('th')).to.have.length(2); + + expect(tableControl.find(`div.${styles.accessibleTable} table thead tr`).at(1).find('th').at(1).text()).to.be.equal(dummyDatasetLabel); + expect(tableControl.find(`div.${styles.accessibleTable} table thead tr`).at(1).find('th').at(0).text()).to.be.equal(dummyXAxisLabel); + done(); + }); + + it('Should render multi dataset labels', (done) => { + tableControl = mount(); + expect(tableControl.find(`div.${styles.accessibleTable} table thead tr`).at(0).find('th').at(0).text()).to.be.equal(""); + expect(tableControl.find(`div.${styles.accessibleTable} table thead tr`).at(0).find('th')).to.have.length(2); + expect(tableControl.find(`div.${styles.accessibleTable} table thead tr`).at(0).find('th').at(1).text()).to.be.equal(dummyYAxisLabel); + expect(tableControl.find(`div.${styles.accessibleTable} table thead tr`).at(1).find('th')).to.have.length(3); + + expect(tableControl.find(`div.${styles.accessibleTable} table thead tr`).at(1).find('th').at(0).text()).to.be.equal(dummyXAxisLabel); + expect(tableControl.find(`div.${styles.accessibleTable} table thead tr`).at(1).find('th').at(1).text()).to.be.equal(dummyDatasetLabel); + expect(tableControl.find(`div.${styles.accessibleTable} table thead tr`).at(1).find('th').at(2).text()).to.be.equal(dummyDatasetLabel2); + done(); + }); + + it('Should render a multi dataset table matching the data provided', (done) => { + tableControl = mount(); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(0).find('th').at(0).text()).to.equal('Human'); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(1).find('th').at(0).text()).to.equal('Chimp'); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(2).find('th').at(0).text()).to.equal('Dolphin'); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(3).find('th').at(0).text()).to.equal('Cat'); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(0).find('td').at(0).text()).to.equal('11000'); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(1).find('td').at(0).text()).to.equal('6200'); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(2).find('td').at(0).text()).to.equal('5800'); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(3).find('td').at(0).text()).to.equal('300'); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(0).find('td').at(1).text()).to.equal('12000'); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(1).find('td').at(1).text()).to.equal('7200'); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(2).find('td').at(1).text()).to.equal('6800'); + expect(tableControl.find(`div.${styles.accessibleTable} table tbody tr`).at(3).find('td').at(1).text()).to.equal('400'); + done(); + }); +}); diff --git a/src/controls/chartControl/AccessibleChartTable.tsx b/src/controls/chartControl/AccessibleChartTable.tsx new file mode 100644 index 000000000..714208933 --- /dev/null +++ b/src/controls/chartControl/AccessibleChartTable.tsx @@ -0,0 +1,178 @@ +import * as React from 'react'; +import { IAccessibleChartTableState, IAccessibleChartTableProps } from './AccessibleChartTable.types'; +import styles from './ChartControl.module.scss'; +import { ChartDataSets } from 'chart.js'; +import { Guid } from '@microsoft/sp-core-library'; +import { css } from 'office-ui-fabric-react/lib/Utilities'; +import { escape } from '@microsoft/sp-lodash-subset'; + +export class AccessibleChartTable extends React.Component { + public render(): React.ReactElement { + const { + onRenderTable, + data + } = this.props; + + if (data === undefined || data.datasets === undefined || data.datasets.length < 1) { + // tslint:disable-next-line:no-null-keyword + return null; + } + + if (onRenderTable !== undefined) { + return ( +
+ {onRenderTable()} +
); + } + const tableBody: JSX.Element[] = this._renderTableBody(); + + return ( +
+ {tableBody && tableBody.length > 0 ? + + {this._renderCaption()} + + {this._renderTableHeader()} + + + {tableBody} + +
+ : undefined} +
+ ); + } + + /** + * Adds a caption to the top of the accessible table + */ + private _renderCaption(): JSX.Element { + const { summary } = this.props; + const title: string = this._getAccessibleTitle(); + const summaryElement: JSX.Element = summary && {escape(summary)}; + + + return title || summary ? + + {escape(title)} + { title && summaryElement &&
} + { summaryElement } + : undefined; + } + + /** + * Renders the table's headers for X and Y axes + */ + private _renderTableHeader(): JSX.Element[] { + const { chartOptions, + data } = this.props; + + const { + datasets + } = data; + + // See if there are labels; we'll need them for the headers + let hasLabels: boolean = true; + datasets.forEach((dataSet: ChartDataSets) => { + if (dataSet.label === undefined) { + hasLabels = false; + } + }); + + // If there are no labels, there is no need to render headers + if (!hasLabels) { + return undefined; + } + + // Get the Y Axis label + const yAxisLabel: string = chartOptions + && chartOptions.scales + && chartOptions.scales.yAxes + && chartOptions.scales.yAxes[0].scaleLabel + && chartOptions.scales.yAxes[0].scaleLabel.labelString; + + // Generate the Y header row + const yHeaderRow: JSX.Element = yAxisLabel + && + + {escape(yAxisLabel)} + ; + + // Get the X axis label + const xAxisLabel: string = + chartOptions + && chartOptions.scales + && chartOptions.scales.xAxes + && chartOptions.scales.xAxes[0].scaleLabel + && chartOptions.scales.xAxes[0].scaleLabel.labelString; + + // Generate the X asix table cells + const xHeaderCells: JSX.Element[] = datasets.map((dataSet: ChartDataSets) => { + return {escape(dataSet.label)}; + }); + + // Generate the X axis header row + const xHeaderRow: JSX.Element = + {escape(xAxisLabel)} + {xHeaderCells} + ; + + return [ + yHeaderRow, + xHeaderRow + ]; + } + + /** + * Renders an accessible table body with data from the chart + */ + private _renderTableBody(): JSX.Element[] { + const { + data + } = this.props; + + // The data must have matching labels to render + // otherwise this is pointless + return data.labels && data.labels.map((labelValue: string, rowIndex: number) => { + const cells: JSX.Element[] = data.datasets.map((dataSet: ChartDataSets, dsIndex: number) => { + return {dataSet.data[rowIndex]}; + }); + return + {escape(labelValue)} + {cells} + ; + }); + } + + /** + * Gets the caption for the table. + * If no caption, gets the title. + */ + private _getAccessibleTitle(): string { + const { + chartOptions, + caption + } = this.props; + + // Is there a caption? + if (caption !== undefined) { + // Let's use it! + return caption; + } + + // No caption. Look for the title + if (chartOptions && chartOptions.title && chartOptions.title.text) { + // ChartJs supports titles in a string array to make them multiline + if (chartOptions.title.text instanceof Array) { + // If we're using an array, join them into a single string + return chartOptions.title.text.join(' '); + } else { + // Return the title + return chartOptions.title.text; + } + } + + // If all else fails, no titles for you + return undefined; + } +} diff --git a/src/controls/chartControl/AccessibleChartTable.types.ts b/src/controls/chartControl/AccessibleChartTable.types.ts new file mode 100644 index 000000000..2b5dce86f --- /dev/null +++ b/src/controls/chartControl/AccessibleChartTable.types.ts @@ -0,0 +1,57 @@ +import { ChartData, ChartOptions } from 'chart.js'; +import { ChartType } from './ChartControl.types'; + +/** + * The properties for the Accessible Chart Table object + */ +export interface IAccessibleChartTableProps { + /** + * Provides a caption for the accessible table + * @default defaults to the chart's title if any + */ + caption?: string; + + /** + * Allows you to overwrite the default CSS class name + * of the accessible table + */ + className?: string; + + /** + * Provides a summary of the data + */ + summary?: string; + + /** + The data to be displayed in the chart + @type {IChartData} + */ + data?: ChartData; + + /** + The options for the chart + @type {ChartOptions} + */ + chartOptions?: ChartOptions & {}; + + /** + The type of chart to render + @type {ChartType} + */ + chartType: ChartType; + + /** The labels as provided by the chart */ + chartLabels?: string[]; + + /** + * Allows you to custom-render your own accessible table + */ + onRenderTable?: () => JSX.Element; +} + +/** + * The state of a chart table + */ +export interface IAccessibleChartTableState { + // nothing for now +} diff --git a/src/controls/chartControl/ChartColorPalettes.ts b/src/controls/chartControl/ChartColorPalettes.ts new file mode 100644 index 000000000..27b0c09b9 --- /dev/null +++ b/src/controls/chartControl/ChartColorPalettes.ts @@ -0,0 +1,221 @@ +/** + * Contains the color definition of Office charts. + * Default color set for all charts + * Use for backgroundColor in chart data set. + */ +export const OFFICE_COLORFUL1: string[] = [ + '#4472C4', + '#ED7D31', + '#A5A5A5', + '#FFC000', + '#5B9BD5', + '#70AD47', + '#264478', + '#9E480E', + '#636363', + '#997300', + '#255E91', + '#43682B', + '#698ED0', + '#F1975A', + '#B7B7B7', + '#FFCD33', + '#7CAFDD', + '#8CC168', + '#335AA1', + '#D26012', + '#848484', + '#CC9A00', + '#327DC2', + '#5A8A39', + '#8FAADC', + '#F4B183', + '#C9C9C9', + '#FFD966', + '#9DC3E6', + '#A9D18E', + '#203864', + '#843C0C', + '#525252', + '#7F6000', + '#1F4E79', + '#385723', + '#7C9CD6', + '#F2A46F', + '#C0C0C0', + '#FFD34D', + '#8CB9E2', + '#9AC97B', + '#2C4F8C', + '#B85410', + '#747474', + '#B38600', + '#2B6DA9', + '#4E7932', + '#A2B9E2', + '#F6BE98', + '#D2D2D2', + '#FFDF7F', + '#ADCDEA', + '#B7D8A1' +]; + +export const OFFICE_COLORFUL2: string[] = [ + '#4472C4', + '#A5A5A5', + '#5B9BD5', + '#264478', + '#636363', + '#255E91', + '#698ED0', + '#B7B7B7', + '#7CAFDD', + '#335AA1', + '#848484', + '#327DC2', + '#8FAADC', + '#C9C9C9', + '#9DC3E6', + '#203864', + '#525252', + '#1F4E79', + '#7C9CD6', + '#C0C0C0', + '#8CB9E2', + '#2C4F8C', + '#747474', + '#2B6DA9', + '#A2B9E2', + '#D2D2D2', + '#ADCDEA' +]; + +export const OFFICE_COLORFUL3: string[] = [ + '#ED7D31', + '#FFC000', + '#70AD47', + '#9E480E', + '#997300', + '#43682B', + '#F1975A', + '#FFCD33', + '#8CC168', + '#D26012', + '#CC9A00', + '#5A8A39', + '#F4B183', + '#FFD966', + '#A9D18E', + '#843C0C', + '#7F6000', + '#385723', + '#F2A46F', + '#FFD34D', + '#9AC97B', + '#B85410', + '#B38600', + '#4E7932', + '#F6BE98', + '#FFDF7F', + '#B7D8A1' +]; + +export const OFFICE_COLORFUL4: string[] = [ + '#70AD47', + '#5B9BD5', + '#FFC000', + '#43682B', + '#255E91', + '#997300', + '#8CC168', + '#7CAFDD', + '#FFCD33', + '#5A8A39', + '#327DC2', + '#CC9A00', + '#A9D18E', + '#9DC3E6', + '#FFD966', + '#385723', + '#1F4E79', + '#7F6000', + '#9AC97B', + '#8CB9E2', + '#FFD34D', + '#4E7932', + '#2B6DA9', + '#B38600', + '#B7D8A1', + '#ADCDEA', + '#FFDF7F' +]; + +export const OFFICE_MONOCHROMATIC1: string[] = [ + '#254275', + '#D8DDEE' +]; + +export const OFFICE_MONOCHROMATIC2: string[] = [ + '#8C4719', + '#F9E0D8' +]; + +export const OFFICE_MONOCHROMATIC3: string[] = [ + '#606060', + '#E7E7E7' +]; + +export const OFFICE_MONOCHROMATIC4: string[] = [ + '#977000', + '#FFEDD7' +]; + +export const OFFICE_MONOCHROMATIC5: string[] = [ + '#325A7D', + '#DCE5F3' +]; + +export const OFFICE_MONOCHROMATIC6: string[] = [ + '#3F6526', + '#DEE9DA' +]; + +export const OFFICE_MONOCHROMATIC7: string[] = [ + '#5F5F5F', + '#B3B3B3', + '#898989', + '#212121', + '#DADADA', + '#AAAAAA', + '#7C7C7C' +]; + +export const OFFICE_MONOCHROMATIC8: string[] = [ + '#D8DDEE', + '#254275' +]; + +export const OFFICE_MONOCHROMATIC9: string[] = [ + '#F9E0D8', + '#8C4719' +]; + +export const OFFICE_MONOCHROMATIC10: string[] = [ + '#E7E7E7', + '#606060' +]; + +export const OFFICE_MONOCHROMATIC11: string[] = [ + '#FFEDD7', + '#977000' +]; + +export const OFFICE_MONOCHROMATIC12: string[] = [ + '#DCE5F3', + '#325A7D' +]; + +export const OFFICE_MONOCHROMATIC13: string[] = [ + '#DEE9DA', + '#3F6526' +]; diff --git a/src/controls/chartControl/ChartControl.module.scss b/src/controls/chartControl/ChartControl.module.scss new file mode 100644 index 000000000..40eeb94f3 --- /dev/null +++ b/src/controls/chartControl/ChartControl.module.scss @@ -0,0 +1,48 @@ +@import '~office-ui-fabric-react/dist/sass/References.scss'; +//@import "~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss"; +// exporting all these variables allows us to pick-up +// these values from within the code +// (we're trying to avoid hard-coding styles in the code) +:export { + defaultFontColor: $ms-color-neutralSecondary; + defaultFontFamily: $ms-font-family-fallbacks; + defaultFontSize: $ms-font-size-m; + lineColor: $ms-color-neutralLighter; + titleColor: $ms-color-neutralSecondary; + titleFont: $ms-font-family-fallbacks; + titleFontSize: $ms-font-size-m; + legendColor: $ms-color-neutralTertiary; + legendFont: $ms-font-family-fallbacks; + legendFontSize: $ms-font-size-s; + tooltipFontSize: $ms-font-size-s; + tooltipFont: $ms-font-family-fallbacks; + tooltipBodyColor:$ms-color-white; + tooltipTitleFont: $ms-font-family-fallbacks; + tooltipTitleColor: $ms-color-white; + tooltipTitleFontSize: $ms-font-size-s; + tooltipFooterColor: $ms-color-white; + tooltipFooterFont: $ms-font-family-fallbacks; + tooltipFooterFontSize: $ms-font-size-s; + tooltipBackgroundColor: $ms-color-neutralPrimary; + tooltipBorderColor: $ms-color-neutralPrimary; + scaleFontColor: $ms-color-neutralSecondary; + scaleFont: $ms-font-family-fallbacks; + scaleFontSize: $ms-font-size-s; +} + +.chartComponent { + &.themed { + background-color: "[theme: bodyBackground, default: #ffffff]"; + } +} + +// Renders an invisible table to everyone but people who use a screen reader +.accessibleTable { + position: 'absolute'; + left: -10000; + top: 'auto'; + width: 1; + height: 1; + overflow: 'hidden'; + display: none; +} diff --git a/src/controls/chartControl/ChartControl.test.tsx b/src/controls/chartControl/ChartControl.test.tsx new file mode 100644 index 000000000..8f0c6de47 --- /dev/null +++ b/src/controls/chartControl/ChartControl.test.tsx @@ -0,0 +1,109 @@ +/// + +import * as React from 'react'; +import { assert, expect } from 'chai'; +import { mount, ReactWrapper } from 'enzyme'; +import { ChartControl } from './ChartControl'; +import styles from './ChartControl.module.scss'; + +declare const sinon; + + + +describe('', () => { + let chartControl: ReactWrapper; + const dummyTitle = "Dummy Title"; + const dummyClass = "DummyClass"; + const dummySummary = "Dummy Summary"; + const dummyData = { + labels: ['Human', 'Chimp', 'Dolphin', 'Cat'], + datasets: [ + { + label: 'Millions', + data: [ + 11000, 6200, 5800, 300 + ] + } + ] + }; + + afterEach(() => { + chartControl.unmount(); + }); + + it('Check that an accessible table gets created by default', (done) => { + chartControl = mount(); + expect(chartControl.find(`div.${styles.accessibleTable}`)).to.have.length(1); + done(); + }); + + it('Check that the accessible table accepts a custom classname', (done) => { + chartControl = mount(); + expect(chartControl.find(`div.customClass`)).to.have.length(1); + done(); + }); + + it('Check that the accessible table doesn\'t get rendered if disabled', (done) => { + chartControl = mount(); + expect(chartControl.find(`div.${styles.accessibleTable}`)).to.have.length(0); + done(); + }); + + it('Check that an accessible table gets created with the caption matching the title', (done) => { + chartControl = mount(); + expect(chartControl.find(`div.${styles.accessibleTable} table caption`).text()).to.be.equal(dummyTitle); + done(); + }); + + it('Check that an accessible table gets created with a caption', (done) => { + chartControl = mount(); + expect(chartControl.find(`div.${styles.accessibleTable} table caption`).text()).to.be.equal(dummyTitle); + done(); + }); + + it('Check that the accessible table has a number of rows matching the number of data elements', (done) => { + chartControl = mount(); + expect(chartControl.find(`div.${styles.accessibleTable} table tbody tr`)).to.have.length(4); + done(); + }); + + it('Check that the accessible table has only one header row', (done) => { + chartControl = mount(); + expect(chartControl.find(`div.${styles.accessibleTable} table thead tr`)).to.have.length(1); + done(); + }); + + it('Check that custom class gets rendered', (done) => { + chartControl = mount(); + expect(chartControl.find(`div.${dummyClass}`)).to.have.length(1); + done(); + }); + + it('Check that a canvas gets rendered', (done) => { + chartControl = mount(); + expect(chartControl.find(`div.${styles.chartComponent} canvas`)).to.have.length(1); + done(); + }); + + it('Check that it doesn\'t crash if data is omitted', (done) => { + chartControl = mount(); + expect(chartControl.find(`div.${styles.chartComponent} canvas`)).to.have.length(1); + expect(chartControl.find(`div.${styles.accessibleTable}`)).to.have.length(0); + done(); + }); + + it('Check that it applies a themed background by default', (done) => { + chartControl = mount(); + expect(chartControl.find(`div.${styles.chartComponent}.${styles.themed}`)).to.have.length(1); + done(); + }); + + it('Check that it disables themed background when useTheme is set to false', (done) => { + chartControl = mount(); + expect(chartControl.find(`div.${styles.chartComponent}.${styles.themed}`)).to.have.length(0); + done(); + }); + +}); + + diff --git a/src/controls/chartControl/ChartControl.tsx b/src/controls/chartControl/ChartControl.tsx new file mode 100644 index 000000000..b4348d82a --- /dev/null +++ b/src/controls/chartControl/ChartControl.tsx @@ -0,0 +1,491 @@ +import * as React from 'react'; +import { IChartControlState, IChartControlProps } from '.'; +import styles from './ChartControl.module.scss'; +import { css } from 'office-ui-fabric-react/lib/Utilities'; +import { Chart, ChartSize } from 'chart.js'; +import { PaletteGenerator } from './PaletteGenerator'; +import { AccessibleChartTable } from './AccessibleChartTable'; +import * as telemetry from '../../common/telemetry'; +import { ChartPalette } from './ChartControl.types'; + +interface Window { + __themeState__: any; +} + +declare var window: Window; + +export class ChartControl extends React.Component { + + /** + * Sets default properties + */ + public static defaultProps: Partial = { + // We want accessibility on by default + // -- it's the law in some countries!!! + accessibility: { + enable: true + }, + useTheme: true, + palette: ChartPalette.OfficeColorful1, + // Make charts responsive so that they fit within their + // parent elements + options: { + responsive: true, + maintainAspectRatio: true + } + }; + + /** + * The ChartJs instance + */ + private _chart: Chart; + + /** + * The canvas element that will host the chart + */ + private _canvasElem: HTMLCanvasElement = undefined; + + constructor(props: IChartControlProps) { + super(props); + + telemetry.track('ReactChartComponent', { + type: !!props.type, + className: !!props.className, + palette: !!props.palette, + accessibility: !!props.accessibility.enable + }); + + this.state = { + isLoading: false, + rejected: undefined, + data: undefined + }; + } + + /** + * componentDidMount lifecycle hook + */ + public componentDidMount(): void { + + if (this.props.datapromise) { + this._doPromise(this.props.datapromise); + } else { + this._initChart(this.props, this.props.data); + } + } + /** + * componentWillReceiveProps lifecycle hook + * + * @param nextProps + */ + public componentWillReceiveProps(nextProps: IChartControlProps): void { + if (nextProps.datapromise !== this.props.datapromise) { + this.setState({ + isLoading: false + }); + + this._doPromise(nextProps.datapromise); + } else { + this._destroyChart(); + this._initChart(nextProps, this.props.data); + } + } + + /** + * componentWillUnmount lifecycle hook + */ + public componentWillUnmount(): void { + this._destroyChart(); + } + + /** + * shouldComponentUpdate lifecycle hook + * + * @param nextProps + * @param nextState + */ + public shouldComponentUpdate(nextProps: IChartControlProps, nextState: IChartControlState): boolean { + const { data, + options, + plugins, + className, + accessibility, + useTheme, + palette } = this.props; + return data !== nextProps.data || + options !== nextProps.options || + plugins !== nextProps.plugins || + className !== nextProps.className || + useTheme !== nextProps.useTheme || + palette !== nextProps.palette || + accessibility !== nextProps.accessibility; + } + + /** + * Default React render method + */ + public render(): React.ReactElement { + const { + type, + accessibility, + useTheme, + options, + data + } = this.props; + + // If we're still loading, try to show the loading template + if (this.state.isLoading) { + if (this.props.loadingtemplate) { + if (typeof this.props.loadingtemplate === "function") { + return this.props.loadingtemplate(); + } else { + return this.props.loadingtemplate; + } + } + } + + // If promise was rejected, try to show the rejected template + if (this.state.rejected) { + if (this.props.rejectedtemplate) { + if (typeof this.props.rejectedtemplate === "function") { + return this.props.rejectedtemplate(this.state.rejected); + } else { + return this.props.rejectedtemplate; + } + } + } + + const alternateText: string = accessibility!.alternateText; + + return ( +
+ + { + accessibility.enable === undefined || accessibility.enable ? ( + + ) : null + } +
+ ); + } + + /** + * Triggers an update of the chart. + * This can be safely called after updating the data object. + * This will update all scales, legends, and then re-render the chart. + * @param config duration (number): Time for the animation of the redraw in milliseconds + * lazy (boolean): If true, the animation can be interrupted by other animations + * easing (string): The animation easing function. + */ + public update(config?: number | boolean | string): void { + this._chart.update(config); + } + + /** + * Triggers a redraw of all chart elements. + * Note, this does not update elements for new data. Use .update() in that case. + * @param config duration is the time for the animation of the redraw in milliseconds + lazy is a boolean. If true, the animation can be interrupted by other animations + */ + public renderChart(config: {}): void { + this._chart.render(config); + } + + /** + Use this to stop any current animation loop. + This will pause the chart during any current animation frame. + Call .render() to re-animate. + */ + public stop(): void { + this._chart.stop(); + } + + /** + Will clear the chart canvas. + Used extensively internally between animation frames, but you might find it useful. + */ + public clear(): void { + this._chart.clear(); + } + + /** + Returns a base 64 encoded string of the chart in it's current state. + @returns {string} A base-64 encoded PNG data URL containing image of the chart in its current state + */ + public toBase64Image(): string { + return this._chart.toBase64Image(); + } + + /** + Return the chartjs instance + */ + // tslint:disable-next-line no-any + public getChart(): Chart { + return this._chart; + } + + /** + Return the canvass element that contains the chart + @returns {HTMLCanvasElement} the canvas element containig the chart + */ + public getCanvas(): HTMLCanvasElement { + return this._chart.canvas; + } + + /** + Looks for the element under the event point, + then returns all elements from that dataset. + This is used internally for 'dataset' mode highlighting + * @param e An array of elements + */ + public getDatasetAtEvent(e: MouseEvent): Array<{}> { + return this.getChart().getDatasetAtEvent(e); + } + + /** + * Calling getElementAtEvent(event) on your Chart instance passing an argument of an event, + * or jQuery event, will return the single element at the event position. + * If there are multiple items within range, only the first is returned. + * The value returned from this method is an array with a single parameter. + * An array is used to keep a consistent API between the get*AtEvent methods. + * @param e the first element at the event point. + */ + public getElementAtEvent(e: MouseEvent): {} { + return this.getChart().getElementAtEvent(e); + } + + /** + Looks for the element under the event point, then returns all elements + at the same data index. This is used internally for 'label' mode highlighting. + Calling getElementsAtEvent(event) on your Chart instance passing an argument of an + event, or jQuery event, will return the point elements that are at that + the same position of that event. + * @param e + */ + public getElementsAtEvent(e: MouseEvent): Array<{}> { + return this.getChart().getElementsAtEvent(e); + } + + /** + * Initializes the chart + * @param props chart control properties + */ + private _initChart(props: IChartControlProps, data: Chart.ChartData): void { + const { + options, + type, + plugins, + useTheme + } = this.props; + + // add event handlers -- if they weren't already provided through options + if (this.props.onClick !== undefined) { + if (options.onClick === undefined) { + options.onClick = this.props.onClick; + } + } + + // Add onhover + if (this.props.onHover !== undefined) { + if (options.onHover === undefined) { + options.onHover = (event: MouseEvent, activeElements: {}[]) => { + this.props.onHover(this.getChart(), event, activeElements); + }; + } + } + + // Add onResize + // Note that onResize won't work unless the chart is + // position: relative and has a height and width defined + if (this.props.onResize !== undefined) { + if (options.onResize === undefined) { + options.onResize = (newSize: ChartSize) => { + this.props.onResize(this.getChart(), newSize); + }; + } + } + + this._applyDatasetPalette(data); + + if (useTheme) { + this._applyChartThemes(); + } + + this._chart = new Chart(this._canvasElem, { + type: type, + data: data, + options: options, + plugins: plugins + }); + } + + private _applyDatasetPalette(data: Chart.ChartData) { + try { + + // Get the dataset + let datasets: Chart.ChartDataSets[] = data.datasets; + + if (datasets !== undefined) { + datasets.forEach(dataset => { + if (dataset.backgroundColor === undefined) { + const datasetLength: number = dataset!.data!.length; + if (datasetLength) { + dataset.backgroundColor = PaletteGenerator.GetPalette(this.props.palette, datasetLength); + } + } + }); + } + } catch (error) { + + } + } + + private _applyChartThemes(): void { + try { + Chart.defaults.global.defaultFontColor = this._getThemeColor(styles.defaultFontColor); + Chart.defaults.global.defaultFontFamily = styles.defaultFontFamily; + Chart.defaults.global.defaultFontSize = this._getFontSizeNumber(styles.defaultFontSize); + Chart.defaults.global.title.fontColor = this._getThemeColor(styles.titleColor); + Chart.defaults.global.title.fontFamily = styles.titleFont; + Chart.defaults.global.title.fontSize = this._getFontSizeNumber(styles.titleFontSize); + Chart.defaults.global.legend.labels.fontColor = this._getThemeColor(styles.legendColor); + Chart.defaults.global.legend.labels.fontFamily = styles.legendFont; + Chart.defaults.global.legend.labels.fontSize = this._getFontSizeNumber(styles.legendFontSize); + Chart.defaults.global.tooltips.backgroundColor = this._getThemeColor(styles.tooltipBackgroundColor); + Chart.defaults.global.tooltips.bodyFontColor = this._getThemeColor(styles.tooltipBodyColor); + Chart.defaults.global.tooltips.bodyFontFamily = styles.tooltipFont; + Chart.defaults.global.tooltips.bodyFontSize = this._getFontSizeNumber(styles.tooltipFontSize); + Chart.defaults.global.tooltips.titleFontColor = this._getThemeColor(styles.tooltipTitleColor); + Chart.defaults.global.tooltips.titleFontFamily = styles.tooltipTitleFont; + Chart.defaults.global.tooltips.titleFontSize = this._getFontSizeNumber(styles.tooltipTitleFontSize); + Chart.defaults.global.tooltips.footerFontColor = this._getThemeColor(styles.tooltipFooterColor); + Chart.defaults.global.tooltips.footerFontFamily = styles.tooltipFooterFont; + Chart.defaults.global.tooltips.footerFontSize = this._getFontSizeNumber(styles.tooltipFooterFontSize); + Chart.defaults.global.tooltips.borderColor = this._getThemeColor(styles.tooltipBorderColor); + + if (Chart.defaults + && Chart.defaults.scale + && Chart.defaults.scale.gridLines + && Chart.defaults.scale.gridLines.color) { + Chart.defaults.scale.gridLines.color = this._getThemeColor(styles.lineColor); + } + } catch (error) { + + } + } + + private _destroyChart(): void { + try { + if (this._chart !== undefined) { + this._chart.destroy(); + } + } catch (error) { + + } + } + + private _linkCanvas = (e: HTMLCanvasElement) => { + this._canvasElem = e; + } + + private _getThemeColor(value: string): string { + try { + if (value.indexOf('theme:') > 0) { + // This value has a theme substitution + const themeParts: string[] = value.replace('[', '').replace(']', '').replace('"', '').split(','); + let defaultValue: string = undefined; + let themeValue: string = undefined; + + // Break the theme string into it's components + themeParts.forEach(themePart => { + if (themePart.indexOf('theme:') >= 0) { + themeValue = themePart.replace('theme:', ''); + } else if (themePart.indexOf('default:') >= 0) { + defaultValue = themePart.replace('default:', '').replace('"', '').trim(); + } + }); + + // If there is a theme value, try to read from environment + if (themeValue !== undefined) { + try { + // This should definitely be easier to do in SPFx! + + // tslint:disable-next-line + const themeStateVariable: any = window.__themeState__; + if (themeStateVariable === undefined) { + return defaultValue; + } + const themeState: {} = themeStateVariable.theme; + + if (themeState === undefined) { + return defaultValue; + } + + for (const varName in themeState) { + if (!themeState.hasOwnProperty(varName)) { + continue; + } + + // Cheesy cleanup of variables to remove extra quotes + if (varName === themeValue) { + return themeState[varName].replace('"', '').trim(); + } + } + } catch (error) { + // do nothing + } + + return defaultValue; + } + } + } catch (error) { + + } + + return value; + } + + // Reads one of the Office Fabric defined font sizes + // and converts to a number + private _getFontSizeNumber(value: string): number { + try { + return parseInt(value.replace('px', ''), 10); + } catch (error) { + return undefined; + } + } + + /** + * Gets the results of a promise and returns it to the chart + * @param promise + */ + private _doPromise(promise: Promise) { + this.setState({ + isLoading: true + }, () => { + promise.then( + results => { + this.setState({ + isLoading: false, + data: results + }, () => { + this._initChart(this.props, results); + }); + }, + rejected => { + this.setState({ + isLoading: false, + rejected: rejected + }); + } + ); + }); + } +} diff --git a/src/controls/chartControl/ChartControl.types.ts b/src/controls/chartControl/ChartControl.types.ts new file mode 100644 index 000000000..d2ae2d995 --- /dev/null +++ b/src/controls/chartControl/ChartControl.types.ts @@ -0,0 +1,339 @@ +/* +* Parameter descriptions are from https://www.chartjs.org/docs/latest, where possible. +*/ +import { + Chart, + ChartSize, + ChartData, + ChartOptions +} from 'chart.js'; + +/** + * The properties for the ChartComponent object + */ +export interface IChartControlProps { + /** + * Provides an accessible version of the chart for + * users with visual impairment + */ + accessibility?: IChartAccessibility; + + /** + The data to be displayed in the chart + @type {ChartData} + */ + data?: ChartData; + + /** + Promise to the data to be displayed in the chart. + ChartControl will automatically display data when + promise returns. + @type {Promise} + */ + datapromise?: Promise; + + /** + If using datapromises, sets the content to display while loading the data. + @type {JSX.Element | Function} + */ + loadingtemplate?: JSX.Element | Function; + + /** + If using datapromises, sets the content to display when a promise is rejected + @type {JSX.Element | Function} + */ + rejectedtemplate?: JSX.Element | Function; + + /** + The options for the chart + @type {ChartOptions} + */ + options?: ChartOptions; + + /** + The type of chart to render + @type {ChartType} + */ + type: ChartType; + + /** + The custom CSS classname + @type {string} + */ + className?: string; + + /** + * Specifies one of the Office color palettes. + * If the background color is set in the datasets, this option will be overwritten. + */ + palette?: ChartPalette; + + /** + Plugins are the most efficient way to customize or change the default behavior of a chart. + They have been introduced in chart.js version 2.1.0 (global plugins only) and extended + in version 2.5.0 (per chart plugins and options). + @type {object[]} an array of plugins + */ + plugins?: object[]; + + /** + * Enables or disables the chart control's ability to detect the environment themes. + * @default true + */ + useTheme?: boolean; + + /** + * Called if the event is of type 'mouseup' or 'click'. + * Called in the context of the chart and passed the event and an array of active elements. + * If onClick is defined in the chart options, this callback will be ignored. + * @param event + * @param activeElements + */ + onClick?(event?: MouseEvent, activeElements?: Array<{}>): void; + + /** + * Called when any of the events fire. + * Called in the context of the chart and passed the event and an array of active elements (bars, points, etc). + * If onHover is defined in the chart options, this callback will be ignored + * @param chart @type {IChartJs} + * @param event @type {MouseEvent} + * @param activeElements @type {Array<{}>} + */ + onHover?(chart: Chart, event: MouseEvent, activeElements: Array<{}>): void; + + /** + * Called when a resize occurs. Gets passed two arguments: the chart instance and the new size. + * If onResize is defined in the chart options, this callback will be ignored + * OnResize doesn't get called when the chart doesn't use relative positioning. + * @param chart @type {IChartJs} the chart instance + * @param newSize @type {IChartSize} the new size. + */ + onResize?(chart: Chart, newSize: ChartSize): void; +} + +/** + * The state of a chart + */ +export interface IChartControlState { + isLoading: boolean; + data?: Chart.ChartData; + rejected?: {}; +} + +/** + * The color palettes available within Office. + */ +export enum ChartPalette { + /** + * Office Colorful Palette 1: + * Blue, Orange, Grey, Gold, Blue, Green + */ + OfficeColorful1, + + /** + * Office Colorful Palette 2: + * Blue, Grey, Blue, Dark Blue, Dark Grey, Dark Blue + */ + OfficeColorful2, + + /** + * Office Colorful Palette 3: + * Orange, Gold, Green, Brown, Dark Yellow, Dark Green */ + OfficeColorful3, + + /** + * Office Colorful Palette 4: + * Green, Blue, Gold, Dark Green, Dark Blue, Dark Yellow */ + OfficeColorful4, + + /** + * Monochromatic Palette 1: + * Blue gradient, dark to light + */ + OfficeMonochromatic1, + + /** + * Monochromatic Palette 2: + * Orange gradient, dark to light + */ + OfficeMonochromatic2, + + /** + * Monochromatic Palette 3: + * Grey gradient, dark to light + */ + OfficeMonochromatic3, + + /** + * Monochromatic Palette 4: + * Gold gradient, dark to light + */ + OfficeMonochromatic4, + + /** + * Monochromatic Palette 5: + * Blue gradient, dark to light + */ + OfficeMonochromatic5, + + /** + * Monochromatic Palette 6: + * Green gradient, dark to light + */ + OfficeMonochromatic6, + + /** + * Monochromatic Palette 7: + * Dark Grey, Light Grey, Grey, Dark Grey, Light Grey, Grey + */ + OfficeMonochromatic7, + + /** + * Monochromatic Palette 8: + * Blue gradient, light to dark + */ + OfficeMonochromatic8, + + /** + * Monochromatic Palette 9: + * Orange gradient, light to dark + */ + OfficeMonochromatic9, + + /** + * Monochromatic Palette 10: + * Grey gradient, light to dark + */ + OfficeMonochromatic10, + + /** + * Monochromatic Palette 11: + * Gold gradient, light to dark + */ + OfficeMonochromatic11, + + /** + * Monochromatic Palette 12: + * Blue gradient, light to dark + */ + OfficeMonochromatic12, + + /** + * Monochromatic Palette 13: + * Green gradient, light to dark + */ + OfficeMonochromatic13 +} + +export interface IChartAccessibility { + /** + * Indicates if the chart should render + * a hidden table that will appear for + * screen readers + * @default true + */ + enable?: boolean; + + /** + * Allows you to overwrite the default classname + * of the accessible table + */ + className?: string; + + /** + * Provides a caption for the accessible table + * @default defaults to the chart's title if any + */ + caption?: string; + + /** + * Provides a summary of the data + */ + summary?: string; + + /** + * Provides an alternate text for the chart. + */ + alternateText?: string; + + /** + * Allows you to custom-render your own accessible table + */ + onRenderTable?: () => JSX.Element; +} + +/** + * Use this interface if you'd like to create a plugin + */ +export interface IChartPlugin { + beforeInit?(chartInstance: Chart, options?: {}): void; + afterInit?(chartInstance: Chart, options?: {}): void; + + beforeUpdate?(chartInstance: Chart, options?: {}): void; + afterUpdate?(chartInstance: Chart, options?: {}): void; + + beforeLayout?(chartInstance: Chart, options?: {}): void; + afterLayout?(chartInstance: Chart, options?: {}): void; + + beforeDatasetsUpdate?(chartInstance: Chart, options?: {}): void; + afterDatasetsUpdate?(chartInstance: Chart, options?: {}): void; + + // This is called at the start of a render. + // It is only called once, even if the animation will run + // for a number of frames. Use beforeDraw or afterDraw + // to do something on each animation frame + beforeRender?(chartInstance: Chart, options?: {}): void; + afterRender?(chartInstance: Chart, options?: {}): void; + + // Easing is for animation + beforeDraw?(chartInstance: Chart, easing: string, options?: {}): void; + afterDraw?(chartInstance: Chart, easing: string, options?: {}): void; + + // Before the datasets are drawn but after scales are drawn + beforeDatasetsDraw?(chartInstance: Chart, easing: string, options?: {}): void; + afterDatasetsDraw?(chartInstance: Chart, easing: string, options?: {}): void; + + // Called before drawing the `tooltip`. If any plugin returns `false`, + // the tooltip drawing is cancelled until another `render` is triggered. + beforeTooltipDraw?(chartInstance: Chart, tooltipData?: {}, options?: {}): void; + // Called after drawing the `tooltip`. Note that this hook will not, + // be called if the tooltip drawing has been previously cancelled. + afterTooltipDraw?(chartInstance: Chart, tooltipData?: {}, options?: {}): void; + + // Called when an event occurs on the chart + beforeEvent?(chartInstance: Chart, event: Event, options?: {}): void; + afterEvent?(chartInstance: Chart, event: Event, options?: {}): void; + + resize?(chartInstance: Chart, newChartSize: Chart.ChartSize, options?: {}): void; + destroy?(chartInstance: Chart): void; +} + +/** + * The types of charts available + */ +export type ChartType = 'line' + | 'bar' + | 'horizontalBar' + | 'radar' + | 'doughnut' + | 'polarArea' + | 'bubble' + | 'pie' + | 'scatter'; + +/** + * The types of charts available + */ +/* tslint:disable */ +export const ChartType = { + Line: 'line' as ChartType, + Bar: 'bar' as ChartType, + HorizontalBar: 'horizontalBar' as ChartType, + Radar: 'radar' as ChartType, + Doughnut: 'doughnut' as ChartType, + PolarArea: 'polarArea' as ChartType, + Bubble: 'bubble' as ChartType, + Pie: 'pie' as ChartType, + Scatter: 'scatter' as ChartType +}; +/* tslint:enable */ diff --git a/src/controls/chartControl/PaletteGenerator.test.ts b/src/controls/chartControl/PaletteGenerator.test.ts new file mode 100644 index 000000000..17d5aa345 --- /dev/null +++ b/src/controls/chartControl/PaletteGenerator.test.ts @@ -0,0 +1,77 @@ +/// + +import * as React from 'react'; +import { assert, expect } from 'chai'; +import { PaletteGenerator } from '.'; +import { ChartPalette } from './ChartControl.types'; +import { uniq } from '@microsoft/sp-lodash-subset'; + +declare const sinon; + +describe('PaletteGenerator', () => { + it('Should repeat palette if array is longer than number of available colors in repeating palette', (done) => { + let palette: string[] = PaletteGenerator.GetPalette(ChartPalette.OfficeColorful2, 60); + expect(palette).to.have.length(60); + done(); + }); + + it('Should stretch palette if array is longer than number of available colors in non-repeating palette', (done) => { + let palette: string[] = PaletteGenerator.GetPalette(ChartPalette.OfficeMonochromatic1, 60); + expect(palette).to.have.length(60); + + // Array shouldn't repeat, so the unique array should be the same length as the returned array + expect(uniq(palette)).to.have.length(60); + done(); + }); + + it('Should return the right alpha palette length', (done) => { + let alphaPalette = PaletteGenerator.alpha(PaletteGenerator.GetPalette(ChartPalette.OfficeColorful1, 60), 0.5); + expect(alphaPalette).to.have.length(60); + done(); + }); + + it('Should repeat a shorter array of colors', (done) => { + let desiredPattern: string[] = ["#0000ff", "#00ff00", "#ff0000"]; + let palette: string[] = PaletteGenerator.generateRepeatingPattern(desiredPattern, 60); + expect(palette).to.have.length(60); + + // Array should repeat the same exact values. + expect(uniq(palette)).to.have.length(3); + done(); + }); + + it('Should not repeat colors in a gradient', (done) => { + let gradientExtremes: string[] = ["#0000ff", "#ff0000"]; + let palette: string[] = PaletteGenerator.generateNonRepeatingGradient(gradientExtremes, 60); + expect(palette).to.have.length(60); + + // Array should not repeat + expect(uniq(palette)).to.have.length(60); + done(); + }); + + + it('Should return an array of alpha value for a given array of colors', (done) => { + let arrayColors: string[] = ["#00ff00", "#ff0000", "#0000ff"]; + let alphaColors = PaletteGenerator.alpha(arrayColors, 0.5); + expect(alphaColors).to.be.an('array'); + expect(alphaColors).to.have.length(3); + done(); + }); + + it('Should return a single alpha value for a single color', (done) => { + let singleColor: string = "#00ff00"; + let alphaColor = PaletteGenerator.alpha(singleColor, 0.5); + expect(alphaColor).to.be.a('string'); + done(); + }); + + it('Should return a single alpha value for any valid type of color value', (done) => { + expect(PaletteGenerator.alpha('red', 0.5)).to.be.a('string'); + expect(PaletteGenerator.alpha('#ff0000', 0.5)).to.be.a('string'); + expect(PaletteGenerator.alpha('rgb(255,0,0)', 0.5)).to.be.a('string'); + expect(PaletteGenerator.alpha('hsl(120,100%,50%)', 0.5)).to.be.a('string'); + done(); + }); +}); + diff --git a/src/controls/chartControl/PaletteGenerator.ts b/src/controls/chartControl/PaletteGenerator.ts new file mode 100644 index 000000000..3b708c59b --- /dev/null +++ b/src/controls/chartControl/PaletteGenerator.ts @@ -0,0 +1,199 @@ +import { ChartPalette } from './ChartControl.types'; +import { + OFFICE_COLORFUL1, + OFFICE_COLORFUL2, + OFFICE_COLORFUL3, + OFFICE_COLORFUL4, + OFFICE_MONOCHROMATIC1, + OFFICE_MONOCHROMATIC2, + OFFICE_MONOCHROMATIC3, + OFFICE_MONOCHROMATIC4, + OFFICE_MONOCHROMATIC5, + OFFICE_MONOCHROMATIC6, + OFFICE_MONOCHROMATIC7, + OFFICE_MONOCHROMATIC8, + OFFICE_MONOCHROMATIC9, + OFFICE_MONOCHROMATIC10, + OFFICE_MONOCHROMATIC11, + OFFICE_MONOCHROMATIC12, + OFFICE_MONOCHROMATIC13 +} from './ChartColorPalettes'; +import * as Color from 'color'; + + +/** + * Generates color palettes matching those you get within Office charts + */ +export class PaletteGenerator { + + /** + * Returns an array of colors generated using one of the + * pre-defined Office palettes colors + * @param palette the pre-defined Office palette + * @param desiredLength the desired resulting array lenght + */ + public static GetPalette(palette: ChartPalette, desiredLength: number): string[] { + // Add default color scheme if it wasn't provided + let paletteColors: string[] = []; + switch (palette) { + case ChartPalette.OfficeColorful4: + paletteColors = this.generateRepeatingPattern(OFFICE_COLORFUL4, desiredLength); + break; + case ChartPalette.OfficeColorful3: + paletteColors = this.generateRepeatingPattern(OFFICE_COLORFUL3, desiredLength); + break; + case ChartPalette.OfficeColorful2: + paletteColors = this.generateRepeatingPattern(OFFICE_COLORFUL2, desiredLength); + break; + case ChartPalette.OfficeMonochromatic1: + paletteColors = this.generateNonRepeatingGradient(OFFICE_MONOCHROMATIC1, desiredLength); + break; + case ChartPalette.OfficeMonochromatic2: + paletteColors = this.generateNonRepeatingGradient(OFFICE_MONOCHROMATIC2, desiredLength); + break; + case ChartPalette.OfficeMonochromatic3: + paletteColors = this.generateNonRepeatingGradient(OFFICE_MONOCHROMATIC3, desiredLength); + break; + case ChartPalette.OfficeMonochromatic4: + paletteColors = this.generateNonRepeatingGradient(OFFICE_MONOCHROMATIC4, desiredLength); + break; + case ChartPalette.OfficeMonochromatic5: + paletteColors = this.generateNonRepeatingGradient(OFFICE_MONOCHROMATIC5, desiredLength); + break; + case ChartPalette.OfficeMonochromatic6: + paletteColors = this.generateNonRepeatingGradient(OFFICE_MONOCHROMATIC6, desiredLength); + break; + case ChartPalette.OfficeMonochromatic7: + paletteColors = this.generateRepeatingPattern(OFFICE_MONOCHROMATIC7, desiredLength); + break; + case ChartPalette.OfficeMonochromatic8: + paletteColors = this.generateNonRepeatingGradient(OFFICE_MONOCHROMATIC8, desiredLength); + break; + case ChartPalette.OfficeMonochromatic9: + paletteColors = this.generateNonRepeatingGradient(OFFICE_MONOCHROMATIC9, desiredLength); + break; + case ChartPalette.OfficeMonochromatic10: + paletteColors = this.generateNonRepeatingGradient(OFFICE_MONOCHROMATIC10, desiredLength); + break; + case ChartPalette.OfficeMonochromatic11: + paletteColors = this.generateNonRepeatingGradient(OFFICE_MONOCHROMATIC11, desiredLength); + break; + case ChartPalette.OfficeMonochromatic12: + paletteColors = this.generateNonRepeatingGradient(OFFICE_MONOCHROMATIC12, desiredLength); + break; + case ChartPalette.OfficeMonochromatic13: + paletteColors = this.generateNonRepeatingGradient(OFFICE_MONOCHROMATIC13, desiredLength); + break; + default: + paletteColors = this.generateRepeatingPattern(OFFICE_COLORFUL1, desiredLength); + break; + } + + return paletteColors; + } + + /** + * ChartJs doesn't give cycle through colors if the length of array of colors is smaller than + * the data length. This function repeats the array as many times as needed to make sure each + * item has a background color. + * @param array the array to make bigger + * @param desiredLength the desired length + */ + public static generateRepeatingPattern(array: string[], desiredLength: number): string[] { + if (desiredLength === 0) { + return []; + } + + let a: string[] = array; + while (a.length * 2 <= desiredLength) { + a = a.concat(a); + } + + if (a.length < desiredLength) { + a = a.concat(a.slice(0, desiredLength - a.length)); + } + return a; + } + + /** + * Some of the Office color palettes consist of + * a non-repeating gradient from one color to another, + * divided by the number of steps (or array elements) + * @param colorRange the colors to start and end with + * @param numSteps the number of gradient steps to generate + */ + public static generateNonRepeatingGradient(colorRange: string[], numSteps: number): string[] { + const startHex: string = colorRange[0]; + const endHex: string = colorRange[colorRange.length - 1]; + const startRGB: number[] = this._hexToRGB(startHex); + const endRGB: number[] = this._hexToRGB(endHex); + + const colors: string[] = []; + + const factorStep: number = 1 / (numSteps - 1); + for (let index: number = 0; index < numSteps; index++) { + const interpolatedColor: number[] = this._interpolateColors(startRGB, endRGB, factorStep * index); + const hexColor: string = this._rgbToHex(interpolatedColor); + colors.push(hexColor); + } + + return colors; + } + + /** + * Converts a color or array of colors to a semi-opaque version + * @param colors a single color, or array of colors to convert + * @param alphaValue a value between 0 and 1 indicating the opacity (alpha) + */ + public static alpha(colors: string | string[], alphaValue: number): string | string[] { + if (colors instanceof Array) { + return colors.map((color: string) => { + return Color(color).alpha(alphaValue).toString(); + }); + } else { + return Color(colors).alpha(alphaValue).toString(); + } + } + + /** + * Interpolates between two colors + * @param color1 start color + * @param color2 end color + * @param factor interpolation facotor + */ + private static _interpolateColors(color1: number[], color2: number[], factor: number): number[] { + if (arguments.length < 3) { + factor = 0.5; + } + + const result: number[] = color1.slice(); + for (let i: number = 0; i < 3; i++) { + result[i] = Math.round(result[i] + factor * (color2[i] - color1[i])); + } + return result; + } + + /** + * Converts a hex color to RGB + * @param hex the hex color string + */ + private static _hexToRGB(hex: string): number[] { + const result: RegExpExecArray = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + + // tslint:disable-next-line:no-bitwise + return result ? [ + parseInt(result[1], 16), + parseInt(result[2], 16), + parseInt(result[3], 16) + ] : undefined; + } + + /** + * Converts an RGB colour to a hex string + * @param rgb The RGC color + */ + private static _rgbToHex(rgb: number[]): string { + // tslint:disable-next-line:no-bitwise + return '#' + ((1 << 24) + (rgb[0] << 16) + (rgb[1] << 8) + rgb[2]).toString(16).slice(1); + } +} diff --git a/src/controls/chartControl/index.ts b/src/controls/chartControl/index.ts new file mode 100644 index 000000000..84e3163b4 --- /dev/null +++ b/src/controls/chartControl/index.ts @@ -0,0 +1,6 @@ +export * from './ChartControl.types'; +export * from './ChartControl'; +export * from './PaletteGenerator'; +export * from './ChartColorPalettes'; +export * from './AccessibleChartTable'; +export * from './AccessibleChartTable.types'; diff --git a/src/controls/iFrameDialog/IFrameDialog.tsx b/src/controls/iFrameDialog/IFrameDialog.tsx index 03b927176..600726c04 100644 --- a/src/controls/iFrameDialog/IFrameDialog.tsx +++ b/src/controls/iFrameDialog/IFrameDialog.tsx @@ -23,6 +23,39 @@ export interface IFrameDialogProps extends IDialogProps { * iframe height */ height: string; + /** + * Specifies if iframe content can be displayed in a full screen. + * Usage: + */ + allowFullScreen?: boolean; + /** + * Specifies if transparency is allowed in iframe + */ + allowTransparency?: boolean; + /** + * Specifies the top and bottom margins of the content of an + + ) : ( +

+ + {this.props.errorMessage ? this.props.errorMessage : strings.mapsErrorMessage} +

+ ) + ) + } + + ); + } +} diff --git a/src/controls/map/Maps.module.scss b/src/controls/map/Maps.module.scss new file mode 100644 index 000000000..eca52f054 --- /dev/null +++ b/src/controls/map/Maps.module.scss @@ -0,0 +1,39 @@ +.mapContainer { + margin-bottom: 20px; +} + +.searchContainer { + margin-bottom: 20px; + + &::after { + content: ""; + clear: both; + display: table; + } +} + +.searchTextBox{ + float: left; + width: 79%; +} + +.submitButton{ + float: right; + width: 19%; +} + + +.errorMessage { + font-size: 12px; + font-weight: 400; + color: #a80000; + margin: 0; + padding-top: 5px; + display: flex; + align-items: center; +} + +.errorIcon { + font-size: 14px; + margin-right: 5px; +} diff --git a/src/controls/map/index.ts b/src/controls/map/index.ts new file mode 100644 index 000000000..c0f979277 --- /dev/null +++ b/src/controls/map/index.ts @@ -0,0 +1,4 @@ +export * from './Map'; +export * from './IMapProps'; +export * from './IMapState'; +export * from './IMap'; diff --git a/src/controls/peoplepicker/IPeoplePicker.ts b/src/controls/peoplepicker/IPeoplePicker.ts index 16282355b..874f5b0d7 100644 --- a/src/controls/peoplepicker/IPeoplePicker.ts +++ b/src/controls/peoplepicker/IPeoplePicker.ts @@ -1,4 +1,5 @@ import { WebPartContext } from '@microsoft/sp-webpart-base'; +import { ExtensionContext } from '@microsoft/sp-extension-base'; import { DirectionalHint } from "office-ui-fabric-react/lib/common/DirectionalHint"; import { IPersonaProps } from "office-ui-fabric-react/lib/components/Persona/Persona.types"; import { PrincipalType } from "."; @@ -11,13 +12,13 @@ export interface IPeoplePickerProps { /** * Context of the component */ - context: WebPartContext; + context: WebPartContext | ExtensionContext; /** * Text of the Control */ - titleText: string; + titleText?: string; /** - * Web Absolute Url of source site + * Web Absolute Url of source site. When this is provided, a search request is done to the local site. */ webAbsoluteUrl?: string; /** @@ -32,6 +33,10 @@ export interface IPeoplePickerProps { * Maximum number of suggestions to show in the full suggestion list. (default: 5) */ suggestionsLimit?: number; + /** + * Specify the user / group types to retrieve + */ + resolveDelay?: number; /** * Selection Limit of Control */ @@ -39,31 +44,31 @@ export interface IPeoplePickerProps { /** * Show or Hide Tooltip */ - showtooltip? : boolean; + showtooltip?: boolean; /** * People Field is mandatory */ - isRequired? : boolean; + isRequired?: boolean; /** * Mandatory field error message */ - errorMessage? : string; + errorMessage?: string; /** * Method to check value of People Picker text */ - selectedItems?: (items: any[]) => void; + selectedItems?: (items: IPersonaProps[]) => void; /** * Tooltip Message */ - tooltipMessage? : string; + tooltipMessage?: string; /** * Directional Hint of tool tip */ - tooltipDirectional? : DirectionalHint; - /** - * Class Name for the whole People picker control - */ - peoplePickerWPclassName?:string; + tooltipDirectional?: DirectionalHint; + /** + * Class Name for the whole People picker control + */ + peoplePickerWPclassName?: string; /** * Class Name for the People picker control */ @@ -75,30 +80,31 @@ export interface IPeoplePickerProps { /** * Default Selected User Emails */ - defaultSelectedUsers? : string[]; + defaultSelectedUsers?: string[]; /** + * @deprecated * Show users which are hidden from the UI */ showHiddenInUI?: boolean; /** * Specify the user / group types to retrieve */ - principleTypes?: PrincipalType[]; + principalTypes?: PrincipalType[]; + /** + * When ensure user property is true, it will return the local user ID on the current site when doing a tenant wide search + */ + ensureUser?: boolean; } export interface IPeoplePickerState { - selectedPersons?: IPersonaProps[]; mostRecentlyUsedPersons: IPersonaProps[]; - currentSelectedPersons: IPersonaProps[]; - allPersons: IPeoplePickerUserItem[]; - delayResults?: boolean; - currentPicker?: number | string; + showRequiredError: boolean; + errorMessage: string; + resolveDelay : number; + + selectedPersons?: IPersonaProps[]; peoplePersonaMenu?: IPersonaProps[]; - peoplePartTitle: string; - peoplePartTooltip : string; - isLoading : boolean; - peopleValidatorText? : string; - showmessageerror: boolean; + delayResults?: boolean; } export interface IPeoplePickerUserItem { diff --git a/src/controls/peoplepicker/IUsers.ts b/src/controls/peoplepicker/IUsers.ts index c3f52362d..2f6cf4510 100644 --- a/src/controls/peoplepicker/IUsers.ts +++ b/src/controls/peoplepicker/IUsers.ts @@ -1,9 +1,9 @@ export interface IUsers { '@odata.context': string; - value: Value[]; + value: IUserInfo[]; } -export interface Value { +export interface IUserInfo { '@odata.type': string; '@odata.id': string; '@odata.editLink': string; diff --git a/src/controls/peoplepicker/PeoplePickerComponent.tsx b/src/controls/peoplepicker/PeoplePickerComponent.tsx index 94669b78a..6217712d1 100644 --- a/src/controls/peoplepicker/PeoplePickerComponent.tsx +++ b/src/controls/peoplepicker/PeoplePickerComponent.tsx @@ -1,32 +1,31 @@ import * as strings from 'ControlStrings'; import * as React from 'react'; -import { IPeoplePickerProps, IPeoplePickerState, IPeoplePickerUserItem } from './IPeoplePicker'; +import * as telemetry from '../../common/telemetry'; +import styles from './PeoplePickerComponent.module.scss'; +import SPPeopleSearchService from "../../services/PeopleSearchService"; +import { IPeoplePickerProps, IPeoplePickerState } from './IPeoplePicker'; import { TooltipHost, DirectionalHint } from 'office-ui-fabric-react/lib/Tooltip'; import { NormalPeoplePicker } from 'office-ui-fabric-react/lib/components/pickers/PeoplePicker/PeoplePicker'; -import { MessageBar } from 'office-ui-fabric-react/lib/MessageBar'; -import { SPHttpClient } from '@microsoft/sp-http'; -import styles from './PeoplePickerComponent.module.scss'; -import * as telemetry from '../../common/telemetry'; -import { assign } from 'office-ui-fabric-react/lib/Utilities'; -import { IUsers } from './IUsers'; import { Label } from 'office-ui-fabric-react/lib/components/Label'; -import { Environment, EnvironmentType } from "@microsoft/sp-core-library"; import { IBasePickerSuggestionsProps } from "office-ui-fabric-react/lib/components/pickers/BasePicker.types"; -import { IPersonaWithMenu } from "office-ui-fabric-react/lib/components/pickers/PeoplePicker/PeoplePickerItems/PeoplePickerItem.types"; import { IPersonaProps } from "office-ui-fabric-react/lib/components/Persona/Persona.types"; -import { MessageBarType } from "office-ui-fabric-react/lib/components/MessageBar"; -import { ValidationState } from 'office-ui-fabric-react/lib/components/pickers/BasePicker.types'; import { Icon } from "office-ui-fabric-react/lib/components/Icon"; -import { isEqual } from "@microsoft/sp-lodash-subset"; +import { isEqual, uniqBy } from "@microsoft/sp-lodash-subset"; /** -* PeoplePicker component -*/ + * PeoplePicker component + */ export class PeoplePicker extends React.Component { + private peopleSearchService: SPPeopleSearchService; + private suggestionsLimit: number; + private groupId: number; constructor(props: IPeoplePickerProps) { super(props); + this.peopleSearchService = new SPPeopleSearchService(props.context); + this.suggestionsLimit = this.props.suggestionsLimit ? this.props.suggestionsLimit : 5; + telemetry.track('ReactPeoplePicker', { groupName: !!props.groupName, name: !!props.groupName, @@ -36,279 +35,114 @@ export class PeoplePicker extends React.Component = this.state.allPersons.length !==0 ? this.state.allPersons : new Array(); - - // Set Default selected persons - let defaultUsers : any = []; - let defaultPeopleList: IPersonaProps[] = []; - if (this.props.defaultSelectedUsers) { - defaultUsers = this.getDefaultUsers(userValuesArray, this.props.defaultSelectedUsers); - for (const persona of defaultUsers) { - let selectedPeople: IPersonaProps = {}; - assign(selectedPeople, persona); - defaultPeopleList.push(selectedPeople); - } - } - - this.setState({ - selectedPersons : defaultPeopleList.length !== 0 ? defaultPeopleList : [], - showmessageerror: this.props.isRequired && defaultPeopleList.length === 0 - }); - } + this.getInitialPersons(); } - /** - * Generate the user photo link - * - * @param value - */ - private generateUserPhotoLink(value: string): string { - return `https://outlook.office365.com/owa/service.svc/s/GetPersonaPhoto?email=${value}&UA=0&size=HR96x96`; - } /** - * Retrieve the users for local demo and testing purposes + * componentWillUpdate lifecycle hook */ - private async _loadLocalWorkbenchUsers(): Promise { - let _fakeUsers: Array = new Array(); - - _fakeUsers.push({ - id: "10dfa208-d7d4-4aef-a7ea-f9e4bb1b85c1", - imageUrl: "", - imageInitials: "RF", - text: "Roger Federer", - secondaryText: "roger@tennis.onmicrosoft.com", - tertiaryText: "", - optionalText: "" - }); - _fakeUsers.push({ - id: "10dfa208-d7d4-4aef-a7ea-f9e4bb1b85c2", - imageUrl: "", - imageInitials: "RN", - text: "Rafael Nadal", - secondaryText: "rafael@tennis.onmicrosoft.com", - tertiaryText: "", - optionalText: "" - }); - _fakeUsers.push({ - id: "10dfa208-d7d4-4aef-a7ea-f9e4bb1b85c3", - imageUrl: "", - imageInitials: "ND", - text: "Novak Djokovic", - secondaryText: "novak@tennis.onmicrosoft.com", - tertiaryText: "", - optionalText: "" - }); - _fakeUsers.push({ - id: "10dfa208-d7d4-4aef-a7ea-f9e4bb1b85c4", - imageUrl: "", - imageInitials: "JP", - text: "Juan Martin del Potro", - secondaryText: "juanmartin@tennis.onmicrosoft.com", - tertiaryText: "", - optionalText: "" - }); - - let personaList: IPersonaProps[] = []; - for (const persona of _fakeUsers) { - let personaWithMenu: IPersonaProps = {}; - assign(personaWithMenu, persona); - personaList.push(personaWithMenu); + public componentWillUpdate(nextProps: IPeoplePickerProps, nextState: IPeoplePickerState): void { + if (!isEqual(this.props.defaultSelectedUsers, nextProps.defaultSelectedUsers) || + this.props.groupName !== nextProps.groupName || + this.props.webAbsoluteUrl !== nextProps.webAbsoluteUrl) { + this.getInitialPersons(); } - - // update the current state - this.setState({ - allPersons: _fakeUsers, - peoplePersonaMenu: personaList, - mostRecentlyUsedPersons: personaList.slice(0, 5), - showmessageerror: this.props.isRequired && this.state.selectedPersons.length === 0 - }); - } + /** - * Retrieve the users + * Get initial persons */ - private async _thisLoadUsers(): Promise { - var stringVal: string = ""; - - if (this.props.groupName) { - stringVal = `/_api/web/sitegroups/GetByName('${this.props.groupName}')/users`; + private async getInitialPersons() { + const { groupName } = this.props; + // Check if a group property was provided, and get the group ID + if (groupName) { + this.groupId = await this.peopleSearchService.getGroupId(this.props.groupName, this.props.webAbsoluteUrl); + if (!this.groupId) { + this.setState({ + errorMessage: "Group could not be found." + }); + return; + } } else { - stringVal = "/_api/web/siteusers"; + this.groupId = null; } - // filter for principal Type - var filterVal: string = ""; - if (this.props.principleTypes) { - filterVal = `?$filter=${this.props.principleTypes.map(principalType => `(PrincipalType eq ${principalType})`).join(" or ")}`; - } + // Check for default user values + if (this.props.defaultSelectedUsers && this.props.defaultSelectedUsers.length) { + let selectedPersons: IPersonaProps[] = []; + for (const userValue of this.props.defaultSelectedUsers) { + const userResult = await this.peopleSearchService.searchPersonByEmailOrLogin(userValue, this.props.principalTypes, this.props.webAbsoluteUrl, this.groupId, this.props.ensureUser); + if (userResult) { + selectedPersons.push(userResult); + } + } - // filter for showHiddenInUI - if (this.props.showHiddenInUI) { - filterVal = filterVal ? `${filterVal} and (IsHiddenInUI eq ${this.props.showHiddenInUI})` : `?$filter=IsHiddenInUI eq ${this.props.showHiddenInUI}`; + this.setState({ + selectedPersons + }); } + } - const webAbsoluteUrl = this.props.webAbsoluteUrl || this.props.context.pageContext.web.absoluteUrl; - // Create the rest API - const restApi = `${webAbsoluteUrl}${stringVal}${filterVal}`; - try { - // Call the API endpoint - const data = await this.props.context.spHttpClient.get(restApi, SPHttpClient.configurations.v1, { - headers: { - 'Accept': 'application/json;odata.metadata=none' - } + /** + * A search field change occured + */ + private onSearchFieldChanged = async (searchText: string, currentSelected: IPersonaProps[]): Promise => { + if (searchText.length > 2) { + const results = await this.peopleSearchService.searchPeople(searchText, this.suggestionsLimit, this.props.principalTypes, this.props.webAbsoluteUrl, this.groupId, this.props.ensureUser); + // Remove duplicates + const { selectedPersons, mostRecentlyUsedPersons } = this.state; + const filteredPersons = this.removeDuplicates(results, selectedPersons); + // Add the users to the most recently used ones + let recentlyUsed = [...filteredPersons, ...mostRecentlyUsedPersons]; + recentlyUsed = uniqBy(recentlyUsed, "text"); + this.setState({ + mostRecentlyUsedPersons: recentlyUsed.slice(0, this.suggestionsLimit) }); - - if (data.ok) { - const items: IUsers = await data.json(); - - // Check if items were retrieved - if (items && items.value && items.value.length > 0) { - - let userValuesArray: Array = new Array(); - - // Loop over all the retrieved items - for (let i = 0; i < items.value.length; i++) { - const item = items.value[i]; - if (!item.IsHiddenInUI || (this.props.showHiddenInUI && item.IsHiddenInUI)) { - // Check if the the type must be returned - if (!this.props.principleTypes || this.props.principleTypes.indexOf(item.PrincipalType) !== -1) { - userValuesArray.push({ - id: item.Id.toString(), - imageUrl: this.generateUserPhotoLink(item.Email), - imageInitials: "", - text: item.Title, // name - secondaryText: item.Email, // email - tertiaryText: "", // status - optionalText: "" // anything - }); - } - } - } - - let personaList: IPersonaProps[] = []; - for (const persona of userValuesArray) { - let personaWithMenu: IPersonaProps = {}; - assign(personaWithMenu, persona); - personaList.push(personaWithMenu); - } - - // Update the current state - this.setState({ - allPersons : userValuesArray, - peoplePersonaMenu : personaList, - mostRecentlyUsedPersons : personaList.slice(0,5) - }); - } - } - } catch (e) { - console.error("Error occured while fetching the users and setting selected users."); + return filteredPersons; + } else { + return []; } } /** - * On persona item changed event + * On item selection change event */ - private _onPersonItemsChange = (items: any[]) => { - const { selectedItems } = this.props; + private onChange = (items: IPersonaProps[]): void => { + const { selectedItems: triggerUpdate } = this.props; this.setState({ selectedPersons: items, - showmessageerror: items.length > 0 ? false : true + showRequiredError: items.length > 0 ? false : true }); - if (selectedItems) { - selectedItems(items); + if (triggerUpdate) { + triggerUpdate(items); } } - /** - * Validates the user input - * - * @param input - */ - private _validateInputPeople = (input: string) => { - if (input.indexOf('@') !== -1) { - return ValidationState.valid; - } else if (input.length > 1) { - return ValidationState.warning; - } else { - return ValidationState.invalid; - } - } /** * Returns the most recently used person * * @param currentPersonas */ - private _returnMostRecentlyUsedPerson = (currentPersonas: IPersonaProps[]): IPersonaProps[] => { + private returnMostRecentlyUsedPerson = (currentPersonas: IPersonaProps[]): IPersonaProps[] => { let { mostRecentlyUsedPersons } = this.state; - return this._removeDuplicates(mostRecentlyUsedPersons, currentPersonas); - } - - /** - * On filter changed event - * - * @param filterText - * @param currentPersonas - * @param limitResults - */ - private _onPersonFilterChanged = (filterText: string, currentPersonas: IPersonaProps[], limitResults?: number): IPersonaProps[] => { - if (filterText) { - let filteredPersonas: IPersonaProps[] = this._filterPersons(filterText); - filteredPersonas = this._removeDuplicates(filteredPersonas, currentPersonas); - filteredPersonas = limitResults ? filteredPersonas.splice(0, limitResults) : filteredPersonas; - return filteredPersonas; - } else { - return []; - } - } - - /** - * Filter persons based on Name and Email (starting with and contains) - * - * @param filterText - */ - private _filterPersons(filterText: string): IPersonaProps[] { - return this.state.peoplePersonaMenu.filter(item => - this._doesTextStartWith(item.text as string, filterText) - || this._doesTextContains(item.text as string, filterText) - || this._doesTextStartWith(item.secondaryText as string, filterText) - || this._doesTextContains(item.secondaryText as string, filterText)); + return this.removeDuplicates(mostRecentlyUsedPersons, currentPersonas); } @@ -318,29 +152,10 @@ export class PeoplePicker extends React.Component { - return personas.filter(persona => !this._listContainsPersona(persona, possibleDupes)); - } - - /** - * Checks if text starts with - * - * @param text - * @param filterText - */ - private _doesTextStartWith(text: string, filterText: string): boolean { - return text && text.toLowerCase().indexOf(filterText.toLowerCase()) === 0; + private removeDuplicates = (personas: IPersonaProps[], possibleDupes: IPersonaProps[]): IPersonaProps[] => { + return personas.filter(persona => !this.listContainsPersona(persona, possibleDupes)); } - /** - * Checks if text contains - * - * @param text - * @param filterText - */ - private _doesTextContains(text: string, filterText: string): boolean { - return text && text.toLowerCase().indexOf(filterText.toLowerCase()) > 0; - } /** * Checks if list contains the person @@ -348,50 +163,13 @@ export class PeoplePicker extends React.Component { + private listContainsPersona = (persona: IPersonaProps, personas: IPersonaProps[]): boolean => { if (!personas || !personas.length || personas.length === 0) { return false; } return personas.filter(item => item.text === persona.text).length > 0; } - /** - * Gets the default users based on the provided email address. - * Adds emails that are not found with a random generated User Id - * - * @param userValuesArray - * @param selectedUsers - */ - private getDefaultUsers(userValuesArray: any[], selectedUsers: string[]): any { - let defaultuserValuesArray: any[] = []; - for (let i = 0; i < selectedUsers.length; i++) { - const obj = { valToCompare: selectedUsers[i] }; - const length = defaultuserValuesArray.length; - defaultuserValuesArray = defaultuserValuesArray.length !== 0 ? defaultuserValuesArray.concat(userValuesArray.filter(this.filterUsers, obj)) : userValuesArray.filter(this.filterUsers, obj); - if (length === defaultuserValuesArray.length) { - const defaultUnknownUser = [{ - id: 1000 + i, //just a random number - imageUrl: "", - imageInitials: "", - text: selectedUsers[i], //Name - secondaryText: selectedUsers[i], //Role - tertiaryText: "", //status - optionalText: "" //stgring - }]; - defaultuserValuesArray = defaultuserValuesArray.length !== 0 ? defaultuserValuesArray.concat(defaultUnknownUser) : defaultUnknownUser; - } - } - return defaultuserValuesArray; - } - - /** - * Filters Users based on email - */ - private filterUsers = function (value: any, index: number, ar: any[]) { - if (value.secondaryText.toLowerCase().indexOf(this.valToCompare.toLowerCase()) !== -1) { - return value; - } - }; /** * Default React component render method @@ -401,29 +179,30 @@ export class PeoplePicker extends React.Component - + {this.props.titleText && } peoplePersonaMenu.text} - className={`'ms-PeoplePicker' ${this.props.peoplePickerCntrlclassName ? this.props.peoplePickerCntrlclassName : ''}`} - key={'normal'} - onValidateInput={this._validateInputPeople} - removeButtonAriaLabel={'Remove'} - inputProps={{ - 'aria-label': 'People Picker' - }} - selectedItems={this.state.selectedPersons} - itemLimit={this.props.personSelectionLimit || 1} - disabled={this.props.disabled} - onChange={this._onPersonItemsChange} /> + onResolveSuggestions={this.onSearchFieldChanged} + onEmptyInputFocus={this.returnMostRecentlyUsedPerson} + getTextFromItem={(peoplePersonaMenu: IPersonaProps) => peoplePersonaMenu.text} + className={`'ms-PeoplePicker' ${this.props.peoplePickerCntrlclassName ? this.props.peoplePickerCntrlclassName : ''}`} + key={'normal'} + removeButtonAriaLabel={'Remove'} + inputProps={{ + 'aria-label': 'People Picker' + }} + selectedItems={this.state.selectedPersons} + itemLimit={this.props.personSelectionLimit || 1} + disabled={this.props.disabled || !!this.state.errorMessage} + onChange={this.onChange} + resolveDelay={this.state.resolveDelay} /> ); @@ -432,9 +211,9 @@ export class PeoplePicker extends React.Component + id='pntp' + calloutProps={{ gapSpace: 0 }} + directionalHint={this.props.tooltipDirectional || DirectionalHint.leftTopEdge}> {peoplepicker} ) : ( @@ -444,14 +223,20 @@ export class PeoplePicker extends React.Component - - {this.props.errorMessage ? this.props.errorMessage : strings.peoplePickerComponentErrorMessage} -

- ) - } + { + (this.props.isRequired && this.state.showRequiredError) || (this.state.errorMessage) && ( +

+ + { + this.state.errorMessage && {this.state.errorMessage} + } + + { + (this.props.isRequired && this.state.showRequiredError) && {this.props.errorMessage ? this.props.errorMessage : strings.peoplePickerComponentErrorMessage} + } +

+ ) + } ); } diff --git a/src/controls/placeholder/IPlaceholderComponent.ts b/src/controls/placeholder/IPlaceholderComponent.ts index 8801a6e37..1d8e8769b 100644 --- a/src/controls/placeholder/IPlaceholderComponent.ts +++ b/src/controls/placeholder/IPlaceholderComponent.ts @@ -21,16 +21,20 @@ export interface IPlaceholderProps { * Optional: As the button is optional. */ buttonLabel?: string; - /** - * onConfigure handler for the button. - * Optional: As the button is optional. - */ - onConfigure?: () => void; /** * This className is applied to the root element of content. Use this to * apply custom styles to the placeholder. */ contentClassName?: string; + /** + * Specify if you want to hide the config button + */ + hideButton?: boolean; + /** + * onConfigure handler for the button. + * Optional: As the button is optional. + */ + onConfigure?: () => void; } export interface IPlaceholderState { diff --git a/src/controls/placeholder/PlaceholderComponent.tsx b/src/controls/placeholder/PlaceholderComponent.tsx index f909665f5..2f1211132 100644 --- a/src/controls/placeholder/PlaceholderComponent.tsx +++ b/src/controls/placeholder/PlaceholderComponent.tsx @@ -54,7 +54,7 @@ export class Placeholder extends React.Component { - this.props.buttonLabel && + (this.props.buttonLabel && !this.props.hideButton) && t.Id.toLowerCase() === this.props.anchorId.toLowerCase()).shift(); - if (anchorTerm) - { + if (anchorTerm) { const anchorDepth = anchorTerm.PathDepth; this._anchorName = anchorTerm.Name; var anchorTerms : ITerm[] = this._terms.filter(t => t.PathOfTerm.substring(0, anchorTerm.PathOfTerm.length) === anchorTerm.PathOfTerm && t.Id !== anchorTerm.Id); diff --git a/src/index.ts b/src/index.ts index f1fe003fa..02cb63efd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ export * from './TaxonomyPicker'; export * from './WebPartTitle'; export * from './ListPicker'; export * from './ListItemPicker'; +export * from './ChartControl'; export * from './IFrameDialog'; export * from './Common'; diff --git a/src/loc/de-de.ts b/src/loc/de-de.ts index 350f17dbd..6d32b37cc 100644 --- a/src/loc/de-de.ts +++ b/src/loc/de-de.ts @@ -32,7 +32,7 @@ define([], () => { "L_RelativeDateTime_XDaysFuture": "{0} Tag ab jetzt||{0} Tage ab jetzt", "L_RelativeDateTime_XDays": "Vor {0} Tag||Vor {0} Tagen", "L_RelativeDateTime_XDaysFutureIntervals": "1||2-", - "L_RelativeDateTime_XDaysIntervals": "1||2-", + "L_RelativeDateTime_XDaysIntervals": "1||2-", "L_RelativeDateTime_Today": "Heute" }, "SendEmailTo": "Email senden an {0}", @@ -48,12 +48,20 @@ define([], () => { peoplePickerComponentTooltipMessage: "People Picker", peoplePickerComponentErrorMessage: "Benutzerauswahl ist ein Pflichtfeld", - peoplePickerComponentTitleText: "Benutzer auswählen", peoplePickerSuggestionsHeaderText: 'Vorgeschlagene Benutzer', peoplePickerLoadingText: 'Laden', + PeoplePickerSearchText: 'Fetching users', + PeoplePickerGroupNotFound: "Group could not be found.", ListItemPickerSelectValue: 'Wähle Wert', - genericNoResultsFoundText: 'Kein Ergebnis gefunden' + mapsErrorMessage: 'Beim Laden von Karten ist ein Fehler aufgetreten', + mapsLoadingText: 'Laden', + mapsSearchButtonText: 'Suche', + mapsTitlePrefix: 'Karte von', + + genericNoResultsFoundText: 'Kein Ergebnis gefunden', + + ListViewFilterLabel: "Filter" }; }); diff --git a/src/loc/en-us.ts b/src/loc/en-us.ts index 4948bbc2a..643836c34 100644 --- a/src/loc/en-us.ts +++ b/src/loc/en-us.ts @@ -32,7 +32,7 @@ define([], () => { "L_RelativeDateTime_XDaysFuture": "{0} day from now||{0} days from now", "L_RelativeDateTime_XDays": "{0} day ago||{0} days ago", "L_RelativeDateTime_XDaysFutureIntervals": "1||2-", - "L_RelativeDateTime_XDaysIntervals": "1||2-", + "L_RelativeDateTime_XDaysIntervals": "1||2-", "L_RelativeDateTime_Today": "Today" }, "SendEmailTo": "Send an email to {0}", @@ -48,12 +48,20 @@ define([], () => { peoplePickerComponentTooltipMessage: "People Picker", peoplePickerComponentErrorMessage: "Required Field", - peoplePickerComponentTitleText: "Pick the user(s)", peoplePickerSuggestionsHeaderText: 'Suggested People', peoplePickerLoadingText: 'Loading', + PeoplePickerSearchText: 'Fetching users', + PeoplePickerGroupNotFound: "Group could not be found.", ListItemPickerSelectValue: 'Select value', - genericNoResultsFoundText: 'No results found' + mapsErrorMessage: 'There was an error while loading the map', + mapsLoadingText: 'Loading', + mapsSearchButtonText: 'Search', + mapsTitlePrefix: 'Map of', + + genericNoResultsFoundText: 'No results found', + + ListViewFilterLabel: "Filter the list" }; }); diff --git a/src/loc/fr-fr.ts b/src/loc/fr-fr.ts index 75359af7a..ff90cf7d8 100644 --- a/src/loc/fr-fr.ts +++ b/src/loc/fr-fr.ts @@ -32,7 +32,7 @@ define([], () => { "L_RelativeDateTime_XDaysFuture": "{0} jours à partir de maintenant || {0} jours à partir de maintenant", "L_RelativeDateTime_XDays": "Il y a {0} jour||Il y a {0} jours", "L_RelativeDateTime_XDaysFutureIntervals": "1||2-", - "L_RelativeDateTime_XDaysIntervals": "1||2-", + "L_RelativeDateTime_XDaysIntervals": "1||2-", "L_RelativeDateTime_Today": "Aujourd'hui" }, "SendEmailTo": "Envoyer un email à {0}", @@ -48,12 +48,20 @@ define([], () => { peoplePickerComponentTooltipMessage: "Sélecteur de personnes", peoplePickerComponentErrorMessage: "Le sélecteur de personnes est obligatoire", - peoplePickerComponentTitleText: "Choisissez l'utilisateur(s)", peoplePickerSuggestionsHeaderText: 'Personnes suggérées', peoplePickerLoadingText: 'Chargement', + PeoplePickerSearchText: 'Fetching users', + PeoplePickerGroupNotFound: "Group could not be found.", ListItemPickerSelectValue: 'Sélectionnez une valeur', - genericNoResultsFoundText: 'Aucun résultat trouvé' + mapsErrorMessage: "Une erreur s'est produite lors du chargement des cartes.", + mapsLoadingText: 'Chargement', + mapsSearchButtonText: 'Chercher', + mapsTitlePrefix: 'Carte de', + + genericNoResultsFoundText: 'Aucun résultat trouvé', + + ListViewFilterLabel: "Filtre" }; }); diff --git a/src/loc/mystrings.d.ts b/src/loc/mystrings.d.ts index 2b58ffa1b..966db17ba 100644 --- a/src/loc/mystrings.d.ts +++ b/src/loc/mystrings.d.ts @@ -1,7 +1,10 @@ declare interface IControlStrings { + PeoplePickerGroupNotFound: string; + ListViewFilterLabel: string; + + PeoplePickerSearchText: string; peoplePickerComponentTooltipMessage: string; peoplePickerComponentErrorMessage: string; - peoplePickerComponentTitleText: string; peoplePickerSuggestionsHeaderText: string; genericNoResultsFoundText: string; peoplePickerLoadingText: string; @@ -11,7 +14,7 @@ declare interface IControlStrings { ListViewGroupEmptyLabel: string; WebPartTitlePlaceholder: string; WebPartTitleLabel: string; - DateTime:{[key: string]: string}; + DateTime: { [key: string]: string }; SendEmailTo: string; StartChatWith: string; Contact: string; @@ -25,6 +28,12 @@ declare interface IControlStrings { TaxonomyPickerTermSetLabel: string; ListItemPickerSelectValue: string; + + //Maps + mapsErrorMessage: string; + mapsLoadingText: string; + mapsSearchButtonText: string; + mapsTitlePrefix: string; } declare module 'ControlStrings' { diff --git a/src/loc/nl-nl.ts b/src/loc/nl-nl.ts index 541730aab..562057391 100644 --- a/src/loc/nl-nl.ts +++ b/src/loc/nl-nl.ts @@ -32,7 +32,7 @@ define([], () => { "L_RelativeDateTime_XDaysFuture": "{0} dag vanaf nu||{0} dagen vanaf nu", "L_RelativeDateTime_XDays": "{0} dag geleden||{0} dagen geleden", "L_RelativeDateTime_XDaysFutureIntervals": "1||2-", - "L_RelativeDateTime_XDaysIntervals": "1||2-", + "L_RelativeDateTime_XDaysIntervals": "1||2-", "L_RelativeDateTime_Today": "Vandaag" }, "SendEmailTo": "Stuur een mail naar {0}", @@ -48,12 +48,20 @@ define([], () => { peoplePickerComponentTooltipMessage: "Personen kiezen", peoplePickerComponentErrorMessage: "Verplicht veld", - peoplePickerComponentTitleText: "Kies personen", peoplePickerSuggestionsHeaderText: 'Voorgestelde personen', peoplePickerLoadingText: 'Laden', + PeoplePickerSearchText: 'Personen worden opgehaald', + PeoplePickerGroupNotFound: "Groep niet gevonden.", ListItemPickerSelectValue: 'Selecteer veld', - genericNoResultsFoundText: 'Geen resultaten gevonden' + mapsErrorMessage: 'Er is een fout opgetreden bij het laden van de kaart', + mapsLoadingText: 'Laden', + mapsSearchButtonText: 'Zoeken', + mapsTitlePrefix: 'Kaart van', + + genericNoResultsFoundText: 'Geen resultaten gevonden', + + ListViewFilterLabel: "Filter de lijst" }; }); diff --git a/src/services/PeoplePickerMockClient.ts b/src/services/PeoplePickerMockClient.ts new file mode 100644 index 000000000..59f83626b --- /dev/null +++ b/src/services/PeoplePickerMockClient.ts @@ -0,0 +1,49 @@ +import { IPeoplePickerUserItem } from "../PeoplePicker"; + +export const MockUsers : IPeoplePickerUserItem[] = [{ + id: "10dfa208-d7d4-4aef-a7ea-f9e4bb1b85c1", + imageUrl: "", + imageInitials: "RF", + text: "Roger Federer", + secondaryText: "roger@tennis.onmicrosoft.com", + tertiaryText: "", + optionalText: "" + }, + { + id: "10dfa208-d7d4-4aef-a7ea-f9e4bb1b85c2", + imageUrl: "", + imageInitials: "RN", + text: "Rafael Nadal", + secondaryText: "rafael@tennis.onmicrosoft.com", + tertiaryText: "", + optionalText: "" + }, + { + id: "10dfa208-d7d4-4aef-a7ea-f9e4bb1b85c3", + imageUrl: "", + imageInitials: "ND", + text: "Novak Djokovic", + secondaryText: "novak@tennis.onmicrosoft.com", + tertiaryText: "", + optionalText: "" + }, + { + id: "10dfa208-d7d4-4aef-a7ea-f9e4bb1b85c4", + imageUrl: "", + imageInitials: "JP", + text: "Juan Martin del Potro", + secondaryText: "juanmartin@tennis.onmicrosoft.com", + tertiaryText: "", + optionalText: "" + } +]; + +export class PeoplePickerMockClient { + public filterPeople = function(value : any, index : number, ar : any[]) { + if (value.secondaryText.toLowerCase().indexOf(this.valToCompare.toLowerCase()) !== -1 || value.text.toLowerCase().indexOf(this.valToCompare.toLowerCase()) !== -1) { + return true; + } else { + return false; + } + }; +} diff --git a/src/services/PeopleSearchService.ts b/src/services/PeopleSearchService.ts new file mode 100644 index 000000000..9184d9aba --- /dev/null +++ b/src/services/PeopleSearchService.ts @@ -0,0 +1,332 @@ +import { ISPHttpClientOptions, SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http'; +import { Environment, EnvironmentType } from '@microsoft/sp-core-library'; +import { WebPartContext } from '@microsoft/sp-webpart-base'; +import { ExtensionContext } from '@microsoft/sp-extension-base'; +import { MockUsers, PeoplePickerMockClient } from './PeoplePickerMockClient'; +import { PrincipalType, IPeoplePickerUserItem } from "../PeoplePicker"; +import { IUsers, IUserInfo } from "../controls/peoplepicker/IUsers"; +import { cloneDeep, findIndex } from "@microsoft/sp-lodash-subset"; + +/** + * Service implementation to search people in SharePoint + */ +export default class SPPeopleSearchService { + private cachedPersonas: { [property: string]: IUserInfo[] }; + private cachedLocalUsers: { [siteUrl: string]: IUserInfo[] }; + + /** + * Service constructor + */ + constructor(private context: WebPartContext | ExtensionContext) { + this.cachedPersonas = {}; + this.cachedLocalUsers = {}; + this.cachedLocalUsers[this.context.pageContext.web.absoluteUrl] = []; + } + + /** + * Generate the user photo link + * + * @param value + */ + public generateUserPhotoLink(value: string): string { + return `https://outlook.office365.com/owa/service.svc/s/GetPersonaPhoto?email=${value}&UA=0&size=HR96x96`; + } + + /** + * Retrieve the specified group + * + * @param groupName + * @param siteUrl + */ + public async getGroupId(groupName: string, siteUrl: string = null): Promise { + if (Environment.type === EnvironmentType.Local) { + return 1; + } else { + const groups = await this.searchTenant(siteUrl, groupName, 1, [PrincipalType.SharePointGroup], false, 0); + return (groups && groups.length > 0) ? parseInt(groups[0].id) : null; + } + } + + /** + * Search person by its email or login name + */ + public async searchPersonByEmailOrLogin(email: string, principalTypes: PrincipalType[], siteUrl: string = null, groupId: number = null, ensureUser: boolean = false): Promise { + if (Environment.type === EnvironmentType.Local) { + // If the running environment is local, load the data from the mock + const mockUsers = await this.searchPeopleFromMock(email); + return (mockUsers && mockUsers.length > 0) ? mockUsers[0] : null; + } else { + const userResults = await this.searchTenant(siteUrl, email, 1, principalTypes, ensureUser, groupId); + return (userResults && userResults.length > 0) ? userResults[0] : null; + } + } + + /** + * Search All Users from the SharePoint People database + */ + public async searchPeople(query: string, maximumSuggestions: number, principalTypes: PrincipalType[], siteUrl: string = null, groupId: number = null, ensureUser: boolean = false): Promise { + if (Environment.type === EnvironmentType.Local) { + // If the running environment is local, load the data from the mock + return this.searchPeopleFromMock(query); + } else { + return await this.searchTenant(siteUrl, query, maximumSuggestions, principalTypes, ensureUser, groupId); + } + } + + /** + * Local site search + */ + private async localSearch(siteUrl: string, query: string, principalTypes: PrincipalType[], showHiddenInUI: boolean, groupName: string, exactMatch: boolean = false): Promise { + try { + let stringVal: string = ""; + let cachedPropertyName: string = null; + + // Check if service needs to search in the site or group + if (groupName) { + stringVal = `/_api/web/sitegroups/GetByName('${groupName}')/users`; + cachedPropertyName = `${siteUrl}-${groupName}`; + } else { + stringVal = "/_api/web/siteusers"; + cachedPropertyName = siteUrl; + } + + if (typeof this.cachedPersonas[cachedPropertyName] === "undefined") { + // filter for principal Type + let filterVal: string = ""; + if (principalTypes) { + filterVal = `?$filter=(${principalTypes.map(principalType => `PrincipalType eq ${principalType}`).join(" or ")})`; + } + + // filter for showHiddenInUI + filterVal = filterVal ? `${filterVal} and (IsHiddenInUI eq ${showHiddenInUI})` : `?$filter=IsHiddenInUI eq ${showHiddenInUI}`; + + // Create the rest API + const restApi = `${siteUrl}${stringVal}${filterVal}`; + const data = await this.context.spHttpClient.get(restApi, SPHttpClient.configurations.v1, { + headers: { + 'Accept': 'application/json;odata.metadata=none' + } + }); + + if (data.ok) { + const userDataResp: IUsers = await data.json(); + if (userDataResp && userDataResp.value && userDataResp.value.length > 0) { + this.cachedPersonas[cachedPropertyName] = cloneDeep(userDataResp.value); + } + } + } + + // Check if persons or groups were retrieved and return the ones for the query + if (this.cachedPersonas[cachedPropertyName]) { + let persons = this.cachedPersonas[cachedPropertyName]; + if (query) { + // Check if exact match is required + if (exactMatch) { + persons = persons.filter(element => element.Email.toLowerCase() === query.toLowerCase() || element.LoginName.toLowerCase() === query.toLowerCase()); + } else { + persons = persons.filter(element => element.Title.toLowerCase().indexOf(query.toLowerCase()) !== -1 || element.Email.toLowerCase().indexOf(query.toLowerCase()) !== -1 || element.LoginName.toLowerCase().indexOf(query.toLowerCase()) !== -1); + } + } + + return persons.map(item => ({ + id: item.Id.toString(), + imageUrl: item.PrincipalType === PrincipalType.User ? this.generateUserPhotoLink(item.Email) : null, + imageInitials: this.getFullNameInitials(item.Title), + text: item.Title, // name + secondaryText: item.Email || item.LoginName, // email + tertiaryText: "", // status + optionalText: "" // anything + } as IPeoplePickerUserItem)); + } + + // Nothing to return + return []; + } catch (e) { + console.error("PeopleSearchService::localSearch: error occured while fetching the users."); + return []; + } + } + + /** + * Tenant search + */ + private async searchTenant(siteUrl: string, query: string, maximumSuggestions: number, principalTypes: PrincipalType[], ensureUser: boolean, groupId: number): Promise { + try { + // If the running env is SharePoint, loads from the peoplepicker web service + const userRequestUrl: string = `${siteUrl || this.context.pageContext.web.absoluteUrl}/_api/SP.UI.ApplicationPages.ClientPeoplePickerWebServiceInterface.clientPeoplePickerSearchUser`; + const searchBody = { + queryParams: { + AllowEmailAddresses: true, + AllowMultipleEntities: false, + AllUrlZones: false, + MaximumEntitySuggestions: maximumSuggestions, + PrincipalSource: 15, + // PrincipalType controls the type of entities that are returned in the results. + // Choices are All - 15, Distribution List - 2 , Security Groups - 4, SharePoint Groups - 8, User - 1. + // These values can be combined (example: 13 is security + SP groups + users) + PrincipalType: !!principalTypes && principalTypes.length > 0 ? principalTypes.reduce((a, b) => a + b, 0) : 1, + QueryString: query + } + }; + + // Search on the local site when "0" + if (siteUrl) { + searchBody.queryParams["SharePointGroupID"] = 0; + } + + // Check if users need to be searched in a specific group + if (groupId) { + searchBody.queryParams["SharePointGroupID"] = groupId; + } + + const httpPostOptions: ISPHttpClientOptions = { + headers: { + 'accept': 'application/json', + 'content-type': 'application/json' + }, + body: JSON.stringify(searchBody) + }; + + // Do the call against the People REST API endpoint + const data = await this.context.spHttpClient.post(userRequestUrl, SPHttpClient.configurations.v1, httpPostOptions); + if (data.ok) { + const userDataResp = await data.json(); + if (userDataResp && userDataResp.value && userDataResp.value.length > 0) { + let values: any = userDataResp.value; + + if (typeof userDataResp.value === "string") { + values = JSON.parse(userDataResp.value); + } + + // Filter out "UNVALIDATED_EMAIL_ADDRESS" + values = values.filter(v => !(v.EntityData && v.EntityData.PrincipalType && v.EntityData.PrincipalType === "UNVALIDATED_EMAIL_ADDRESS")); + + // Check if local user IDs need to be retrieved + if (ensureUser) { + for (const value of values) { + // Only ensure the user if it is not a SharePoint group + if (!value.EntityData || (value.EntityData && typeof value.EntityData.SPGroupID === "undefined")) { + const id = await this.ensureUser(value.Key); + value.Key = id; + } + } + } + + // Filter out NULL keys + values = values.filter(v => v.Key !== null); + + const userResults = values.map(element => { + switch (element.EntityType) { + case 'User': + let email : string = element.EntityData.Email !== null ? element.EntityData.Email : element.Description; + return { + id: element.Key, + imageUrl: this.generateUserPhotoLink(email), + imageInitials: this.getFullNameInitials(element.DisplayText), + text: element.DisplayText, // name + secondaryText: email, // email + tertiaryText: "", // status + optionalText: "" // anything + } as IPeoplePickerUserItem; + case 'SecGroup': + return { + id: element.Key, + imageInitials: this.getFullNameInitials(element.DisplayText), + text: element.DisplayText, + secondaryText: element.ProviderName + } as IPeoplePickerUserItem; + case 'FormsRole': + return { + id: element.Key, + imageInitials: this.getFullNameInitials(element.DisplayText), + text: element.DisplayText, + secondaryText: element.ProviderName + } as IPeoplePickerUserItem; + default: + return { + id: element.EntityData.SPGroupID, + imageInitials: this.getFullNameInitials(element.DisplayText), + text: element.DisplayText, + secondaryText: element.EntityData.AccountName + } as IPeoplePickerUserItem; + } + }); + + return userResults; + } + } + + // Nothing to return + return []; + } catch (e) { + console.error("PeopleSearchService::searchTenant: error occured while fetching the users."); + return []; + } + } + + /** + * Retrieves the local user ID + * + * @param userId + */ + private async ensureUser(userId: string): Promise { + const siteUrl = this.context.pageContext.web.absoluteUrl; + if (this.cachedLocalUsers && this.cachedLocalUsers[siteUrl]) { + const users = this.cachedLocalUsers[siteUrl]; + const userIdx = findIndex(users, u => u.LoginName === userId); + if (userIdx !== -1) { + return users[userIdx].Id; + } + } + + const restApi = `${siteUrl}/_api/web/ensureuser`; + const data = await this.context.spHttpClient.post(restApi, SPHttpClient.configurations.v1, { + body: JSON.stringify({ 'logonName': userId }) + }); + + if (data.ok) { + const user: IUserInfo = await data.json(); + if (user && user.Id) { + this.cachedLocalUsers[siteUrl].push(user); + return user.Id; + } + } + + return null; + } + + /** + * Generates Initials from a full name + */ + private getFullNameInitials(fullName: string): string { + if (fullName === null) { + return fullName; + } + + const words: string[] = fullName.split(' '); + if (words.length === 0) { + return ''; + } else if (words.length === 1) { + return words[0].charAt(0); + } else { + return (words[0].charAt(0) + words[1].charAt(0)); + } + } + + /** + * Gets the user photo url + */ + private getUserPhotoUrl(userEmail: string, siteUrl: string): string { + return `${siteUrl}/_layouts/15/userphoto.aspx?size=S&accountname=${userEmail}`; + } + + + /** + * Returns fake people results for the Mock mode + */ + private searchPeopleFromMock(query: string): Promise> { + let mockClient: PeoplePickerMockClient = new PeoplePickerMockClient(); + let filterValue = { valToCompare: query }; + return new Promise>((resolve) => resolve(MockUsers.filter(mockClient.filterPeople, filterValue))); + } +} diff --git a/src/services/SPService.ts b/src/services/SPService.ts index 615f02093..8cbf5d602 100644 --- a/src/services/SPService.ts +++ b/src/services/SPService.ts @@ -1,12 +1,12 @@ import { ISPService, ILibsOptions, LibsOrderBy } from "./ISPService"; import { ISPLists } from "../common/SPEntities"; import { WebPartContext } from "@microsoft/sp-webpart-base"; -import { ApplicationCustomizerContext } from "@microsoft/sp-application-base"; +import { ExtensionContext } from "@microsoft/sp-extension-base"; import { SPHttpClient, SPHttpClientResponse } from "@microsoft/sp-http"; export default class SPService implements ISPService { - constructor(private _context: WebPartContext | ApplicationCustomizerContext) {} + constructor(private _context: WebPartContext | ExtensionContext) { } /** * Get lists or libraries @@ -17,7 +17,7 @@ export default class SPService implements ISPService { let queryUrl: string = `${this._context.pageContext.web.absoluteUrl}/_api/web/lists?$select=Title,id,BaseTemplate`; if (options.orderBy) { - queryUrl += `&$orderby=${options.orderBy === LibsOrderBy.Id ? 'Id': 'Title'}`; + queryUrl += `&$orderby=${options.orderBy === LibsOrderBy.Id ? 'Id' : 'Title'}`; } if (options.baseTemplate) { diff --git a/src/services/SPServiceFactory.ts b/src/services/SPServiceFactory.ts index 9870bec76..6d25ee0b5 100644 --- a/src/services/SPServiceFactory.ts +++ b/src/services/SPServiceFactory.ts @@ -1,4 +1,4 @@ -import { ApplicationCustomizerContext } from '@microsoft/sp-application-base'; +import { ExtensionContext } from '@microsoft/sp-extension-base'; import { IWebPartContext, WebPartContext } from "@microsoft/sp-webpart-base"; import { ISPService } from "./ISPService"; import { Environment, EnvironmentType } from "@microsoft/sp-core-library"; @@ -6,7 +6,7 @@ import SPServiceMock from "./SPServiceMock"; import SPService from "./SPService"; export class SPServiceFactory { - public static createService(context: WebPartContext | ApplicationCustomizerContext, includeDelay?: boolean, delayTimeout?: number): ISPService { + public static createService(context: WebPartContext | ExtensionContext, includeDelay?: boolean, delayTimeout?: number): ISPService { if (Environment.type === EnvironmentType.Local) { return new SPServiceMock(includeDelay, delayTimeout); } diff --git a/src/services/SPTermStorePickerService.ts b/src/services/SPTermStorePickerService.ts index 3b6604590..05496eff1 100644 --- a/src/services/SPTermStorePickerService.ts +++ b/src/services/SPTermStorePickerService.ts @@ -57,7 +57,7 @@ export default class SPTermStorePickerService { return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then((serviceResponse: SPHttpClientResponse) => { return serviceResponse.json().then((serviceJSONResponse: any) => { // Construct results - let termStoreResult: ITermStore[] = serviceJSONResponse.filter(r => r['_ObjectType_'] === 'SP.Taxonomy.TermStore'); + let termStoreResult: ITermStore[] = serviceJSONResponse.filter((r: { [x: string]: string; }) => r['_ObjectType_'] === 'SP.Taxonomy.TermStore'); // Check if term store was retrieved if (termStoreResult.length > 0) { // Check if the termstore needs to be filtered or limited @@ -146,13 +146,13 @@ export default class SPTermStorePickerService { return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then((serviceResponse: SPHttpClientResponse) => { return serviceResponse.json().then((serviceJSONResponse: any) => { - const termStoreResultTermSets: ITermSet[] = serviceJSONResponse.filter(r => r['_ObjectType_'] === 'SP.Taxonomy.TermSet'); + const termStoreResultTermSets: ITermSet[] = serviceJSONResponse.filter((r: { [x: string]: string; }) => r['_ObjectType_'] === 'SP.Taxonomy.TermSet'); if (termStoreResultTermSets.length > 0) { var termStoreResultTermSet = termStoreResultTermSets[0]; termStoreResultTermSet.Terms = []; // Retrieve the term collection results - const termStoreResultTerms: ITerms[] = serviceJSONResponse.filter(r => r['_ObjectType_'] === 'SP.Taxonomy.TermCollection'); + const termStoreResultTerms: ITerms[] = serviceJSONResponse.filter((r: { [x: string]: string; }) => r['_ObjectType_'] === 'SP.Taxonomy.TermCollection'); if (termStoreResultTerms.length > 0) { // Retrieve all terms let terms = termStoreResultTerms[0]._Child_Items_; @@ -264,7 +264,7 @@ export default class SPTermStorePickerService { return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then((serviceResponse: SPHttpClientResponse) => { return serviceResponse.json().then((serviceJSONResponse: any) => { // Retrieve the term collection results - const termStoreResult: ITerms[] = serviceJSONResponse.filter(r => r['_ObjectType_'] === 'SP.Taxonomy.TermCollection'); + const termStoreResult: ITerms[] = serviceJSONResponse.filter((r: { [x: string]: string; }) => r['_ObjectType_'] === 'SP.Taxonomy.TermCollection'); if (termStoreResult.length > 0) { // Retrieve all terms @@ -302,15 +302,15 @@ export default class SPTermStorePickerService { * @param b term 2 */ private _sortTerms(a: ITerm, b: ITerm) { - if(a.CustomSortOrderIndex === -1){ - if (a.PathOfTerm < b.PathOfTerm) { + if (a.CustomSortOrderIndex === -1) { + if (a.PathOfTerm.toLowerCase() < b.PathOfTerm.toLowerCase()) { return -1; } - if (a.PathOfTerm > b.PathOfTerm) { + if (a.PathOfTerm.toLowerCase() > b.PathOfTerm.toLowerCase()) { return 1; } return 0; - }else{ + } else { if (a.CustomSortOrderIndex < b.CustomSortOrderIndex) { return -1; } @@ -319,7 +319,6 @@ export default class SPTermStorePickerService { } return 0; } - } /** diff --git a/src/webparts/controlsTest/components/ControlsTest.tsx b/src/webparts/controlsTest/components/ControlsTest.tsx index 18ff386f0..c87481278 100644 --- a/src/webparts/controlsTest/components/ControlsTest.tsx +++ b/src/webparts/controlsTest/components/ControlsTest.tsx @@ -14,12 +14,14 @@ import { WebPartTitle } from '../../../WebPartTitle'; import { TaxonomyPicker, IPickerTerms } from '../../../TaxonomyPicker'; import { ListPicker } from '../../../ListPicker'; import { IFrameDialog } from '../../../IFrameDialog'; -import { Environment, EnvironmentType } from '@microsoft/sp-core-library'; +import { Environment, EnvironmentType, DisplayMode } from '@microsoft/sp-core-library'; import { SecurityTrimmedControl, PermissionLevel } from '../../../SecurityTrimmedControl'; import { SPPermission } from '@microsoft/sp-page-context'; import { PeoplePicker, PrincipalType } from '../../../PeoplePicker'; import { getItemClassNames } from 'office-ui-fabric-react/lib/components/ContextualMenu/ContextualMenu.classNames'; import { ListItemPicker } from "../../../ListItemPicker"; +import { Map, ICoordinates, MapType } from '../../../Map'; +import { ChartControl, ChartType } from "../../../ChartControl"; /** * Component that can be used to test out the React controls from this project @@ -54,17 +56,17 @@ export default class ControlsTest extends React.Component { return resp.json(); }) - // .then(items => { - // let emails : string[] = items.value ? items.value.map((item, key)=> { return item.Author.EMail}) : []; - // console.log(emails); - // this.setState({ - // authorEmails: emails - // }); - // }); + // // Get Authors in the SharePoint Document library -- For People Picker Testing + // const restAuthorApi = `${this.props.context.pageContext.web.absoluteUrl}/_api/web/lists/GetByTitle('Documents')/Items?$select=Id, Author/EMail&$expand=Author/EMail`; + // this.props.context.spHttpClient.get(restAuthorApi, SPHttpClient.configurations.v1) + // .then(resp => { return resp.json(); }) + // .then(items => { + // let emails : string[] = items.value ? items.value.map((item, key)=> { return item.Author.EMail}) : []; + // console.log(emails); + // this.setState({ + // authorEmails: emails + // }); + // }); } /** @@ -92,14 +94,14 @@ export default class ControlsTest extends React.Component { + private _onTaxPickerChange = (terms: IPickerTerms) => { this.setState({ initialValues: terms }); @@ -216,7 +218,8 @@ private onServicePickerChange(terms: IPickerTerms): void { // Specify the fields on which you want to group your items // Grouping is takes the field order into account from the array - const groupByFields: IGrouping[] = [{ name: "ListItemAllFields.City", order: GroupOrder.ascending }, { name: "ListItemAllFields.Country.Label", order: GroupOrder.descending }]; + // const groupByFields: IGrouping[] = [{ name: "ListItemAllFields.City", order: GroupOrder.ascending }, { name: "ListItemAllFields.Country.Label", order: GroupOrder.descending }]; + const groupByFields: IGrouping[] = [{ name: "ListItemAllFields.Department.Label", order: GroupOrder.ascending }]; let iframeUrl: string = '/temp/workbench.html'; if (Environment.type === EnvironmentType.SharePoint) { @@ -232,6 +235,139 @@ private onServicePickerChange(terms: IPickerTerms): void { title={this.props.title} updateProperty={this.props.updateProperty} /> + + + + + + + + + + + + + + + + + + + + + + + console.log("Updated location:", coordinates)} + // zoom={15} + //mapType={MapType.cycle} + //width="50" + //height={150} + //loadingMessage="Loading maps" + //errorMessage="Hmmm, we do not have maps for Mars yet. Working on it..." + /> +
@@ -268,42 +404,42 @@ private onServicePickerChange(terms: IPickerTerms): void {
List picker tester: + label="Select your list(s)" + placeHolder="Select your list(s)" + baseTemplate={100} + includeHidden={false} + multiSelect={true} + onSelectionChanged={this.onListPickerChange} />
Field picker list data tester: + columnInternalName="Title" + itemLimit={5} + context={this.props.context} + onSelectedItem={this.listItemPickerDataSelected} />
Services tester: - - + allowMultipleSelections={true} + termsetNameOrID="ef1d77ab-51f6-492f-bf28-223a8ebc4b65" // id to termset that has a custom sort + panelTitle="Select Sorted Term" + label="Service Picker" + context={this.props.context} + onChange={this.onServicePickerChange} + isTermSetSelectable={false} + /> + + - { - this.setState({ - initialValues: [{ - key: "ab703558-2546-4b23-b8b8-2bcb2c0086f5", - name: "HR", - path: "HR", - termSet: "b3e9b754-2593-4ae6-abc2-35345402e186" - }] - }); - }} /> + { + this.setState({ + initialValues: [{ + key: "ab703558-2546-4b23-b8b8-2bcb2c0086f5", + name: "HR", + path: "HR", + termSet: "b3e9b754-2593-4ae6-abc2-35345402e186" + }] + }); + }} />
iframe dialog tester:
- - - - -

Deletes second item

- - - - +

Deletes second item

); }