From f217dd917af055d9710fdd90fbe924ac83693cae Mon Sep 17 00:00:00 2001 From: "Subarroca, Salvador" Date: Fri, 24 Apr 2020 12:49:40 +0200 Subject: [PATCH] feat(stacked-bar-chart): Create component --- .github/CODEOWNERS | 2 + angular.json | 40 ++ apps/dev/src/app.module.ts | 2 + apps/dev/src/devapp-routing.module.ts | 2 + apps/dev/src/devapp.component.ts | 1 + apps/dev/src/dt-components.module.ts | 2 + .../stacked-bar-chart-demo-data.ts | 70 ++ .../stacked-bar-chart-demo.component.html | 70 ++ .../stacked-bar-chart-demo.component.scss | 11 + .../stacked-bar-chart-demo.component.ts | 55 ++ apps/universal/src/app/barista.module.ts | 2 + .../src/app/kitchen-sink/kitchen-sink.html | 14 +- .../src/app/kitchen-sink/kitchen-sink.ts | 55 ++ .../stacked-bar-chart/README.md | 170 +++++ .../stacked-bar-chart/barista.json | 34 + .../stacked-bar-chart/index.ts | 27 + .../stacked-bar-chart/jest.config.js | 27 + .../stacked-bar-chart-overlay.directive.ts | 29 + .../src/stacked-bar-chart.html | 72 +++ .../src/stacked-bar-chart.layout.md | 219 +++++++ .../src/stacked-bar-chart.mock.ts | 157 +++++ .../src/stacked-bar-chart.module.ts | 29 + .../src/stacked-bar-chart.scss | 247 +++++++ .../src/stacked-bar-chart.spec.ts | 608 ++++++++++++++++++ .../src/stacked-bar-chart.ts | 471 ++++++++++++++ .../src/stacked-bar-chart.util.spec.ts | 545 ++++++++++++++++ .../src/stacked-bar-chart.util.ts | 249 +++++++ .../stacked-bar-chart/src/test-setup.ts | 17 + .../stacked-bar-chart/tsconfig.json | 7 + .../stacked-bar-chart/tsconfig.lib.json | 20 + .../stacked-bar-chart/tsconfig.lib.prod.json | 6 + .../stacked-bar-chart/tsconfig.spec.json | 10 + .../stacked-bar-chart/tslint.json | 7 + libs/examples/src/examples.module.ts | 2 + libs/examples/src/index.ts | 20 + libs/examples/src/stacked-bar-chart/index.ts | 21 + .../stacked-bar-chart-column-example.html | 12 + .../stacked-bar-chart-column-example.ts | 26 + ...ed-bar-chart-connected-legend-example.html | 42 ++ ...ed-bar-chart-connected-legend-example.scss | 17 + ...cked-bar-chart-connected-legend-example.ts | 40 ++ .../stacked-bar-chart-demo-data.ts | 157 +++++ .../stacked-bar-chart-examples.module.ts | 50 ++ .../stacked-bar-chart-filled-example.html | 11 + .../stacked-bar-chart-filled-example.ts | 27 + .../stacked-bar-chart-generic-example.html | 7 + .../stacked-bar-chart-generic-example.ts | 26 + .../stacked-bar-chart-single-example.html | 17 + .../stacked-bar-chart-single-example.ts | 27 + nx.json | 4 + tsconfig.json | 3 + 51 files changed, 3783 insertions(+), 3 deletions(-) create mode 100644 apps/dev/src/stacked-bar-chart/stacked-bar-chart-demo-data.ts create mode 100644 apps/dev/src/stacked-bar-chart/stacked-bar-chart-demo.component.html create mode 100644 apps/dev/src/stacked-bar-chart/stacked-bar-chart-demo.component.scss create mode 100644 apps/dev/src/stacked-bar-chart/stacked-bar-chart-demo.component.ts create mode 100644 libs/barista-components/stacked-bar-chart/README.md create mode 100644 libs/barista-components/stacked-bar-chart/barista.json create mode 100644 libs/barista-components/stacked-bar-chart/index.ts create mode 100644 libs/barista-components/stacked-bar-chart/jest.config.js create mode 100644 libs/barista-components/stacked-bar-chart/src/stacked-bar-chart-overlay.directive.ts create mode 100644 libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.html create mode 100644 libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.layout.md create mode 100644 libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.mock.ts create mode 100644 libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.module.ts create mode 100644 libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.scss create mode 100644 libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.spec.ts create mode 100644 libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.ts create mode 100644 libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.util.spec.ts create mode 100644 libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.util.ts create mode 100644 libs/barista-components/stacked-bar-chart/src/test-setup.ts create mode 100644 libs/barista-components/stacked-bar-chart/tsconfig.json create mode 100644 libs/barista-components/stacked-bar-chart/tsconfig.lib.json create mode 100644 libs/barista-components/stacked-bar-chart/tsconfig.lib.prod.json create mode 100644 libs/barista-components/stacked-bar-chart/tsconfig.spec.json create mode 100644 libs/barista-components/stacked-bar-chart/tslint.json create mode 100644 libs/examples/src/stacked-bar-chart/index.ts create mode 100644 libs/examples/src/stacked-bar-chart/stacked-bar-chart-column-example/stacked-bar-chart-column-example.html create mode 100644 libs/examples/src/stacked-bar-chart/stacked-bar-chart-column-example/stacked-bar-chart-column-example.ts create mode 100644 libs/examples/src/stacked-bar-chart/stacked-bar-chart-connected-legend-example/stacked-bar-chart-connected-legend-example.html create mode 100644 libs/examples/src/stacked-bar-chart/stacked-bar-chart-connected-legend-example/stacked-bar-chart-connected-legend-example.scss create mode 100644 libs/examples/src/stacked-bar-chart/stacked-bar-chart-connected-legend-example/stacked-bar-chart-connected-legend-example.ts create mode 100644 libs/examples/src/stacked-bar-chart/stacked-bar-chart-demo-data.ts create mode 100644 libs/examples/src/stacked-bar-chart/stacked-bar-chart-examples.module.ts create mode 100644 libs/examples/src/stacked-bar-chart/stacked-bar-chart-filled-example/stacked-bar-chart-filled-example.html create mode 100644 libs/examples/src/stacked-bar-chart/stacked-bar-chart-filled-example/stacked-bar-chart-filled-example.ts create mode 100644 libs/examples/src/stacked-bar-chart/stacked-bar-chart-generic-example/stacked-bar-chart-generic-example.html create mode 100644 libs/examples/src/stacked-bar-chart/stacked-bar-chart-generic-example/stacked-bar-chart-generic-example.ts create mode 100644 libs/examples/src/stacked-bar-chart/stacked-bar-chart-single-example/stacked-bar-chart-single-example.html create mode 100644 libs/examples/src/stacked-bar-chart/stacked-bar-chart-single-example/stacked-bar-chart-single-example.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1be6387a87..d0880b0022 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -48,6 +48,7 @@ /libs/barista-components/select/** @thomaspink /libs/barista-components/selection-area/** @ffriedl89 @lukasholzer /libs/barista-components/show-more/** @rowa-audil +/libs/barista-components/stacked-bar-chart/** @subarroca @airime /libs/barista-components/stepper/** @ffriedl89 @thomaspink /libs/barista-components/sunburst-chart/** @subarroca @pedromosquera /libs/barista-components/switch/** @thomaspink @@ -144,6 +145,7 @@ /apps/dev/src/select/** @thomaspink /apps/dev/src/selection-area/** @ffriedl89 @lukasholzer /apps/dev/src/show-more/** @rowa-audil +/apps/dev/src/stacked-bar-chart/** @subarroca @airime /apps/dev/src/stepper/** @ffriedl89 @thomaspink /apps/dev/src/sunburst-chart/** @subarroca /apps/dev/src/switch/** @thomaspink diff --git a/angular.json b/angular.json index ea96c272c1..48194dcacc 100644 --- a/angular.json +++ b/angular.json @@ -2916,6 +2916,46 @@ }, "schematics": {} }, + "stacked-bar-chart": { + "projectType": "library", + "root": "libs/barista-components/stacked-bar-chart", + "sourceRoot": "libs/barista-components/stacked-bar-chart/src", + "prefix": "dt", + "architect": { + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "libs/barista-components/stacked-bar-chart/tsconfig.lib.json", + "libs/barista-components/stacked-bar-chart/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**", + "!libs/barista-components/stacked-bar-chart/**" + ] + } + }, + "lint-styles": { + "builder": "./dist/libs/workspace:stylelint", + "options": { + "stylelintConfig": ".stylelintrc", + "reportFile": "dist/stylelint/report.xml", + "exclude": ["**/node_modules/**"], + "files": ["libs/barista-components/stacked-bar-chart/**/*.scss"] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "options": { + "jestConfig": "libs/barista-components/stacked-bar-chart/jest.config.js", + "tsConfig": "libs/barista-components/stacked-bar-chart/tsconfig.spec.json", + "setupFile": "libs/barista-components/stacked-bar-chart/src/test-setup.ts", + "passWithNoTests": true + } + } + }, + "schematics": {} + }, "stepper": { "projectType": "library", "root": "libs/barista-components/stepper", diff --git a/apps/dev/src/app.module.ts b/apps/dev/src/app.module.ts index e20c8fda81..8315060b77 100644 --- a/apps/dev/src/app.module.ts +++ b/apps/dev/src/app.module.ts @@ -74,6 +74,7 @@ import { RadioDemo } from './radio/radio-demo.component'; import { SecondaryNavDemo } from './secondary-nav/secondary-nav-demo.component'; import { SelectDemo } from './select/select-demo.component'; import { ShowMoreDemo } from './show-more/show-more-demo.component'; +import { StackedBarChartDemo } from './stacked-bar-chart/stacked-bar-chart-demo.component'; import { SidenavDemo } from './sidenav/sidenav-demo.component'; import { StepperDemo } from './stepper/stepper-demo.component'; import { SliderDemo } from './slider/slider-demo.component'; @@ -146,6 +147,7 @@ export class NoopRouteComponent {} SecondaryNavDemo, SelectDemo, ShowMoreDemo, + StackedBarChartDemo, SunburstChartDemo, SwitchDemo, TableDemo, diff --git a/apps/dev/src/devapp-routing.module.ts b/apps/dev/src/devapp-routing.module.ts index 0922936a94..651f5c089b 100644 --- a/apps/dev/src/devapp-routing.module.ts +++ b/apps/dev/src/devapp-routing.module.ts @@ -62,6 +62,7 @@ import { RadioDemo } from './radio/radio-demo.component'; import { SecondaryNavDemo } from './secondary-nav/secondary-nav-demo.component'; import { SelectDemo } from './select/select-demo.component'; import { ShowMoreDemo } from './show-more/show-more-demo.component'; +import { StackedBarChartDemo } from './stacked-bar-chart/stacked-bar-chart-demo.component'; import { SidenavDemo } from './sidenav/sidenav-demo.component'; import { StepperDemo } from './stepper/stepper-demo.component'; import { SliderDemo } from './slider/slider-demo.component'; @@ -122,6 +123,7 @@ const routes: Routes = [ { path: 'secondary-nav', component: SecondaryNavDemo }, { path: 'select', component: SelectDemo }, { path: 'show-more', component: ShowMoreDemo }, + { path: 'stacked-bar-chart', component: StackedBarChartDemo }, { path: 'stepper', component: StepperDemo }, { path: 'sunburst-chart', component: SunburstChartDemo }, { path: 'switch', component: SwitchDemo }, diff --git a/apps/dev/src/devapp.component.ts b/apps/dev/src/devapp.component.ts index f976b3d2f8..12421e6d65 100644 --- a/apps/dev/src/devapp.component.ts +++ b/apps/dev/src/devapp.component.ts @@ -88,6 +88,7 @@ export class DevApp implements AfterContentInit, OnDestroy { { name: 'Secondary-nav', route: '/secondary-nav' }, { name: 'Select', route: '/select' }, { name: 'Show-more', route: '/show-more' }, + { name: 'Stacked-bar-chart', route: '/stacked-bar-chart' }, { name: 'Stepper', route: '/stepper' }, { name: 'Slider', route: '/slider' }, { name: 'Sunburst-chart', route: '/sunburst-chart' }, diff --git a/apps/dev/src/dt-components.module.ts b/apps/dev/src/dt-components.module.ts index 2c6eaa0608..c0c61e998f 100644 --- a/apps/dev/src/dt-components.module.ts +++ b/apps/dev/src/dt-components.module.ts @@ -60,6 +60,7 @@ import { DtRadioModule } from '@dynatrace/barista-components/radio'; import { DtSecondaryNavModule } from '@dynatrace/barista-components/secondary-nav'; import { DtSelectModule } from '@dynatrace/barista-components/select'; import { DtShowMoreModule } from '@dynatrace/barista-components/show-more'; +import { DtStackedBarChartModule } from '@dynatrace/barista-components/stacked-bar-chart'; import { DtStepperModule } from '@dynatrace/barista-components/stepper'; import { DtSliderModule } from '@dynatrace/barista-components/slider'; import { DtSunburstChartModule } from '@dynatrace/barista-components/sunburst-chart'; @@ -117,6 +118,7 @@ import { DtTreeTableModule } from '@dynatrace/barista-components/tree-table'; DtSecondaryNavModule, DtSelectModule, DtShowMoreModule, + DtStackedBarChartModule, DtSunburstChartModule, DtSwitchModule, DtTableModule, diff --git a/apps/dev/src/stacked-bar-chart/stacked-bar-chart-demo-data.ts b/apps/dev/src/stacked-bar-chart/stacked-bar-chart-demo-data.ts new file mode 100644 index 0000000000..e5ab7946fe --- /dev/null +++ b/apps/dev/src/stacked-bar-chart/stacked-bar-chart-demo-data.ts @@ -0,0 +1,70 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const stackedBarChartDemoData = [ + { + label: 'Espresso', + nodes: [ + { + value: 1, + label: 'Coffee', + }, + ], + }, + { + label: 'Macchiato', + nodes: [ + { + value: 2, + label: 'Coffee', + }, + { + value: 1, + label: 'Milk', + }, + ], + }, + { + label: 'Americano', + nodes: [ + { + value: 2, + label: 'Coffee', + }, + { + value: 3, + label: 'Water', + }, + ], + }, + { + label: 'Mocha', + nodes: [ + { + value: 2, + label: 'Coffee', + }, + { + value: 2, + label: 'Chocolate', + }, + { + value: 1, + label: 'Milk', + }, + ], + }, +]; diff --git a/apps/dev/src/stacked-bar-chart/stacked-bar-chart-demo.component.html b/apps/dev/src/stacked-bar-chart/stacked-bar-chart-demo.component.html new file mode 100644 index 0000000000..1e188ccabb --- /dev/null +++ b/apps/dev/src/stacked-bar-chart/stacked-bar-chart-demo.component.html @@ -0,0 +1,70 @@ + + + {{ tooltip.seriesOrigin.label }} +
{{ tooltip.origin.label }}: {{ tooltip.origin.value }}
+
+
+ +

Demo controls

+ +

Mode

+ + Bar + Column + + +

Label

+Visible + +

Track

+

Background

+Visible + +

Max size

+ + None + 8 + 16 + 32 + 1000 + + +

Interactivity

+Selectable + +

Legend

+Visible + +

Fill mode

+ + Full + Relative + + +

Amount of series

+Multiple + + +

+ Legend value (only for single bar) +

+ + None + Absolute + Percent + +
diff --git a/apps/dev/src/stacked-bar-chart/stacked-bar-chart-demo.component.scss b/apps/dev/src/stacked-bar-chart/stacked-bar-chart-demo.component.scss new file mode 100644 index 0000000000..2bb13950ea --- /dev/null +++ b/apps/dev/src/stacked-bar-chart/stacked-bar-chart-demo.component.scss @@ -0,0 +1,11 @@ +:host { + display: block; +} + +dt-stacked-bar-chart { + margin-bottom: 40px; + + &.column { + min-height: 200px; + } +} diff --git a/apps/dev/src/stacked-bar-chart/stacked-bar-chart-demo.component.ts b/apps/dev/src/stacked-bar-chart/stacked-bar-chart-demo.component.ts new file mode 100644 index 0000000000..6d4104b1a6 --- /dev/null +++ b/apps/dev/src/stacked-bar-chart/stacked-bar-chart-demo.component.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component } from '@angular/core'; +import { + DtStackedBarChartFillMode, + DtStackedBarChartMode, + DtStackedBarChartNode, + DtStackedBarChartSeries, + DtStackedBarChartValueDisplayMode, +} from '@dynatrace/barista-components/stacked-bar-chart'; +import { stackedBarChartDemoData } from './stacked-bar-chart-demo-data'; + +@Component({ + selector: 'stacked-bar-chart-dev-app-demo', + templateUrl: './stacked-bar-chart-demo.component.html', + styleUrls: ['./stacked-bar-chart-demo.component.scss'], +}) +export class StackedBarChartDemo { + selectable: boolean = true; + selected: [DtStackedBarChartSeries, DtStackedBarChartNode] = [ + stackedBarChartDemoData[3], + stackedBarChartDemoData[3].nodes[1], + ]; + valueDisplayMode: DtStackedBarChartValueDisplayMode = 'absolute'; + visibleLabel: boolean = true; + visibleLegend: boolean = true; + fillMode: DtStackedBarChartFillMode = 'relative'; + multiSeries = true; + mode: DtStackedBarChartMode = 'bar'; + maxTrackSize: number = 16; + visibleTrackBackground: boolean = true; + + series = stackedBarChartDemoData; + + toggleSeries(multi: boolean): void { + this.multiSeries = multi; + this.series = multi + ? stackedBarChartDemoData + : [stackedBarChartDemoData[3]]; + } +} diff --git a/apps/universal/src/app/barista.module.ts b/apps/universal/src/app/barista.module.ts index 4b5eaf036e..965484bde6 100644 --- a/apps/universal/src/app/barista.module.ts +++ b/apps/universal/src/app/barista.module.ts @@ -46,6 +46,7 @@ import { DtRadialChartModule } from '@dynatrace/barista-components/radial-chart' import { DtRadioModule } from '@dynatrace/barista-components/radio'; import { DtSelectModule } from '@dynatrace/barista-components/select'; import { DtShowMoreModule } from '@dynatrace/barista-components/show-more'; +import { DtStackedBarChartModule } from '@dynatrace/barista-components/stacked-bar-chart'; import { DtStepperModule } from '@dynatrace/barista-components/stepper'; import { DtSliderModule } from '@dynatrace/barista-components/slider'; import { DtSunburstChartModule } from '@dynatrace/barista-components/sunburst-chart'; @@ -95,6 +96,7 @@ import { DtTreeTableModule } from '@dynatrace/barista-components/tree-table'; DtSelectModule, DtShowMoreModule, DtSliderModule, + DtStackedBarChartModule, DtStepperModule, DtSunburstChartModule, DtSwitchModule, diff --git a/apps/universal/src/app/kitchen-sink/kitchen-sink.html b/apps/universal/src/app/kitchen-sink/kitchen-sink.html index b3d3444a64..056e418b6d 100644 --- a/apps/universal/src/app/kitchen-sink/kitchen-sink.html +++ b/apps/universal/src/app/kitchen-sink/kitchen-sink.html @@ -322,9 +322,11 @@

Packets

- + + + + + @@ -343,6 +345,12 @@

Packets

+ + + {{ tooltip.label }}: {{ tooltip.value }} + + +

This element is alway visible

diff --git a/apps/universal/src/app/kitchen-sink/kitchen-sink.ts b/apps/universal/src/app/kitchen-sink/kitchen-sink.ts index 061d74af35..8c7de14ed2 100644 --- a/apps/universal/src/app/kitchen-sink/kitchen-sink.ts +++ b/apps/universal/src/app/kitchen-sink/kitchen-sink.ts @@ -129,4 +129,59 @@ export class KitchenSink { ], }, ]; + + stackedBarChartSeries = [ + { + label: 'Espresso', + nodes: [ + { + value: 1, + label: 'Coffee', + }, + ], + }, + { + label: 'Macchiato', + nodes: [ + { + value: 2, + label: 'Coffee', + }, + { + value: 1, + label: 'Milk', + }, + ], + }, + { + label: 'Americano', + nodes: [ + { + value: 2, + label: 'Coffee', + }, + { + value: 3, + label: 'Water', + }, + ], + }, + { + label: 'Mocha', + nodes: [ + { + value: 2, + label: 'Coffee', + }, + { + value: 2, + label: 'Chocolate', + }, + { + value: 1, + label: 'Milk', + }, + ], + }, + ]; } diff --git a/libs/barista-components/stacked-bar-chart/README.md b/libs/barista-components/stacked-bar-chart/README.md new file mode 100644 index 0000000000..fc6330f529 --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/README.md @@ -0,0 +1,170 @@ +# Stacked bar chart + + + +## Imports + +You have to import the `DtStackedBarChartModule` when you want to use the +``: + +```typescript +@NgModule({ + imports: [DtStackedBarChartModule], +}) +class MyModule {} +``` + +## Modes + +This chart allows 2 different modes: Bar and Column + +### Bar + + + +### Column + +Please be aware that this mode requires a height to be set + + + +## Initialization + +To create a `dtStackedBarChart` in a minimal configuration, only `series` +attribute is required to create a valid output. For multiple series, slices +follow the same ordering given by the developer + +## Options & Properties + +### DtStackedBarChart + +#### Inputs + +| Name | Type | Default | Description | +| ------------------------ | -------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `mode` | `DtStackedBarChartMode` | `'bar'` | Display mode. | +| `series` | `DtStackedBarChartSeries[]` | - | Array of series with their nodes. | +| `selectable` | `boolean` | false | Allow selections to be made on chart | +| `selected` | `[DtStackedBarChartSeries, DtStackedBarChartNode]` | - | Current selection [series, node] | +| `max` | `number | undefined` | - | Max value in the chart. Useful when binding multiple stacked-bar-chart. | +| `fillMode` | `DtStackedBarChartFillMode` | - | Whether each bar should be filled completely or should take into account their siblings and max. | +| `valueDisplayMode` | `DtStackedBarChartValueDisplayMode` | `'none'` | Sets the display mode for the stacked-bar-chart values in legend to either 'none' 'percent' or 'absolute'. Only valid for single track chart. | +| `legends` | `DtStackedBarChartLegend[]` | true | Array of legends that can be used to toggle bar nodes. As change detection is on push the changes will only affect when the reference is different. | +| `visibleLegend` | `boolean` | true | Visibility of the legend | +| `visibleTrackBackground` | `boolean` | true | Whether background should be transparent or show a background. | +| `visibleLabel` | `boolean` | true | Visibility of series label. | +| `maxTrackSize` | `number` | 16 | Maximum size of the track. | + +#### Outputs + +| Name | Type | Description | +| ---------------- | ------------------------------------- | --------------------------------------- | +| `selectedChange` | `EventEmitter` | Event that fires when a node is clicked | + +### DtStackedBarChartOverlay + +The `dtStackedBarChartOverlay` directive applies to an `ng-template` element +lets you provide a template for the rendered overlay. The overlay will be shown +when a user hovers the slice in stacked-bar-chart. `tooltip` is a +`DtStackedBarChartTooltipData`. + +```html + + + +``` + +## Models + +### DtStackedBarChartMode + +| Value | Description | +| -------- | ----------------- | +| `bar` | Horizontal tracks | +| `column` | Vertical tracks | + +### DtStackedBarChartFillMode + +For multiple series charts, every track can be fully filled or take into account +the maximum value among all series + +| Value | Description | +| ---------- | ---------------------------------------------------------------------- | +| `full` | It fills the whole track with this series nodes | +| `relative` | It takes into account the `max` input and the max value for all series | + +### DtStackedBarChartValueDisplayMode + +For single series charts, legend can display the value as the received value, as +a percentage of the total or not show it. + +| Value | Description | +| ---------- | ----------------------------------------------------- | +| `none` | Do not display the value in legend | +| `absolute` | Display the value present in DtStackedBarChartNode | +| `percent` | Display the percentage of the node within that series | + +### DtStackedBarChartSeries + +This `DtStackedBarChartSeries` holds the information for one series. + +| Name | Type | Description | +| ------- | ------------------------- | ------------------------------------- | +| `label` | `string` | Name of the series to be shown. | +| `nodes` | `DtStackedBarChartNode[]` | Array of node for the current series. | + +### DtStackedBarChartNode + +This `DtStackedBarChartNode` holds the information for every node in a given +series. + +| Name | Type | Optional | Description | +| ------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------- | +| `label` | `string` | No | Name of the node to be shown. | +| `value` | `number` | No | Numeric value used to calculate the slices. | +| `color` | `DtColors | string` | Yes | Color to be used. Fallback to [sorted chart colors](/resources/colors/chartcolors#sorted-chart-colors). | + +### DtStackedBarChartTooltipData + +The context of the overlay will be set to DtStackedBarChartTooltipData object +containing useful information that can be used inside the overlay's template + +| Name | Type | Description | +| --------------- | ----------------------- | ---------------------------------------------------------------- | +| `origin` | `DtStackedBarChartNode` | Node passed by user in `series` array. | +| `valueRelative` | `number` | Numeric percentage value based on this node vs sum of top level. | +| `color` | `DtColors | string` | Color for this node in this state. | +| `visible` | `boolean` | If node is visible in the stacked-bar-chart. | +| `selected` | `boolean` | If node is currently selected. | +| `width` | `string` | Current width in percentage given only the visible nodes. | + +### DtStackedBarChartLegend + +This `DtStackedBarChartLegend` holds the information for every legend item so +color and visibility is unified among same chart but also distributed charts +(i.e. multiple charts in a table). + +| Name | Type | Description | +| --------- | ------------------- | ------------------------------------------ | +| `label` | `string` | Label of the node. | +| `color` | `DtColors | string` | Color to be used based on nodes and theme. | +| `visible` | `boolean` | Whether it should be visible. | + +## Examples + +### Fill mode + + + +### Single selectable stacked bar chart + +For 3 nodes or less theme is applied + + + +### Connected legend + +When needed legend can be set outside and linked to distributed stacked bar +charts. Color for each node should be set in legend object + + diff --git a/libs/barista-components/stacked-bar-chart/barista.json b/libs/barista-components/stacked-bar-chart/barista.json new file mode 100644 index 0000000000..9d545f5920 --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/barista.json @@ -0,0 +1,34 @@ +{ + "title": "StackedBarChart", + "description": "The stacked-bar-chart chart is used to display one level of related data in a horizontal stacked bar", + "postid": "stacked-bar-chart", + "public": true, + "identifier": "Ss", + "themable": true, + "category": "components", + "contributors": { + "dev": [ + { + "name": "Salvador Subarroca", + "githubuser": "subarroca" + }, + { + "name": "Mireia Martín", + "githubuser": "airime" + } + ], + "ux": [ + { + "name": "Xavier Javaloyas", + "githubuser": "xavi-j" + } + ] + }, + "tags": [ + "chart", + "single stacked bar chart", + "bar chart", + "angular", + "component" + ] +} diff --git a/libs/barista-components/stacked-bar-chart/index.ts b/libs/barista-components/stacked-bar-chart/index.ts new file mode 100644 index 0000000000..9cb9860ce9 --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/index.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './src/stacked-bar-chart.module'; +export * from './src/stacked-bar-chart'; +export { + DtStackedBarChartNode, + DtStackedBarChartSeries, + DtStackedBarChartTooltipData, + DtStackedBarChartValueDisplayMode, + DtStackedBarChartMode, + DtStackedBarChartFillMode, + DtStackedBarChartLegend, +} from './src/stacked-bar-chart.util'; diff --git a/libs/barista-components/stacked-bar-chart/jest.config.js b/libs/barista-components/stacked-bar-chart/jest.config.js new file mode 100644 index 0000000000..5d5a5b712f --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/jest.config.js @@ -0,0 +1,27 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = { + name: 'stacked-bar-chart', + preset: '../../../jest.config.js', + coverageDirectory: + '../../../coverage/libs/barista-components/stacked-bar-chart', + snapshotSerializers: [ + 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', + 'jest-preset-angular/build/AngularSnapshotSerializer.js', + 'jest-preset-angular/build/HTMLCommentSerializer.js', + ], +}; diff --git a/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart-overlay.directive.ts b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart-overlay.directive.ts new file mode 100644 index 0000000000..53de4454ba --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart-overlay.directive.ts @@ -0,0 +1,29 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Directive } from '@angular/core'; + +/** + * Overlay directive to be used alongside with StackedBarChart. + * + * [content of overlay here] + * + */ +@Directive({ + selector: 'ng-template[dtStackedBarChartOverlay]', + exportAs: 'dtStackedBarChartOverlay', +}) +export class DtStackedBarChartOverlay {} diff --git a/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.html b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.html new file mode 100644 index 0000000000..e17e7119a9 --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.html @@ -0,0 +1,72 @@ +

+ + + {{ track.origin.label }} + + +
+ + +
+
+ + +
+ + + + + + + {{ _tracks[0].nodes[i].origin.value }} + + + {{ _tracks[0].nodes[i].valueRelative * 100 | dtPercent }} + + {{ legend.label }} + + diff --git a/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.layout.md b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.layout.md new file mode 100644 index 0000000000..968474f950 --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.layout.md @@ -0,0 +1,219 @@ +# Bar/Column + Label positioning + +As it's currently done, and probably will be recovered in the future, here's the +proposed solution for all the cases: + +```ts +/** Positioning of the label relative to the track */ +export type DtStackedBarChartLabelPosition = + | 'none' + | 'top' + | 'right' + | 'bottom' + | 'left'; +``` + +```scss +/** HOW TO of layout + +For positioning the elements we have 2 variables: +chart direction and label position. +To avoid having a switch case in the template +we apply a display grid to the container. +For each pair this would work like this: + +column + none + label: -; track: col auto +column + top + label: row 1; track: row 2 +column + right + label: col n+1; track: col n +column + bottom + label: row 2; track: row 1 +column + left + label: col n; track: col n+1 + +bar + none + label: -; track: row auto +bar + top + label: row n; track: row n+1 +bar + right + label: col 2; track: col 1 +bar + bottom + label: row n+1; track: row n +bar + left + label: col 1; track: col 2 + +The trick used is getting the index of the array +inside a css custom var and do the calculation in css. +Every layout will have a different 'drawer' +and every piece will fit in place. +This allows working with only one html +*/ + +/** Chart orientation */ +.dt-stacked-bar-chart-chart-container { + display: grid; + grid-auto-flow: dense; + align-self: stretch; + gap: $gap; +} + +.dt-stacked-bar-chart-bar { + align-items: center; + + .dt-stacked-bar-chart-axis-series { + display: none; + } + + .dt-stacked-bar-chart-track { + min-height: 1px; + height: var(--dt-stacked-bar-chart-max-bar-size); + } + + .dt-stacked-bar-chart-slice { + width: var(--dt-stacked-bar-chart-length); + } +} + +.dt-stacked-bar-chart-column { + justify-items: center; + .dt-stacked-bar-chart-series-label { + align-self: center; + } + + .dt-stacked-bar-chart-series-label, + .dt-stacked-bar-chart-track { + grid-row: 1; + } + + .dt-stacked-bar-chart-track { + flex-direction: column-reverse; + max-width: var(--dt-stacked-bar-chart-max-bar-size); + width: 100%; + } + + .dt-stacked-bar-chart-slice { + height: var(--dt-stacked-bar-chart-length); + } + + .dt-stacked-bar-chart-axis-series { + grid-column: 1/-1; + border-bottom: 1px solid $axis-color; + width: 100%; + grid-row: 1; + } +} + +/** Bar */ +.dt-stacked-bar-chart-bar.dt-stacked-bar-chart-label-none { + gap: $gap; + grid-template-columns: 1fr; +} + +.dt-stacked-bar-chart-bar.dt-stacked-bar-chart-label-top { + gap: 0; + grid-template-columns: 1fr; + @include gridPosition( + 'grid-row', + calc(2 * var(--dt-stacked-bar-chart-track-index) - 1), + calc(2 * var(--dt-stacked-bar-chart-track-index)) + ); + .dt-stacked-bar-chart-track { + margin-bottom: $gap; + } +} + +.dt-stacked-bar-chart-bar.dt-stacked-bar-chart-label-right { + grid-template-columns: 1fr auto; + @include gridPosition('grid-column', 2, 1); +} + +.dt-stacked-bar-chart-bar.dt-stacked-bar-chart-label-bottom { + gap: 0; + grid-template-columns: 1fr; + @include gridPosition( + 'grid-row', + calc(2 * var(--dt-stacked-bar-chart-track-index)), + calc(2 * var(--dt-stacked-bar-chart-track-index) - 1) + ); + // all but last + .dt-stacked-bar-chart-series-label:nth-last-of-type(n + 2) { + margin-bottom: $gap; + } +} + +.dt-stacked-bar-chart-bar.dt-stacked-bar-chart-label-left { + grid-template-columns: auto 1fr; + @include gridPosition('grid-column', 1, 2); +} + +/** Column */ +.dt-stacked-bar-chart-column.dt-stacked-bar-chart-label-none { + gap: 0; + grid-template-rows: 1fr; + grid-template-columns: repeat(var(--dt-stacked-bar-chart-track-amount), 1fr); +} + +.dt-stacked-bar-chart-column.dt-stacked-bar-chart-label-top { + grid-template-rows: auto 1fr; + @include gridPosition('grid-row', 1, 2); + grid-template-columns: repeat(var(--dt-stacked-bar-chart-track-amount), 1fr); + + .dt-stacked-bar-chart-axis-series { + grid-row: 2; + } + + .dt-stacked-bar-chart-track { + grid-column: var(--dt-stacked-bar-chart-track-index); + } +} + +.dt-stacked-bar-chart-column.dt-stacked-bar-chart-label-right { + @include gridPosition( + 'grid-column', + calc(2 * var(--dt-stacked-bar-chart-track-index)), + calc(2 * var(--dt-stacked-bar-chart-track-index) - 1) + ); + grid-template-columns: repeat( + calc(2 * var(--dt-stacked-bar-chart-track-amount)), + 1fr + ); + + .dt-stacked-bar-chart-series-label { + justify-self: start; + } + .dt-stacked-bar-chart-track { + justify-self: end; + } +} + +.dt-stacked-bar-chart-column.dt-stacked-bar-chart-label-bottom { + grid-template-rows: 1fr auto; + @include gridPosition('grid-row', 2, 1); + grid-template-columns: repeat(var(--dt-stacked-bar-chart-track-amount), 1fr); + + .dt-stacked-bar-chart-track { + grid-column: var(--dt-stacked-bar-chart-track-index); + } +} + +.dt-stacked-bar-chart-column.dt-stacked-bar-chart-label-left { + @include gridPosition( + 'grid-column', + calc(2 * var(--dt-stacked-bar-chart-track-index) - 1), + calc(2 * var(--dt-stacked-bar-chart-track-index)) + ); + grid-template-columns: repeat( + calc(2 * var(--dt-stacked-bar-chart-track-amount)), + 1fr + ); + + .dt-stacked-bar-chart-series-label { + justify-self: end; + } + .dt-stacked-bar-chart-track { + justify-self: start; + } +} +``` diff --git a/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.mock.ts b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.mock.ts new file mode 100644 index 0000000000..951f96906d --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.mock.ts @@ -0,0 +1,157 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DtColors } from '@dynatrace/barista-components/theming'; +import { DtStackedBarChartSeries } from './stacked-bar-chart.util'; + +export const stackedBarChartDemoDataCoffee: DtStackedBarChartSeries[] = [ + { + label: 'Espresso', + nodes: [ + { + value: 1, + label: 'Coffee', + }, + ], + }, + { + label: 'Macchiato', + nodes: [ + { + value: 2, + label: 'Coffee', + }, + { + value: 1, + label: 'Milk', + }, + ], + }, + { + label: 'Americano', + nodes: [ + { + value: 2, + label: 'Coffee', + }, + { + value: 3, + label: 'Water', + }, + ], + }, + { + label: 'Mocha', + nodes: [ + { + value: 2, + label: 'Coffee', + }, + { + value: 2, + label: 'Chocolate', + }, + { + value: 1, + label: 'Milk', + }, + ], + }, +]; + +export const stackedBarChartDemoDataShows: DtStackedBarChartSeries[] = [ + { + label: 'Lost', + nodes: [ + { + value: 25, + label: 'Season 1', + color: DtColors.RED_500, + }, + { + value: 24, + label: 'Season 2', + color: DtColors.ORANGE_400, + }, + { + value: 23, + label: 'Season 3', + color: DtColors.YELLOW_500, + }, + { + value: 14, + label: 'Season 4', + color: DtColors.GREEN_500, + }, + { + value: 17, + label: 'Season 5', + color: DtColors.BLUE_500, + }, + { + value: 18, + label: 'Season 6', + color: DtColors.PURPLE_500, + }, + ], + }, + { + label: 'Six feet under', + nodes: [ + { + value: 13, + label: 'Season 1', + }, + { + value: 13, + label: 'Season 2', + }, + { + value: 13, + label: 'Season 3', + }, + { + value: 12, + label: 'Season 4', + }, + { + value: 12, + label: 'Season 5', + }, + ], + }, + { + label: 'Halt and catch fire', + nodes: [ + { + value: 10, + label: 'Season 1', + }, + { + value: 10, + label: 'Season 2', + }, + { + value: 10, + label: 'Season 3', + }, + { + value: 10, + label: 'Season 4', + }, + ], + }, +]; diff --git a/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.module.ts b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.module.ts new file mode 100644 index 0000000000..5f35e6de8e --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.module.ts @@ -0,0 +1,29 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { DtStackedBarChart } from './stacked-bar-chart'; +import { DtStackedBarChartOverlay } from './stacked-bar-chart-overlay.directive'; +import { DtFormattersModule } from '@dynatrace/barista-components/formatters'; +import { DtLegendModule } from '@dynatrace/barista-components/legend'; + +@NgModule({ + imports: [CommonModule, DtFormattersModule, DtLegendModule], + exports: [DtStackedBarChart, DtStackedBarChartOverlay], + declarations: [DtStackedBarChart, DtStackedBarChartOverlay], +}) +export class DtStackedBarChartModule {} diff --git a/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.scss b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.scss new file mode 100644 index 0000000000..e84129070c --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.scss @@ -0,0 +1,247 @@ +@import '../../core/src/style/variables'; +@import '../../core/src/style/overlay'; + +/** VARS */ +$gap: 16px; +$bullet-height: 12px; +$track-color: $gray-200; +$selected-size: 4px; +$selected-color: rgba($turquoise-500, 0.2); +$selected-border-color: $turquoise-500; +$selected-border-size: 1px; +$hover-transition-time: 0.1s; +$hidden-color: $gray-300; +$axis-color: $gray-300; +$hidden-transition-time: 0.2s; +$hover-opacity: 0.8; + +/** HOW TO of layout + +For positioning the elements we have 2 variables: +chart direction and label position. +To avoid having a switch case in the template +we apply a display grid to the container. +For each pair this would work like this: + +column + none + label: -; track: col auto +column + top + label: row 1; track: row 2 +column + right + label: col n+1; track: col n +column + bottom + label: row 2; track: row 1 +column + left + label: col n; track: col n+1 + +bar + none + label: -; track: row auto +bar + top + label: row n; track: row n+1 +bar + right + label: col 2; track: col 1 +bar + bottom + label: row n+1; track: row n +bar + left + label: col 1; track: col 2 + +The trick used is getting the index of the array +inside a css custom var and do the calculation in css. +Every layout will have a different 'drawer' and +every piece will fit in place. +This allows working with only one html +*/ + +@mixin gridPosition($property, $label, $chart) { + .dt-stacked-bar-chart-track-label { + #{$property}: $label; + } + .dt-stacked-bar-chart-track { + #{$property}: $chart; + } +} + +:host { + display: grid; + align-items: center; + grid-template-rows: 1fr auto; + + &.dt-stacked-bar-chart-with-legend { + gap: $gap; + } +} + +/** Chart orientation */ +.dt-stacked-bar-chart-chart-container { + display: grid; + grid-auto-flow: dense; + align-self: stretch; + gap: $gap; +} + +.dt-stacked-bar-chart-bar { + align-items: center; + + .dt-stacked-bar-chart-axis-series { + display: none; + } + + .dt-stacked-bar-chart-track { + min-height: 1px; + height: var(--dt-stacked-bar-chart-max-bar-size); + } + + .dt-stacked-bar-chart-slice { + width: var(--dt-stacked-bar-chart-length); + } +} + +.dt-stacked-bar-chart-column { + justify-items: center; + .dt-stacked-bar-chart-track-label { + align-self: center; + } + + .dt-stacked-bar-chart-track-label, + .dt-stacked-bar-chart-track { + grid-row: 1; + } + + .dt-stacked-bar-chart-track { + flex-direction: column-reverse; + max-width: var(--dt-stacked-bar-chart-max-bar-size); + width: 100%; + } + + .dt-stacked-bar-chart-slice { + height: var(--dt-stacked-bar-chart-length); + } + + .dt-stacked-bar-chart-axis-series { + grid-column: 1/-1; + border-bottom: 1px solid $axis-color; + width: 100%; + grid-row: 1; + } +} + +/** Bar */ +.dt-stacked-bar-chart-bar { + grid-template-columns: auto 1fr; + @include gridPosition('grid-column', 1, 2); +} + +.dt-stacked-bar-chart-bar.dt-stacked-bar-chart-label-none { + gap: $gap; + grid-template-columns: 1fr; + @include gridPosition('grid-column', 0, 1); +} + +/** Column */ +.dt-stacked-bar-chart-column { + grid-template-rows: 1fr auto; + @include gridPosition('grid-row', 2, 1); + grid-template-columns: repeat(var(--dt-stacked-bar-chart-track-amount), 1fr); + + .dt-stacked-bar-chart-track { + grid-column: var(--dt-stacked-bar-chart-track-index); + } +} + +.dt-stacked-bar-chart-column.dt-stacked-bar-chart-label-none { + gap: 0; + grid-template-rows: 1fr; +} + +/** Track */ +.dt-stacked-bar-chart-track { + display: flex; +} +.dt-stacked-bar-chart-track-background { + background: $track-color; +} + +.dt-stacked-bar-chart-track-selectable { + .dt-stacked-bar-chart-slice { + cursor: pointer; + } +} + +.dt-stacked-bar-chart-slice { + background: var(--dt-stacked-bar-chart-color); + box-sizing: content-box; + transition: width $hidden-transition-time, height $hidden-transition-time, + opacity $hover-transition-time; +} + +.dt-stacked-bar-chart-track-hoverable { + .dt-stacked-bar-chart-slice:hover { + opacity: $hover-opacity; + } +} + +.dt-stacked-bar-chart-slice-selected { + cursor: default; + position: relative; + + &::before { + content: ' '; + display: block; + position: absolute; + border: $selected-border-size solid $selected-border-color; + border-radius: 3px; + + .dt-stacked-bar-chart-column & { + box-shadow: $selected-size 0 $selected-color inset, + -$selected-size 0 $selected-color inset; + right: -$selected-size; + left: -$selected-size; + top: 0; + bottom: 0; + } + + .dt-stacked-bar-chart-bar & { + box-shadow: 0 $selected-size $selected-color inset, + 0 #{-$selected-size} $selected-color inset; + border: $selected-border-size solid $selected-border-color; + top: -$selected-size; + bottom: -$selected-size; + right: 0; + left: 0; + } + } +} + +/** Legend */ +.dt-stacked-bar-chart-legend { + display: flex; + padding: 0; + justify-self: center; +} + +.dt-stacked-bar-chart-legend-symbol { + display: block; + content: ' '; + height: $bullet-height; + width: $bullet-height; + background: var(--dt-stacked-bar-chart-color); + transition: background $hidden-transition-time; + + &:hover { + opacity: $hover-opacity; + } +} + +.dt-stacked-bar-chart-legend-item-hidden { + .dt-stacked-bar-chart-legend-symbol { + background: $hidden-color; + } +} + +/** StackedBarChart overlay */ +/** The selector is combined with the cdk-overlay-pane to +increase the specificity to avoid edge cases where the cdk styles +are loaded after the component's styles in the angular.json */ +::ng-deep .cdk-overlay-pane.dt-stacked-bar-chart-overlay-panel { + @include dt-overlay-container(); +} diff --git a/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.spec.ts b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.spec.ts new file mode 100644 index 0000000000..eea3b27e00 --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.spec.ts @@ -0,0 +1,608 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { OverlayContainer } from '@angular/cdk/overlay'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { Component, ViewChild } from '@angular/core'; +import { + async, + ComponentFixture, + inject, + TestBed, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { + DtUiTestConfiguration, + DT_UI_TEST_CONFIG, +} from '@dynatrace/barista-components/core'; +import { DtIconModule } from '@dynatrace/barista-components/icon'; +import { DtThemingModule } from '@dynatrace/barista-components/theming'; +import { createComponent, dispatchFakeEvent } from '@dynatrace/testing/browser'; +import { DtStackedBarChart } from './stacked-bar-chart'; +import { stackedBarChartDemoDataCoffee } from './stacked-bar-chart.mock'; +import { DtStackedBarChartModule } from './stacked-bar-chart.module'; +import { + DtStackedBarChartFillMode, + DtStackedBarChartLegend, + DtStackedBarChartMode, + DtStackedBarChartNode, + DtStackedBarChartSeries, + DtStackedBarChartValueDisplayMode, +} from './stacked-bar-chart.util'; + +describe('DtStackedBarChart', () => { + let fixture: ComponentFixture; + let rootComponent: TestApp; + let component: DtStackedBarChart; + let overlayContainer: OverlayContainer; + let overlayContainerElement: HTMLElement; + const overlayConfig: DtUiTestConfiguration = { + attributeName: 'dt-ui-test-id', + constructOverlayAttributeValue(attributeName: string): string { + return `${attributeName}-overlay`; + }, + }; + + let selectedChangeSpy; + const getSliceByPosition = (track, slice) => { + return fixture.debugElement + .queryAll(By.css(selectors.track)) + [track].queryAll(By.css(selectors.slice))[slice]; + }; + + const selectors = { + overlay: '.dt-stacked-bar-chart-overlay-panel', + track: '.dt-stacked-bar-chart-track', + trackLabel: '.dt-stacked-bar-chart-track-label', + trackWithBackground: '.dt-stacked-bar-chart-track-background', + slice: '.dt-stacked-bar-chart-slice', + sliceSelected: '.dt-stacked-bar-chart-slice-selected', + axisSeries: '.dt-stacked-bar-chart-axis-series', + legend: 'dt-legend', + legendItem: 'dt-legend-item', + legendItemHidden: '.dt-stacked-bar-chart-legend-item-hidden', + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + DtIconModule.forRoot({ svgIconLocation: `{{name}}.svg` }), + DtStackedBarChartModule, + DtThemingModule, + ], + declarations: [TestApp, DefaultsTestApp], + providers: [{ provide: DT_UI_TEST_CONFIG, useValue: overlayConfig }], + }); + + TestBed.compileComponents(); + fixture = createComponent(TestApp); + + rootComponent = fixture.componentInstance; + component = fixture.debugElement.query(By.directive(DtStackedBarChart)) + .componentInstance; + + selectedChangeSpy = jest.spyOn(component.selectedChange, 'emit'); + })); + + describe('should have defaults', () => { + let defComponent; + + beforeEach(() => { + const defFixture = createComponent(DefaultsTestApp); + defComponent = defFixture.debugElement.query( + By.directive(DtStackedBarChart), + ).componentInstance; + }); + + const propsAndDefaults = { + selectable: false, + selected: [], + valueDisplayMode: 'none', + _max: undefined, + fillMode: 'relative', + visibleLegend: true, + visibleTrackBackground: true, + visibleLabel: true, + mode: 'bar', + maxTrackSize: 16, + }; + + Object.keys(propsAndDefaults).forEach((key) => + it(`${key} should be ${propsAndDefaults[key]}`, () => + expect(defComponent[key]).toEqual(propsAndDefaults[key])), + ); + }); + + describe('Series', () => { + it('should render after change', () => { + const tracks = fixture.debugElement.queryAll(By.css(selectors.track)); + const slice = getSliceByPosition(0, 0); + + expect(tracks.length).toEqual(stackedBarChartDemoDataCoffee.length); + + expect(slice.properties.style).toContain( + '--dt-stacked-bar-chart-length: 20%', + ); + }); + }); + + describe('Selectable + Selected', () => { + it('should select nodes when input', () => { + rootComponent.selectable = true; + rootComponent.selected = [ + stackedBarChartDemoDataCoffee[1], + stackedBarChartDemoDataCoffee[1].nodes[1], + ]; + fixture.detectChanges(); + + const sliceByPosition = getSliceByPosition(1, 1); + const selected = fixture.debugElement.query( + By.css(selectors.sliceSelected), + ); + + expect(selected).toBe(sliceByPosition); + }); + + it('should make a selection', () => { + const sliceByPosition = getSliceByPosition(1, 1); + dispatchFakeEvent(sliceByPosition.nativeElement, 'click'); + fixture.detectChanges(); + + const selected = fixture.debugElement.query( + By.css(selectors.sliceSelected), + ); + + expect(selectedChangeSpy).toHaveBeenCalledWith([ + component._tracks[1].origin, + component._tracks[1].nodes[1].origin, + ]); + expect(selected).toBe(sliceByPosition); + }); + + it('should not allow selection from input if disabled', () => { + rootComponent.selectable = false; + rootComponent.selected = [ + stackedBarChartDemoDataCoffee[1], + stackedBarChartDemoDataCoffee[1].nodes[1], + ]; + fixture.detectChanges(); + + const selected = fixture.debugElement.query( + By.css(selectors.sliceSelected), + ); + + expect(selected).toBe(null); + }); + + it('should not allow selection from template if disabled', () => { + rootComponent.selectable = false; + fixture.detectChanges(); + selectedChangeSpy.mockClear(); + + const sliceByPosition = getSliceByPosition(1, 1); + dispatchFakeEvent(sliceByPosition.nativeElement, 'click'); + + expect(selectedChangeSpy).not.toHaveBeenCalled(); + }); + }); + + describe('Value Display Mode', () => { + describe('Single tracked', () => { + beforeEach(() => { + rootComponent.visibleLegend = true; + + rootComponent.series = [stackedBarChartDemoDataCoffee[3]]; + fixture.detectChanges(); + }); + + it('should switch to ABSOLUTE display when set', () => { + rootComponent.valueDisplayMode = 'absolute'; + fixture.detectChanges(); + + const legendItem = fixture.debugElement.queryAll( + By.css(selectors.legendItem), + )[1]; + + expect(legendItem.nativeElement.textContent.trim()).toBe( + '2 Chocolate', + ); + }); + + it('should switch to PERCENT display when set', () => { + rootComponent.valueDisplayMode = 'percent'; + fixture.detectChanges(); + + const legendItem = fixture.debugElement.queryAll( + By.css(selectors.legendItem), + )[1]; + + expect(legendItem.nativeElement.textContent.trim()).toBe( + '40 % Chocolate', + ); + }); + + it('should switch to NONE display when set', () => { + rootComponent.valueDisplayMode = 'none'; + fixture.detectChanges(); + + const legendItem = fixture.debugElement.queryAll( + By.css(selectors.legendItem), + )[1]; + + expect(legendItem.nativeElement.textContent.trim()).toBe('Chocolate'); + }); + }); + + it('should not display value if multi series', () => { + rootComponent.valueDisplayMode = 'percent'; + fixture.detectChanges(); + + const legendItem = fixture.debugElement.queryAll( + By.css(selectors.legendItem), + )[1]; + + expect(legendItem.nativeElement.textContent.trim()).toBe('Milk'); + }); + }); + + describe('Max + Fill mode', () => { + it('should allow a max with fillMode=relative', () => { + rootComponent.max = 100; + fixture.detectChanges(); + + const sliceByPosition = getSliceByPosition(0, 0); + + expect(sliceByPosition.properties.style).toContain( + '--dt-stacked-bar-chart-length: 1%', + ); + }); + + it('should ignore max with fillMode=full', () => { + rootComponent.fillMode = 'full'; + rootComponent.max = 100; + fixture.detectChanges(); + + const sliceByPosition = getSliceByPosition(0, 0); + + expect(sliceByPosition.properties.style).toContain( + '--dt-stacked-bar-chart-length: 100%', + ); + }); + + it('should fill the whole bar if fillMode=full', () => { + rootComponent.fillMode = 'full'; + fixture.detectChanges(); + + const sliceByPosition = getSliceByPosition(0, 0); + + expect(sliceByPosition.properties.style).toContain( + '--dt-stacked-bar-chart-length: 100%', + ); + }); + + it('should take into account the rest of series when no max if fillMode=relative', () => { + const sliceByPosition = getSliceByPosition(0, 0); + + expect(sliceByPosition.properties.style).toContain( + '--dt-stacked-bar-chart-length: 20%', + ); + }); + }); + + describe('Legends', () => { + it('should show the legend', () => { + rootComponent.visibleLegend = true; + fixture.detectChanges(); + + const legend = fixture.debugElement.query(By.css(selectors.legend)); + + expect(legend).toBeTruthy(); + }); + + it('should hide the legend', () => { + rootComponent.visibleLegend = false; + fixture.detectChanges(); + + const legend = fixture.debugElement.query(By.css(selectors.legend)); + + expect(legend).toBeFalsy(); + }); + + it('should hide nodes if hidden in legend', () => { + const legends = component.legends.slice(); + // Coffee node + legends[0].visible = false; + rootComponent.legends = legends; + fixture.detectChanges(); + + const hiddenLegendItem = fixture.debugElement.query( + By.css(selectors.legendItemHidden), + ); + const tracks = fixture.debugElement.queryAll(By.css(selectors.track)); + const hiddenSlices = fixture.debugElement + .queryAll(By.css(selectors.slice)) + .filter((slice) => + slice.properties.style.includes('--dt-stacked-bar-chart-length: 0'), + ); + + // item to be hidden + expect(hiddenLegendItem.nativeElement.textContent.trim()).toBe( + legends[0].label, + ); + // tracks to be all shown + expect(tracks.length).toBe(4); + // nodes to be hidden, coffee is present in all of them + expect(hiddenSlices.length).toBe(4); + }); + + it('should toggle legend on click', () => { + const firstLegendItem = fixture.debugElement.query( + By.css(selectors.legendItem), + ); + dispatchFakeEvent(firstLegendItem.nativeElement, 'click'); + fixture.detectChanges(); + + const hiddenLegendItems = fixture.debugElement + .queryAll(By.css(selectors.legendItemHidden)) + .map((item) => item.nativeElement.textContent.trim()); + + expect(hiddenLegendItems).toEqual(['Coffee']); + }); + + it('should not allow all legends to be hidden', () => { + const legends = component.legends.slice(); + legends.forEach((legend, i) => (legend.visible = i === 0)); + rootComponent.legends = legends; + fixture.detectChanges(); + + const firstLegendItem = fixture.debugElement.query( + By.css(selectors.legendItem), + ); + dispatchFakeEvent(firstLegendItem.nativeElement, 'click'); + fixture.detectChanges(); + + const hiddenLegendItems = fixture.debugElement + .queryAll(By.css(selectors.legendItemHidden)) + .map((item) => item.nativeElement.textContent.trim()); + + expect(hiddenLegendItems).toEqual(['Milk', 'Water', 'Chocolate']); + }); + }); + + describe('Track background', () => { + it('should show the track background by default', () => { + const tracksWithBackground = fixture.debugElement.queryAll( + By.css(selectors.trackWithBackground), + ); + + expect(tracksWithBackground.length).toBe(4); + }); + + it('should hide the track background when indicated', () => { + rootComponent.visibleTrackBackground = false; + fixture.detectChanges(); + + const tracksWithBackground = fixture.debugElement.queryAll( + By.css(selectors.trackWithBackground), + ); + + expect(tracksWithBackground.length).toBe(0); + }); + }); + + describe('Mode', () => { + describe('Bar', () => { + it('should display all the tracks and slices', () => { + const tracks = fixture.debugElement.queryAll(By.css(selectors.track)); + const slices = fixture.debugElement.queryAll(By.css(selectors.slice)); + + expect(tracks.length).toBe(4); + expect(slices.length).toBe(8); + }); + + it('should display left aligned labels', () => { + rootComponent.visibleLabel = true; + fixture.detectChanges(); + + const labels = fixture.debugElement.queryAll( + By.css(selectors.trackLabel), + ); + + expect(labels.length).toBe(4); + }); + + it('should not display the label if required', () => { + rootComponent.visibleLabel = false; + fixture.detectChanges(); + + const labels = fixture.debugElement.queryAll( + By.css(selectors.trackLabel), + ); + + expect(labels.length).toBe(0); + }); + + it('should not display the series axis', () => { + const axis = fixture.debugElement.query(By.css(selectors.axisSeries)); + + expect(axis).toBeFalsy(); + }); + }); + + describe('Column', () => { + beforeEach(function (): void { + rootComponent.mode = 'column'; + fixture.detectChanges(); + }); + + it('should display all the tracks and slices', () => { + const tracks = fixture.debugElement.queryAll(By.css(selectors.track)); + const slices = fixture.debugElement.queryAll(By.css(selectors.slice)); + + expect(tracks.length).toBe(4); + expect(slices.length).toBe(8); + }); + + it('should display bottom aligned labels', () => { + rootComponent.visibleLabel = true; + fixture.detectChanges(); + + const labels = fixture.debugElement.queryAll( + By.css(selectors.trackLabel), + ); + + expect(labels.length).toBe(4); + }); + + it('should not display the label if required', () => { + rootComponent.visibleLabel = false; + fixture.detectChanges(); + + const labels = fixture.debugElement.queryAll( + By.css(selectors.trackLabel), + ); + + expect(labels.length).toBe(0); + }); + + it('should display the series axis', () => { + const axis = fixture.debugElement.query(By.css(selectors.axisSeries)); + + expect(axis).toBeTruthy(); + }); + }); + }); + + describe('Overlay', () => { + beforeEach(inject([OverlayContainer], (oc: OverlayContainer) => { + overlayContainer = oc; + overlayContainerElement = oc.getContainerElement(); + })); + + it('should have an overlay container defined', () => { + expect(overlayContainer).toBeDefined(); + }); + + it('should not display an overlay if missing', () => { + rootComponent.hasOverlay = false; + fixture.detectChanges(); + + const firstSlice = fixture.debugElement.query(By.css(selectors.slice)); + + dispatchFakeEvent(firstSlice.nativeElement, 'mouseenter'); + fixture.detectChanges(); + + const overlayPane = overlayContainerElement.querySelector( + selectors.overlay, + ); + expect(overlayPane).toBeNull(); + }); + + it('should display an overlay when hovering over a slice', () => { + const firstSlice = fixture.debugElement.query(By.css(selectors.slice)); + + dispatchFakeEvent(firstSlice.nativeElement, 'mouseenter'); + fixture.detectChanges(); + + const overlayPane = overlayContainerElement.querySelector( + selectors.overlay, + ); + expect(overlayPane).toBeDefined(); + + const overlayContent = (overlayPane!.textContent || '').trim(); + expect(overlayContent).toBe('Coffee'); + }); + + it('should remove the overlay when moving the mouse away from the slice', () => { + const firstSlice = fixture.debugElement.query(By.css(selectors.slice)); + let overlayPane = overlayContainerElement.querySelector( + selectors.overlay, + ); + + dispatchFakeEvent(firstSlice.nativeElement, 'mouseenter'); + fixture.detectChanges(); + overlayPane = overlayContainerElement.querySelector(selectors.overlay); + + dispatchFakeEvent(firstSlice.nativeElement, 'mouseleave'); + fixture.detectChanges(); + overlayPane = overlayContainerElement.querySelector(selectors.overlay); + + expect(overlayPane).toBeNull(); + }); + }); +}); + +/** Test component that contains an DtStackedBarChart. */ +@Component({ + selector: 'dt-test-app', + template: ` + + +
+ {{ tooltip.origin.label }} +
+
+
+ `, +}) +class TestApp { + series: DtStackedBarChartSeries[] = stackedBarChartDemoDataCoffee; + selectable: boolean = true; + selected: [DtStackedBarChartSeries, DtStackedBarChartNode] | [] = []; + valueDisplayMode: DtStackedBarChartValueDisplayMode; + max: number; + fillMode: DtStackedBarChartFillMode = 'relative'; + legends: DtStackedBarChartLegend[]; + visibleLegend: boolean = true; + visibleTrackBackground: boolean = true; + visibleLabel: boolean = true; + mode: DtStackedBarChartMode; + maxTrackSize: number; + + theme = 'blue'; + hasOverlay: boolean = true; + @ViewChild(DtStackedBarChart) stackedBarChart: DtStackedBarChart; +} + +/** Test component that contains an DtStackedBarChart. */ +@Component({ + selector: 'dt-defaults-test-app', + template: ` + + + `, +}) +class DefaultsTestApp { + series: DtStackedBarChartSeries[] = stackedBarChartDemoDataCoffee; + theme = 'blue'; + + @ViewChild(DtStackedBarChart) stackedBarChart: DtStackedBarChart; +} diff --git a/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.ts b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.ts new file mode 100644 index 0000000000..679fdcee20 --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.ts @@ -0,0 +1,471 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ConnectedPosition, + Overlay, + OverlayConfig, + OverlayRef, +} from '@angular/cdk/overlay'; +import { TemplatePortal } from '@angular/cdk/portal'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + ElementRef, + EventEmitter, + Inject, + Input, + OnDestroy, + Optional, + Output, + SkipSelf, + TemplateRef, + ViewContainerRef, + ViewEncapsulation, +} from '@angular/core'; +import { + dtSetUiTestAttribute, + DtUiTestConfiguration, + DT_UI_TEST_CONFIG, +} from '@dynatrace/barista-components/core'; +import { DtTheme, DtColors } from '@dynatrace/barista-components/theming'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { DtStackedBarChartNode } from '..'; +import { DtStackedBarChartOverlay } from './stacked-bar-chart-overlay.directive'; +import { + DtStackedBarChartFilledSeries, + DtStackedBarChartFillMode, + DtStackedBarChartLegend, + DtStackedBarChartMode, + DtStackedBarChartSeries, + DtStackedBarChartTooltipData, + DtStackedBarChartValueDisplayMode, + fillSeries, + getLegends, + getSeriesWithState, + getTotalMaxValue, + updateNodesVisibility as updateNodesWithLegend, +} from './stacked-bar-chart.util'; +import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; + +const OVERLAY_PANEL_CLASS = 'dt-stacked-bar-chart-overlay-panel'; +const OVERLAY_GUTTER = 8; + +const BAR_OVERLAY_POSITIONS: ConnectedPosition[] = [ + { + originX: 'center', + originY: 'top', + overlayX: 'center', + overlayY: 'bottom', + offsetY: -OVERLAY_GUTTER, + }, + { + originX: 'center', + originY: 'bottom', + overlayX: 'center', + overlayY: 'top', + offsetY: OVERLAY_GUTTER, + }, + { + originX: 'center', + originY: 'top', + overlayX: 'start', + overlayY: 'bottom', + offsetY: -OVERLAY_GUTTER, + }, + { + originX: 'center', + originY: 'top', + overlayX: 'end', + overlayY: 'bottom', + offsetY: -OVERLAY_GUTTER, + }, + { + originX: 'center', + originY: 'bottom', + overlayX: 'start', + overlayY: 'top', + offsetY: OVERLAY_GUTTER, + }, + { + originX: 'center', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top', + offsetY: OVERLAY_GUTTER, + }, +]; +const COLUMN_OVERLAY_POSITIONS: ConnectedPosition[] = [ + { + originX: 'end', + originY: 'center', + overlayX: 'start', + overlayY: 'center', + offsetX: OVERLAY_GUTTER, + }, + { + originX: 'start', + originY: 'center', + overlayX: 'end', + overlayY: 'center', + offsetX: -OVERLAY_GUTTER, + }, + { + originX: 'end', + originY: 'center', + overlayX: 'start', + overlayY: 'top', + offsetX: OVERLAY_GUTTER, + }, + { + originX: 'start', + originY: 'center', + overlayX: 'end', + overlayY: 'top', + offsetX: -OVERLAY_GUTTER, + }, + { + originX: 'end', + originY: 'center', + overlayX: 'start', + overlayY: 'bottom', + offsetX: OVERLAY_GUTTER, + }, + { + originX: 'start', + originY: 'center', + overlayX: 'end', + overlayY: 'bottom', + offsetX: -OVERLAY_GUTTER, + }, +]; + +/** + * TODO: + * get color from legends + */ + +@Component({ + selector: 'dt-stacked-bar-chart', + exportAs: 'dtStackedBarChart', + templateUrl: 'stacked-bar-chart.html', + styleUrls: ['stacked-bar-chart.scss'], + preserveWhitespaces: false, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.Emulated, + host: { + '[class.dt-stacked-bar-chart-with-legend]': 'visibleLegend', + }, +}) +export class DtStackedBarChart implements OnDestroy { + /** Overlay reference */ + @ContentChild(DtStackedBarChartOverlay, { read: TemplateRef }) + _overlay: TemplateRef; + + /** @internal Slices to be painted */ + _tracks: DtStackedBarChartFilledSeries[] = []; + + /** Array of series with their nodes. */ + @Input() + get series(): DtStackedBarChartSeries[] { + return this._series; + } + set series(value: DtStackedBarChartSeries[]) { + if (value !== this._series) { + this._series = value; + this._legends = getLegends(value, this._theme); + this._filledSeries = fillSeries(value, this._legends); + + this._canShowValue = this._series.length === 1; + + this._render(); + } + } + private _series: DtStackedBarChartSeries[]; + /** Series with filled nodes */ + private _filledSeries: DtStackedBarChartFilledSeries[]; + + /** Allow selections to be made on chart */ + @Input() + get selectable(): boolean { + return this._selectable; + } + set selectable(value: boolean) { + if (value !== this._selectable) { + this._toggleSelect(); + this._selectable = value; + } + } + _selectable: boolean = false; + + /** Current selection [series, node] */ + @Input() + get selected(): [DtStackedBarChartSeries, DtStackedBarChartNode] | [] { + return this._selected; + } + set selected([series, node]: + | [DtStackedBarChartSeries, DtStackedBarChartNode] + | []) { + // if selected node is different than current + if (this._selected[1] !== node) { + this._toggleSelect(series, node); + } + } + private _selected: [DtStackedBarChartSeries, DtStackedBarChartNode] | [] = []; + + @Output() selectedChange: EventEmitter< + [DtStackedBarChartSeries, DtStackedBarChartNode] | [] + > = new EventEmitter(); + + /** Max value in the chart */ + @Input() + get max(): number | undefined { + return this._max !== undefined ? this._max : getTotalMaxValue(this.series); + } + set max(value: number | undefined) { + if (value !== this._max) { + this._max = value; + this._render(); + } + } + private _max: number | undefined; + + /** Whether each bar should be filled completely or should take into account their siblings and max */ + @Input() + get fillMode(): DtStackedBarChartFillMode { + return this._fillMode; + } + set fillMode(value: DtStackedBarChartFillMode) { + if (value !== this._fillMode) { + this._fillMode = value; + this._render(); + } + } + private _fillMode: DtStackedBarChartFillMode = 'relative'; + + /** Sets the display mode for the stacked-bar-chart values in legend to either 'none' 'percent' or 'absolute'. Only valid for single track chart. */ + @Input() valueDisplayMode: DtStackedBarChartValueDisplayMode = 'none'; + /** @internal Will be true if display mode is not 'none' and only has one series */ + _canShowValue: boolean; + + /** Array of legends that can be used to toggle bar nodes. Useful when the legend used is outside this component */ + @Input() + get legends(): DtStackedBarChartLegend[] { + return this._legends; + } + set legends(values: DtStackedBarChartLegend[]) { + if (values !== undefined && values !== this._legends) { + this._legends = values; + updateNodesWithLegend(this._filledSeries, this._legends); + + this._render(); + } + } + _legends: DtStackedBarChartLegend[]; + + /** Visibility of the legend */ + @Input() visibleLegend: boolean = true; + + /** Whether background should be transparent or show a background. Default: true */ + @Input() visibleTrackBackground: boolean = true; + + /** Visibility of series label */ + @Input() visibleLabel: boolean = true; + + /** Display mode */ + @Input() mode: DtStackedBarChartMode = 'bar'; + + /** Maximum size of the track */ + @Input() maxTrackSize: number = 16; + + /** Template portal for the default overlay */ + // tslint:disable-next-line: use-default-type-parameter no-any + private _portal: TemplatePortal | null; + + /** Reference to the open overlay. */ + private _overlayRef: OverlayRef; + + /** Subject to be called upon component destroy to remove pending subscriptions */ + private readonly _destroy$ = new Subject(); + + constructor( + private readonly _overlayService: Overlay, + private readonly _viewContainerRef: ViewContainerRef, + private readonly _cdr: ChangeDetectorRef, + // TODO: remove this sanitizer when ivy is no longer opt out + private readonly _sanitizer: DomSanitizer, + @Optional() @SkipSelf() private readonly _theme: DtTheme, + private readonly _elementRef?: ElementRef, + @Optional() + @Inject(DT_UI_TEST_CONFIG) + private readonly _config?: DtUiTestConfiguration, + ) { + if (this._theme) { + this._theme._stateChanges + .pipe(takeUntil(this._destroy$)) + .subscribe(() => { + console.log(this._theme.name); + + this.series = this._series.slice(); + this._cdr.markForCheck(); + }); + } + } + + ngOnDestroy(): void { + this._destroy$.next(); + this._destroy$.complete(); + } + + /** Toggle the selection of an element */ + _toggleSelect( + series?: DtStackedBarChartSeries, + node?: DtStackedBarChartNode, + // selected?: boolean, + ): void { + if (this._selectable) { + if (series && node && this._selected[1] !== node) { + this._selected = [series, node]; + } else { + this._selected = []; + } + this.selectedChange.emit(this._selected); + + this._render(); + } else { + this._selected = []; + } + } + + /** Toggle the visibility of an element */ + _toggleLegend(slice: DtStackedBarChartLegend): void { + // don't allow hiding last element + if ( + this._legends.filter((node) => node.visible).length > 1 || + !slice.visible + ) { + slice.visible = !slice.visible; + updateNodesWithLegend(this._filledSeries, this._legends); + + this._render(); + } + } + + /** Track array by label in order to have transitions we have to track the elements in the list */ + _trackByFn( + _: number, + item: DtStackedBarChartTooltipData | DtStackedBarChartFilledSeries, + ): string { + return item.origin.label; + } + + /** Calculate current state */ + private _render(): void { + this._tracks = getSeriesWithState( + this._filledSeries, + this._selected, + this._fillMode === 'relative' ? this.max : undefined, + ); + } + + /** Programmatically close the overlay. */ + _closeOverlay(): void { + this._dismissOverlay(); + } + + /** Puts the overlay in place. */ + _openOverlay(event: MouseEvent, node: DtStackedBarChartTooltipData): void { + // const origin = this._getOriginFromSlice(event); + + this._dismissOverlay(); + if (event.target) { + this._createOverlay(event.target as HTMLElement, node); + } + } + + /** Creates the overlay and attaches it. */ + private _createOverlay( + origin: HTMLElement, + node: DtStackedBarChartTooltipData, + ): void { + // If we do not have an overlay defined, we do not need to attach it + if (!this._overlay) { + return; + } + + // Create the template portal + if (!this._portal) { + // tslint:disable-next-line: no-any + this._portal = new TemplatePortal( + this._overlay, + this._viewContainerRef, + { $implicit: node }, + ); + } + + const positionStrategy = this._overlayService + .position() + .flexibleConnectedTo(origin) + // .setOrigin(origin) + .withPositions( + this.mode === 'bar' ? BAR_OVERLAY_POSITIONS : COLUMN_OVERLAY_POSITIONS, + ) + .withFlexibleDimensions(true) + .withPush(false) + .withGrowAfterOpen(true) + .withViewportMargin(0) + .withLockedPosition(false); + + const overlayConfig = new OverlayConfig({ + positionStrategy, + panelClass: OVERLAY_PANEL_CLASS, + backdropClass: 'cdk-overlay-transparent-backdrop', + }); + this._overlayRef = this._overlayService.create(overlayConfig); + + // If the portal is not yet attached to the overlay, attach it. + if (!this._overlayRef.hasAttached()) { + this._overlayRef.attach(this._portal); + } + dtSetUiTestAttribute( + this._overlayRef.overlayElement, + this._overlayRef.overlayElement.id, + this._elementRef, + this._config, + ); + } + + /** Dismisses the overlay. */ + private _dismissOverlay(): void { + if (this._overlayRef) { + this._overlayRef.detach(); + this._portal = null; + } + } + + /** + * Sanitization of the custom property is necessary as, custom property assignments do not work + * in a viewEngine setup. This can be removed with angular version 10, if ivy is no longer opt out. + */ + _sanitizeCSS(styles: [string, string | number | DtColors][]): SafeStyle { + return this._sanitizer.bypassSecurityTrustStyle( + styles.map(([prop, value]) => `${prop}: ${value}`).join('; '), + ); + } +} diff --git a/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.util.spec.ts b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.util.spec.ts new file mode 100644 index 0000000000..de68d9247e --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.util.spec.ts @@ -0,0 +1,545 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + DtTheme, + DtThemingModule, + DT_CHART_COLOR_PALETTES, + DT_CHART_COLOR_PALETTE_ORDERED, +} from '@dynatrace/barista-components/theming'; +import { + stackedBarChartDemoDataCoffee, + stackedBarChartDemoDataShows, +} from './stacked-bar-chart.mock'; +import { + fillSeries, + getLegends, + getSeriesWithState, + getTotalMaxValue, + updateNodesVisibility, +} from './stacked-bar-chart.util'; + +describe('StackedBarChart util', () => { + const series = stackedBarChartDemoDataCoffee; + const legends = getLegends(series); + const filledSeries = fillSeries(series, legends); + const palette = DT_CHART_COLOR_PALETTE_ORDERED; + + describe('fillSeries', () => { + it('should fill the series and nodes', () => { + const expected = [ + { + nodes: [ + { + color: palette[0], + origin: { label: 'Coffee', value: 1 }, + selected: false, + seriesOrigin: series[0], + valueRelative: 1, + visible: true, + }, + ], + origin: series[0], + }, + { + nodes: [ + { + color: palette[0], + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[1], + valueRelative: 0.6666666666666666, + visible: true, + }, + { + color: palette[1], + origin: { label: 'Milk', value: 1 }, + selected: false, + seriesOrigin: series[1], + valueRelative: 0.3333333333333333, + visible: true, + }, + ], + origin: series[1], + }, + { + nodes: [ + { + color: palette[0], + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[2], + valueRelative: 0.4, + visible: true, + }, + { + color: palette[2], + origin: { label: 'Water', value: 3 }, + selected: false, + seriesOrigin: series[2], + valueRelative: 0.6, + visible: true, + }, + ], + origin: series[2], + }, + { + nodes: [ + { + color: palette[0], + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.4, + visible: true, + }, + { + color: palette[3], + origin: { label: 'Chocolate', value: 2 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.4, + visible: true, + }, + { + color: palette[1], + origin: { label: 'Milk', value: 1 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.2, + visible: true, + }, + ], + origin: series[3], + }, + ]; + const actual = fillSeries(series, legends); + + expect(actual).toEqual(expected); + }); + }); + + describe('getSeriesWithState', () => { + it('should return the nodes with filled state', () => { + const expected = [ + { + nodes: [ + { + color: palette[0], + length: '100%', + origin: { label: 'Coffee', value: 1 }, + selected: false, + seriesOrigin: series[0], + valueRelative: 1, + visible: true, + }, + ], + origin: series[0], + }, + { + nodes: [ + { + color: palette[0], + length: '66.66666666666667%', + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[1], + valueRelative: 0.6666666666666666, + visible: true, + }, + { + color: palette[1], + length: '33.333333333333336%', + origin: { label: 'Milk', value: 1 }, + selected: false, + seriesOrigin: series[1], + valueRelative: 0.3333333333333333, + visible: true, + }, + ], + origin: series[1], + }, + { + nodes: [ + { + color: palette[0], + length: '40%', + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[2], + valueRelative: 0.4, + visible: true, + }, + { + color: palette[2], + length: '60%', + origin: { label: 'Water', value: 3 }, + selected: false, + seriesOrigin: series[2], + valueRelative: 0.6, + visible: true, + }, + ], + origin: series[2], + }, + { + nodes: [ + { + color: palette[0], + length: '40%', + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.4, + visible: true, + }, + { + color: palette[3], + length: '40%', + origin: { label: 'Chocolate', value: 2 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.4, + visible: true, + }, + { + color: palette[1], + length: '20%', + origin: { label: 'Milk', value: 1 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.2, + visible: true, + }, + ], + origin: series[3], + }, + ]; + const actual = getSeriesWithState(filledSeries, []); + + expect(actual).toEqual(expected); + }); + + it('should return the nodes with filled state WHEN one is selected', () => { + const expected = [ + { + nodes: [ + { + color: palette[0], + length: '100%', + origin: { label: 'Coffee', value: 1 }, + selected: false, + seriesOrigin: series[0], + valueRelative: 1, + visible: true, + }, + ], + origin: series[0], + }, + { + nodes: [ + { + color: palette[0], + length: '66.66666666666667%', + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[1], + valueRelative: 0.6666666666666666, + visible: true, + }, + { + color: palette[1], + length: '33.333333333333336%', + origin: { label: 'Milk', value: 1 }, + selected: true, + seriesOrigin: series[1], + valueRelative: 0.3333333333333333, + visible: true, + }, + ], + origin: series[1], + }, + { + nodes: [ + { + color: palette[0], + length: '40%', + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[2], + valueRelative: 0.4, + visible: true, + }, + { + color: palette[2], + length: '60%', + origin: { label: 'Water', value: 3 }, + selected: false, + seriesOrigin: series[2], + valueRelative: 0.6, + visible: true, + }, + ], + origin: series[2], + }, + { + nodes: [ + { + color: palette[0], + length: '40%', + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.4, + visible: true, + }, + { + color: palette[3], + length: '40%', + origin: { label: 'Chocolate', value: 2 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.4, + visible: true, + }, + { + color: palette[1], + length: '20%', + origin: { label: 'Milk', value: 1 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.2, + visible: true, + }, + ], + origin: series[3], + }, + ]; + const actual = getSeriesWithState(filledSeries, [ + series[1], + series[1].nodes[1], + ]); + + expect(actual).toEqual(expected); + }); + }); + + describe('updateNodesVisibility', () => { + it('should match legend visibility to nodes visibility', () => { + const expected = [ + { + nodes: [ + { + color: palette[0], + origin: { label: 'Coffee', value: 1 }, + selected: false, + seriesOrigin: series[0], + valueRelative: 1, + visible: false, + }, + ], + origin: series[0], + }, + { + nodes: [ + { + color: palette[0], + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[1], + valueRelative: 0.6666666666666666, + visible: false, + }, + { + color: palette[1], + origin: { label: 'Milk', value: 1 }, + selected: false, + seriesOrigin: series[1], + valueRelative: 0.3333333333333333, + visible: true, + }, + ], + origin: series[1], + }, + { + nodes: [ + { + color: palette[0], + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[2], + valueRelative: 0.4, + visible: false, + }, + { + color: palette[2], + origin: { label: 'Water', value: 3 }, + selected: false, + seriesOrigin: series[2], + valueRelative: 0.6, + visible: true, + }, + ], + origin: series[2], + }, + { + nodes: [ + { + color: palette[0], + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.4, + visible: false, + }, + { + color: palette[3], + origin: { label: 'Chocolate', value: 2 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.4, + visible: true, + }, + { + color: palette[1], + origin: { label: 'Milk', value: 1 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.2, + visible: true, + }, + ], + origin: series[3], + }, + ]; + + const alteredLegends = legends.slice(); + alteredLegends[0].visible = false; + const actual = updateNodesVisibility(filledSeries, alteredLegends); + + expect(actual).toEqual(expected); + }); + }); + + describe('getLegends', () => { + it('should return an array of legends', () => { + const expected = [ + { color: palette[0], label: 'Coffee', visible: true }, + { color: palette[1], label: 'Milk', visible: true }, + { color: palette[2], label: 'Water', visible: true }, + { color: palette[3], label: 'Chocolate', visible: true }, + ]; + const legendList = getLegends(series); + + expect(legendList).toEqual(expected); + }); + + it('should keep the assigned color', () => { + const expected = [ + { + color: stackedBarChartDemoDataShows[0].nodes[0].color, + label: 'Season 1', + visible: true, + }, + { + color: stackedBarChartDemoDataShows[0].nodes[1].color, + label: 'Season 2', + visible: true, + }, + { + color: stackedBarChartDemoDataShows[0].nodes[2].color, + label: 'Season 3', + visible: true, + }, + { + color: stackedBarChartDemoDataShows[0].nodes[3].color, + label: 'Season 4', + visible: true, + }, + { + color: stackedBarChartDemoDataShows[0].nodes[4].color, + label: 'Season 5', + visible: true, + }, + { + color: stackedBarChartDemoDataShows[0].nodes[5].color, + label: 'Season 6', + visible: true, + }, + ]; + const legendList = getLegends(stackedBarChartDemoDataShows); + + expect(legendList).toEqual(expected); + }); + + it('should get the colors from theme if less than 4 items', () => { + TestBed.configureTestingModule({ + imports: [DtThemingModule], + declarations: [TestApp], + }).compileComponents(); + const fixture: ComponentFixture = TestBed.createComponent( + TestApp, + ); + fixture.detectChanges(); + + const expected = [ + { + color: DT_CHART_COLOR_PALETTES.royalblue[0], + label: 'Coffee', + visible: true, + }, + { + color: DT_CHART_COLOR_PALETTES.royalblue[1], + label: 'Chocolate', + visible: true, + }, + { + color: DT_CHART_COLOR_PALETTES.royalblue[2], + label: 'Milk', + visible: true, + }, + ]; + const legendList = getLegends( + [stackedBarChartDemoDataCoffee[3]], + fixture.componentInstance.dtThemeInstance, + ); + + expect(legendList).toEqual(expected); + }); + }); + + describe('getTotalMaxValue', () => { + it("should return sum of values for node's nodes", () => { + const expected = 5; + const actual = getTotalMaxValue(series); + + expect(actual).toEqual(expected); + }); + }); +}); + +@Component({ + selector: 'dt-test-app', + template: `
`, +}) +class TestApp { + theme = 'royalblue'; + + @ViewChild(DtTheme, { static: true }) + dtThemeInstance: DtTheme; +} diff --git a/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.util.ts b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.util.ts new file mode 100644 index 0000000000..1e284206c0 --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/src/stacked-bar-chart.util.ts @@ -0,0 +1,249 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + DtColors, + DtTheme, + getDtChartColorPalette, +} from '@dynatrace/barista-components/theming'; + +/** + * Definition a series with all its nodes + */ +export interface DtStackedBarChartSeries { + /** Label of the series */ + label: string; + /** Nodes for this series */ + nodes: DtStackedBarChartNode[]; +} + +/** + * @internal Definition of series containing extended information for every mode + */ +export interface DtStackedBarChartFilledSeries { + /** Original series */ + origin: DtStackedBarChartSeries; + /** Filled nodes for this series */ + nodes: DtStackedBarChartTooltipData[]; +} + +/** + * Definition of a legend item + */ +export interface DtStackedBarChartLegend { + /** Label of the node */ + label: string; + /** Color assigned */ + color: DtColors | string; + /** Whether it should be visible */ + visible: boolean; +} + +/** + * DtStackedBarChartNode represents a single node within the sunburst datastructure. + */ +export interface DtStackedBarChartNode { + /** Label of the node to be shown */ + label: string; + /** Optional if it has children. Numeric value used to calculate the slices. If it has children you can skip it and it will be calculated based on them */ + value: number; + /** Color to be used */ + color?: DtColors | string; +} + +/** Extended information of DtStackedBarChartNode containing useful information that can be used inside the overlay's template */ +export interface DtStackedBarChartTooltipData { + /** Node passed by user in `series` array */ + origin: DtStackedBarChartNode; + /** Original parent series */ + seriesOrigin: DtStackedBarChartSeries; + + /** Numeric percentage value based on this node vs sum of top level */ + valueRelative: number; + /** Color for this node in this state */ + color: DtColors | string; + /** If node is visible */ + visible: boolean; + /** If node is currently selected */ + selected: boolean; + /** Current length in percentage given only the visible nodes */ + length?: string; +} + +/** For single track only, format of value to be displayed in legend */ +export type DtStackedBarChartValueDisplayMode = 'none' | 'absolute' | 'percent'; + +/** Whether track should be filled fully or should take into account the rest of tracks for max value */ +export type DtStackedBarChartFillMode = 'full' | 'relative'; + +/** Orientation of the chart */ +export type DtStackedBarChartMode = 'bar' | 'column'; + +/* + * + * NODE PARSING + * + */ + +/** + * @description Fill the series with extended information and preserv original items + * + * @param series whole set of series provided by user + * @param legends used to calculate visibility and color + * + * @returns Filled series + */ +export const fillSeries = ( + series: DtStackedBarChartSeries[], + legends: DtStackedBarChartLegend[], +): DtStackedBarChartFilledSeries[] => + series.map((s) => ({ + origin: s, + nodes: s.nodes.map((node) => ({ + origin: node, + seriesOrigin: s, + color: legends.find((legend) => legend.label === node.label)?.color ?? '', + valueRelative: node.value / getValue(s.nodes), + visible: true, + selected: false, + })), + })); + +/** + * @description Get series with visibility options based on selected id + * + * @param series whole set of filled series + * @param selectedSeries currently selected series if present + * @param selectedNode currently selected node if present + * @param max maximum amount to be used for scaling the slices + * + * @returns Set of series with visibility, selection and length + */ +export const getSeriesWithState = ( + series: DtStackedBarChartFilledSeries[] = [], + [selectedSeries, selectedNode]: + | [DtStackedBarChartSeries, DtStackedBarChartNode] + | [], + max?: number, +): DtStackedBarChartFilledSeries[] => + series.map((s) => ({ + ...s, + nodes: s.nodes.map((node) => ({ + ...node, + // in order to use transitions in the track we cannot hide the element but make it 0 + length: node.visible + ? `${ + (100 * node.origin.value) / + (max !== undefined + ? max + : getValueForFilled(s.nodes.filter((n) => n.visible))) + }%` + : '0', + selected: s.origin === selectedSeries && node.origin === selectedNode, + })), + })); + +/** + * @description Set visibility of the node based on legends + * + * @param series whole set of filled series + * @param legends legends used to calculate visibility + */ +export const updateNodesVisibility = ( + series: DtStackedBarChartFilledSeries[], + legends: DtStackedBarChartLegend[], +): DtStackedBarChartFilledSeries[] => { + series.forEach((s) => { + s.nodes.forEach((node) => { + const found = legends.find((l) => l.label === node.origin.label); + + node.color = found?.color || node.color; + node.visible = !!found?.visible; + }); + }); + + return series; +}; + +/** + * @description Get unified legends with one color for each label. Colors are theme based if not preset in the node + * + * @param series Whole set of filled series + * @param theme Theme used in the page or component + * + * @returns Set of legends with one color for each label + */ +export const getLegends = ( + series: DtStackedBarChartSeries[], + theme?: DtTheme, +): DtStackedBarChartLegend[] => { + const legends = series + // flatten + .reduce((nodes, s) => [...nodes, ...s.nodes], []) + // take default colors + .reduce( + (labels, node) => ({ + ...labels, + [node.label]: + labels[node.label] !== undefined ? labels[node.label] : node.color, + }), + {}, + ); + + const colors = getDtChartColorPalette(Object.keys(legends).length, theme); + + return Object.keys(legends).map((key, i) => ({ + label: key, + color: legends[key] ? legends[key] : colors[i], + visible: true, + })); +}; + +/* + * + * UTILS + * + */ + +/** + * @description Get sum of values + * + * @param nodes whole set of nodes + * + * @returns sum of nodes values + */ +const getValue = (nodes: DtStackedBarChartNode[]): number => + nodes.reduce((total, p) => total + (p?.value ?? 0), 0); + +/** + * @description Get sum of values for filled nodes + * + * @param nodes whole set of filled nodes + * + * @returns sum of nodes values + */ +const getValueForFilled = (nodes: DtStackedBarChartTooltipData[]): number => + nodes.reduce((total, p) => total + (p?.origin.value ?? 0), 0); + +/** + * @description Get max of all bars sum of values + * + * @param series whole set of filled series + * + * @returns sum of series values + */ +export const getTotalMaxValue = (series: DtStackedBarChartSeries[]): number => + Math.max(...series.map((s) => getValue(s.nodes))); diff --git a/libs/barista-components/stacked-bar-chart/src/test-setup.ts b/libs/barista-components/stacked-bar-chart/src/test-setup.ts new file mode 100644 index 0000000000..3c66e43d72 --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/src/test-setup.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'jest-preset-angular'; diff --git a/libs/barista-components/stacked-bar-chart/tsconfig.json b/libs/barista-components/stacked-bar-chart/tsconfig.json new file mode 100644 index 0000000000..7484bd1248 --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "types": ["node", "jest"] + }, + "include": ["**/*.ts", "jest.config.js"] +} diff --git a/libs/barista-components/stacked-bar-chart/tsconfig.lib.json b/libs/barista-components/stacked-bar-chart/tsconfig.lib.json new file mode 100644 index 0000000000..1c600457d3 --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/tsconfig.lib.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "target": "es2015", + "declaration": true, + "inlineSources": true, + "types": [], + "lib": ["dom", "es2018"] + }, + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true, + "enableResourceInlining": true + }, + "exclude": ["src/test-setup.ts", "**/*.spec.ts"] +} diff --git a/libs/barista-components/stacked-bar-chart/tsconfig.lib.prod.json b/libs/barista-components/stacked-bar-chart/tsconfig.lib.prod.json new file mode 100644 index 0000000000..cbae794224 --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/tsconfig.lib.prod.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.lib.json", + "angularCompilerOptions": { + "enableIvy": false + } +} diff --git a/libs/barista-components/stacked-bar-chart/tsconfig.spec.json b/libs/barista-components/stacked-bar-chart/tsconfig.spec.json new file mode 100644 index 0000000000..fd405a65ef --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/barista-components/stacked-bar-chart/tslint.json b/libs/barista-components/stacked-bar-chart/tslint.json new file mode 100644 index 0000000000..95392b22f1 --- /dev/null +++ b/libs/barista-components/stacked-bar-chart/tslint.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tslint.json", + "rules": {}, + "linterOptions": { + "exclude": ["!**/*"] + } +} diff --git a/libs/examples/src/examples.module.ts b/libs/examples/src/examples.module.ts index 9a6f4c9900..c35503484a 100644 --- a/libs/examples/src/examples.module.ts +++ b/libs/examples/src/examples.module.ts @@ -69,6 +69,7 @@ import { DtExamplesSelectModule } from './select/select-examples.module'; import { DtExamplesShowMoreModule } from './show-more/show-more-examples.module'; import { DtExamplesSidenavModule } from './sidenav/sidenav-examples.module'; import { DtExamplesSliderModule } from './slider/slider-examples.module'; +import { DtExamplesStackedBarChartModule } from './stacked-bar-chart/stacked-bar-chart-examples.module'; import { DtExamplesStepperModule } from './stepper/stepper-examples.module'; import { DtSunburstChartExamplesModule } from './sunburst-chart/sunburst-chart-examples.module'; import { DtExamplesSwitchModule } from './switch/switch-examples.module'; @@ -135,6 +136,7 @@ import { DtExamplesTreeTableModule } from './tree-table/tree-table-examples.modu DtExamplesShowMoreModule, DtExamplesSidenavModule, DtExamplesSliderModule, + DtExamplesStackedBarChartModule, DtExamplesStepperModule, DtSunburstChartExamplesModule, DtExamplesSwitchModule, diff --git a/libs/examples/src/index.ts b/libs/examples/src/index.ts index b364c6935c..3fcc2cfecd 100644 --- a/libs/examples/src/index.ts +++ b/libs/examples/src/index.ts @@ -258,6 +258,11 @@ import { DtExampleSidenavDefault } from './sidenav/sidenav-default-example/siden import { DtExampleDisabledSlider } from './slider/slider-disabled-example/slider-disabled-example'; import { DtExampleFractionSlider } from './slider/slider-fraction-example/slider-fraction-example'; import { DtExampleSimpleSlider } from './slider/slider-simple-example/slider-simple-example'; +import { DtExampleStackedBarChartColumn } from './stacked-bar-chart/stacked-bar-chart-column-example/stacked-bar-chart-column-example'; +import { DtExampleStackedBarChartConnectedLegend } from './stacked-bar-chart/stacked-bar-chart-connected-legend-example/stacked-bar-chart-connected-legend-example'; +import { DtExampleStackedBarChartFilled } from './stacked-bar-chart/stacked-bar-chart-filled-example/stacked-bar-chart-filled-example'; +import { DtExampleStackedBarChartGeneric } from './stacked-bar-chart/stacked-bar-chart-generic-example/stacked-bar-chart-generic-example'; +import { DtExampleStackedBarChartSingle } from './stacked-bar-chart/stacked-bar-chart-single-example/stacked-bar-chart-single-example'; import { DtExampleStepperDefault } from './stepper/stepper-default-example/stepper-default-example'; import { DtExampleStepperEditable } from './stepper/stepper-editable-example/stepper-editable-example'; import { DtExampleStepperLinear } from './stepper/stepper-linear-example/stepper-linear-example'; @@ -367,6 +372,7 @@ export { DtExamplesSelectModule } from './select/select-examples.module'; export { DtExamplesShowMoreModule } from './show-more/show-more-examples.module'; export { DtExamplesSidenavModule } from './sidenav/sidenav-examples.module'; export { DtExamplesSliderModule } from './slider/slider-examples.module'; +export { DtExamplesStackedBarChartModule } from './stacked-bar-chart/stacked-bar-chart-examples.module'; export { DtExamplesStepperModule } from './stepper/stepper-examples.module'; export { DtSunburstChartExamplesModule } from './sunburst-chart/sunburst-chart-examples.module'; export { DtExamplesSwitchModule } from './switch/switch-examples.module'; @@ -614,6 +620,11 @@ export { DtExampleDisabledSlider, DtExampleFractionSlider, DtExampleSimpleSlider, + DtExampleStackedBarChartColumn, + DtExampleStackedBarChartConnectedLegend, + DtExampleStackedBarChartFilled, + DtExampleStackedBarChartGeneric, + DtExampleStackedBarChartSingle, DtExampleStepperDefault, DtExampleStepperEditable, DtExampleStepperLinear, @@ -788,6 +799,7 @@ export const EXAMPLES_MAP = new Map>([ ['DtExampleDrawerDynamic', DtExampleDrawerDynamic], ['DtExampleDrawerNested', DtExampleDrawerNested], ['DtExampleDrawerOver', DtExampleDrawerOver], + ['DtExampleDrawerTableDefault', DtExampleDrawerTableDefault], ['DtExampleCustomEmptyStateTable', DtExampleCustomEmptyStateTable], ['DtExampleCustomEmptyState', DtExampleCustomEmptyState], ['DtExampleEmptyStateDefault', DtExampleEmptyStateDefault], @@ -957,6 +969,14 @@ export const EXAMPLES_MAP = new Map>([ ['DtExampleDisabledSlider', DtExampleDisabledSlider], ['DtExampleFractionSlider', DtExampleFractionSlider], ['DtExampleSimpleSlider', DtExampleSimpleSlider], + ['DtExampleStackedBarChartColumn', DtExampleStackedBarChartColumn], + [ + 'DtExampleStackedBarChartConnectedLegend', + DtExampleStackedBarChartConnectedLegend, + ], + ['DtExampleStackedBarChartFilled', DtExampleStackedBarChartFilled], + ['DtExampleStackedBarChartGeneric', DtExampleStackedBarChartGeneric], + ['DtExampleStackedBarChartSingle', DtExampleStackedBarChartSingle], ['DtExampleStepperDefault', DtExampleStepperDefault], ['DtExampleStepperEditable', DtExampleStepperEditable], ['DtExampleStepperLinear', DtExampleStepperLinear], diff --git a/libs/examples/src/stacked-bar-chart/index.ts b/libs/examples/src/stacked-bar-chart/index.ts new file mode 100644 index 0000000000..7fe4ceed8c --- /dev/null +++ b/libs/examples/src/stacked-bar-chart/index.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './stacked-bar-chart-single-example/stacked-bar-chart-single-example'; +export * from './stacked-bar-chart-connected-legend-example/stacked-bar-chart-connected-legend-example'; +export * from './stacked-bar-chart-generic-example/stacked-bar-chart-generic-example'; +export * from './stacked-bar-chart-filled-example/stacked-bar-chart-filled-example'; +export * from './stacked-bar-chart-examples.module'; diff --git a/libs/examples/src/stacked-bar-chart/stacked-bar-chart-column-example/stacked-bar-chart-column-example.html b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-column-example/stacked-bar-chart-column-example.html new file mode 100644 index 0000000000..9658f603d4 --- /dev/null +++ b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-column-example/stacked-bar-chart-column-example.html @@ -0,0 +1,12 @@ + + + {{ tooltip.seriesOrigin.label }} +
+ {{ tooltip.origin.label }}: {{ tooltip.origin.value }} +
+
diff --git a/libs/examples/src/stacked-bar-chart/stacked-bar-chart-column-example/stacked-bar-chart-column-example.ts b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-column-example/stacked-bar-chart-column-example.ts new file mode 100644 index 0000000000..d5d2ac95c2 --- /dev/null +++ b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-column-example/stacked-bar-chart-column-example.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component } from '@angular/core'; +import { stackedBarChartDemoDataCoffee } from '../stacked-bar-chart-demo-data'; + +@Component({ + selector: 'dt-example-stacked-bar-chart-column-barista', + templateUrl: './stacked-bar-chart-column-example.html', +}) +export class DtExampleStackedBarChartColumn { + series = stackedBarChartDemoDataCoffee; +} diff --git a/libs/examples/src/stacked-bar-chart/stacked-bar-chart-connected-legend-example/stacked-bar-chart-connected-legend-example.html b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-connected-legend-example/stacked-bar-chart-connected-legend-example.html new file mode 100644 index 0000000000..943d70b141 --- /dev/null +++ b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-connected-legend-example/stacked-bar-chart-connected-legend-example.html @@ -0,0 +1,42 @@ + + + + + {{ node.label }} + + + + + + + Episodes + + + + + {{ tooltip.origin.label }}: + {{ tooltip.origin.value }} episodes + + + + + + + + + diff --git a/libs/examples/src/stacked-bar-chart/stacked-bar-chart-connected-legend-example/stacked-bar-chart-connected-legend-example.scss b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-connected-legend-example/stacked-bar-chart-connected-legend-example.scss new file mode 100644 index 0000000000..217d093ce8 --- /dev/null +++ b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-connected-legend-example/stacked-bar-chart-connected-legend-example.scss @@ -0,0 +1,17 @@ +@import '../../../../../libs/barista-components/core/src/style/variables'; + +dt-stacked-bar-chart { + width: 100%; +} + +.dt-stacked-bar-chart-demo-legend-symbol { + background: var(--node-color); + width: 12px; + height: 12px; +} + +.dt-stacked-bar-chart-demo-legend-hidden { + .dt-stacked-bar-chart-demo-legend-symbol { + background-color: $gray-300; + } +} diff --git a/libs/examples/src/stacked-bar-chart/stacked-bar-chart-connected-legend-example/stacked-bar-chart-connected-legend-example.ts b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-connected-legend-example/stacked-bar-chart-connected-legend-example.ts new file mode 100644 index 0000000000..fd2750bc21 --- /dev/null +++ b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-connected-legend-example/stacked-bar-chart-connected-legend-example.ts @@ -0,0 +1,40 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component } from '@angular/core'; +import { stackedBarChartDemoDataShows } from '../stacked-bar-chart-demo-data'; +import { DtTableDataSource } from '@dynatrace/barista-components/table'; +import { DtStackedBarChartLegend } from '@dynatrace/barista-components/stacked-bar-chart'; + +@Component({ + selector: 'dt-example-stacked-bar-chart-connected-legend-barista', + templateUrl: './stacked-bar-chart-connected-legend-example.html', + styleUrls: ['./stacked-bar-chart-connected-legend-example.scss'], +}) +export class DtExampleStackedBarChartConnectedLegend { + shows = stackedBarChartDemoDataShows; + dataSource = new DtTableDataSource(stackedBarChartDemoDataShows); + legends = this.shows[0].nodes.map((node) => ({ + label: node.label, + color: node.color, + visible: true, + })); + + _toggleNode(node: DtStackedBarChartLegend): void { + node.visible = !node.visible; + this.legends = this.legends.slice(); + } +} diff --git a/libs/examples/src/stacked-bar-chart/stacked-bar-chart-demo-data.ts b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-demo-data.ts new file mode 100644 index 0000000000..d737cd9314 --- /dev/null +++ b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-demo-data.ts @@ -0,0 +1,157 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DtColors } from '@dynatrace/barista-components/theming'; +import { DtStackedBarChartSeries } from '@dynatrace/barista-components/stacked-bar-chart'; + +export const stackedBarChartDemoDataCoffee = [ + { + label: 'Espresso', + nodes: [ + { + value: 1, + label: 'Coffee', + }, + ], + }, + { + label: 'Macchiato', + nodes: [ + { + value: 2, + label: 'Coffee', + }, + { + value: 1, + label: 'Milk', + }, + ], + }, + { + label: 'Americano', + nodes: [ + { + value: 2, + label: 'Coffee', + }, + { + value: 3, + label: 'Water', + }, + ], + }, + { + label: 'Mocha', + nodes: [ + { + value: 2, + label: 'Coffee', + }, + { + value: 2, + label: 'Chocolate', + }, + { + value: 1, + label: 'Milk', + }, + ], + }, +]; + +export const stackedBarChartDemoDataShows: DtStackedBarChartSeries[] = [ + { + label: 'Lost', + nodes: [ + { + value: 25, + label: 'Season 1', + color: DtColors.RED_500, + }, + { + value: 24, + label: 'Season 2', + color: DtColors.ORANGE_400, + }, + { + value: 23, + label: 'Season 3', + color: DtColors.YELLOW_500, + }, + { + value: 14, + label: 'Season 4', + color: DtColors.GREEN_500, + }, + { + value: 17, + label: 'Season 5', + color: DtColors.BLUE_500, + }, + { + value: 18, + label: 'Season 6', + color: DtColors.PURPLE_500, + }, + ], + }, + { + label: 'Six feet under', + nodes: [ + { + value: 13, + label: 'Season 1', + }, + { + value: 13, + label: 'Season 2', + }, + { + value: 13, + label: 'Season 3', + }, + { + value: 12, + label: 'Season 4', + }, + { + value: 12, + label: 'Season 5', + }, + ], + }, + { + label: 'Halt and catch fire', + nodes: [ + { + value: 10, + label: 'Season 1', + }, + { + value: 10, + label: 'Season 2', + }, + { + value: 10, + label: 'Season 3', + }, + { + value: 10, + label: 'Season 4', + }, + ], + }, +]; diff --git a/libs/examples/src/stacked-bar-chart/stacked-bar-chart-examples.module.ts b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-examples.module.ts new file mode 100644 index 0000000000..93e8900324 --- /dev/null +++ b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-examples.module.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NgModule } from '@angular/core'; +import { DtFormattersModule } from '@dynatrace/barista-components/formatters'; +import { DtLegendModule } from '@dynatrace/barista-components/legend'; +import { DtStackedBarChartModule } from '@dynatrace/barista-components/stacked-bar-chart'; +import { DtExampleStackedBarChartSingle } from './stacked-bar-chart-single-example/stacked-bar-chart-single-example'; +import { DtExampleStackedBarChartConnectedLegend } from './stacked-bar-chart-connected-legend-example/stacked-bar-chart-connected-legend-example'; +import { DtExampleStackedBarChartGeneric } from './stacked-bar-chart-generic-example/stacked-bar-chart-generic-example'; +import { DtExampleStackedBarChartFilled } from './stacked-bar-chart-filled-example/stacked-bar-chart-filled-example'; +import { DtExampleStackedBarChartColumn } from './stacked-bar-chart-column-example/stacked-bar-chart-column-example'; +import { CommonModule } from '@angular/common'; +import { DtTableModule } from '@dynatrace/barista-components/table'; +import { DtButtonGroupModule } from '@dynatrace/barista-components/button-group'; + +export const DT_SINGLE_STACKED_BAR_CHART_EXAMPLES = [ + DtExampleStackedBarChartSingle, + DtExampleStackedBarChartConnectedLegend, + DtExampleStackedBarChartGeneric, + DtExampleStackedBarChartFilled, + DtExampleStackedBarChartColumn, +]; + +@NgModule({ + imports: [ + CommonModule, + DtStackedBarChartModule, + DtFormattersModule, + DtButtonGroupModule, + DtLegendModule, + DtTableModule, + ], + declarations: [...DT_SINGLE_STACKED_BAR_CHART_EXAMPLES], + entryComponents: [...DT_SINGLE_STACKED_BAR_CHART_EXAMPLES], +}) +export class DtExamplesStackedBarChartModule {} diff --git a/libs/examples/src/stacked-bar-chart/stacked-bar-chart-filled-example/stacked-bar-chart-filled-example.html b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-filled-example/stacked-bar-chart-filled-example.html new file mode 100644 index 0000000000..ff9279489d --- /dev/null +++ b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-filled-example/stacked-bar-chart-filled-example.html @@ -0,0 +1,11 @@ + + + {{ tooltip.origin.label }}: {{ tooltip.valueRelative * 100 | dtPercent }} + + + +
Fill mode
+ + Full + Relative + diff --git a/libs/examples/src/stacked-bar-chart/stacked-bar-chart-filled-example/stacked-bar-chart-filled-example.ts b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-filled-example/stacked-bar-chart-filled-example.ts new file mode 100644 index 0000000000..4323f614b0 --- /dev/null +++ b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-filled-example/stacked-bar-chart-filled-example.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component } from '@angular/core'; +import { stackedBarChartDemoDataCoffee } from '../stacked-bar-chart-demo-data'; + +@Component({ + selector: 'dt-example-stacked-bar-chart-filled-barista', + templateUrl: './stacked-bar-chart-filled-example.html', +}) +export class DtExampleStackedBarChartFilled { + series = stackedBarChartDemoDataCoffee; + fillMode = 'full'; +} diff --git a/libs/examples/src/stacked-bar-chart/stacked-bar-chart-generic-example/stacked-bar-chart-generic-example.html b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-generic-example/stacked-bar-chart-generic-example.html new file mode 100644 index 0000000000..987c57beae --- /dev/null +++ b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-generic-example/stacked-bar-chart-generic-example.html @@ -0,0 +1,7 @@ + + + {{ tooltip.seriesOrigin.label }} +
+ {{ tooltip.origin.label }}: {{ tooltip.origin.value }} +
+
diff --git a/libs/examples/src/stacked-bar-chart/stacked-bar-chart-generic-example/stacked-bar-chart-generic-example.ts b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-generic-example/stacked-bar-chart-generic-example.ts new file mode 100644 index 0000000000..3e3792fa75 --- /dev/null +++ b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-generic-example/stacked-bar-chart-generic-example.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component } from '@angular/core'; +import { stackedBarChartDemoDataCoffee } from '../stacked-bar-chart-demo-data'; + +@Component({ + selector: 'dt-example-stacked-bar-chart-generic-barista', + templateUrl: './stacked-bar-chart-generic-example.html', +}) +export class DtExampleStackedBarChartGeneric { + series = stackedBarChartDemoDataCoffee; +} diff --git a/libs/examples/src/stacked-bar-chart/stacked-bar-chart-single-example/stacked-bar-chart-single-example.html b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-single-example/stacked-bar-chart-single-example.html new file mode 100644 index 0000000000..eefa62b685 --- /dev/null +++ b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-single-example/stacked-bar-chart-single-example.html @@ -0,0 +1,17 @@ + + + {{ tooltip.origin.label }}: {{ tooltip.value }} + + + +
Value display mode
+ + None + Absolute + Percent + diff --git a/libs/examples/src/stacked-bar-chart/stacked-bar-chart-single-example/stacked-bar-chart-single-example.ts b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-single-example/stacked-bar-chart-single-example.ts new file mode 100644 index 0000000000..3c7c8d2cf9 --- /dev/null +++ b/libs/examples/src/stacked-bar-chart/stacked-bar-chart-single-example/stacked-bar-chart-single-example.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component } from '@angular/core'; +import { stackedBarChartDemoDataCoffee } from '../stacked-bar-chart-demo-data'; + +@Component({ + selector: 'dt-example-stacked-bar-chart-single-barista', + templateUrl: './stacked-bar-chart-single-example.html', +}) +export class DtExampleStackedBarChartSingle { + series = [stackedBarChartDemoDataCoffee[2]]; + valueDisplayMode = 'percent'; +} diff --git a/nx.json b/nx.json index f8fffb3265..82fe746bd5 100644 --- a/nx.json +++ b/nx.json @@ -110,6 +110,7 @@ "select", "slider", "show-more", + "stacked-bar-chart", "stepper", "sunburst-chart", "switch", @@ -269,6 +270,9 @@ "show-more": { "tags": ["scope:components", "type:library"] }, + "stacked-bar-chart": { + "tags": ["scope:components", "type:library"] + }, "stepper": { "tags": ["scope:components", "type:library"] }, diff --git a/tsconfig.json b/tsconfig.json index 14b55e4c11..f3f743fdda 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -161,6 +161,9 @@ "@dynatrace/barista-components/show-more": [ "libs/barista-components/show-more/index.ts" ], + "@dynatrace/barista-components/stacked-bar-chart": [ + "libs/barista-components/stacked-bar-chart/index.ts" + ], "@dynatrace/barista-components/stepper": [ "libs/barista-components/stepper/index.ts" ],