From fe97f3c6e133736c60c293c2cd1d28f11e6cfadb Mon Sep 17 00:00:00 2001 From: "Subarroca, Salvador" Date: Fri, 24 Apr 2020 12:49:40 +0200 Subject: [PATCH] feat(stacked-series-chart): Create component --- .github/CODEOWNERS | 2 + angular.json | 40 ++ .../src/app/app.routing.module.ts | 7 + .../stacked-series-chart.e2e.ts | 254 +++++++ .../stacked-series-chart.html | 161 +++++ .../stacked-series-chart.module.ts | 35 + .../stacked-series-chart.po.ts | 90 +++ .../stacked-series-chart.ts | 131 ++++ 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-series-chart-demo-data.ts | 70 ++ .../stacked-series-chart-demo.component.html | 82 +++ .../stacked-series-chart-demo.component.scss | 17 + .../stacked-series-chart-demo.component.ts | 56 ++ 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-series-chart/README.md | 169 +++++ .../stacked-series-chart/barista.json | 34 + .../stacked-series-chart/index.ts | 27 + .../stacked-series-chart/jest.config.js | 27 + .../stacked-series-chart/package.json | 7 + .../src/_stacked-series-chart-shared.scss | 25 + .../src/stacked-series-chart-bar.scss | 61 ++ .../src/stacked-series-chart-column.scss | 93 +++ .../stacked-series-chart-overlay.directive.ts | 31 + .../src/stacked-series-chart.html | 95 +++ .../src/stacked-series-chart.layout.md | 228 ++++++ .../src/stacked-series-chart.mock.ts | 157 +++++ .../src/stacked-series-chart.module.ts | 34 + .../src/stacked-series-chart.scss | 118 ++++ .../src/stacked-series-chart.spec.ts | 650 ++++++++++++++++++ .../src/stacked-series-chart.ts | 438 ++++++++++++ .../src/stacked-series-chart.util.spec.ts | 577 ++++++++++++++++ .../src/stacked-series-chart.util.ts | 258 +++++++ .../stacked-series-chart/src/test-setup.ts | 17 + .../stacked-series-chart/tsconfig.json | 7 + .../stacked-series-chart/tsconfig.lib.json | 20 + .../tsconfig.lib.prod.json | 6 + .../stacked-series-chart/tsconfig.spec.json | 10 + .../stacked-series-chart/tslint.json | 7 + libs/examples/src/examples.module.ts | 2 + libs/examples/src/index.ts | 20 + .../src/stacked-series-chart/index.ts | 21 + .../stacked-series-chart-column-example.html | 12 + .../stacked-series-chart-column-example.ts | 26 + ...series-chart-connected-legend-example.html | 43 ++ ...series-chart-connected-legend-example.scss | 17 + ...d-series-chart-connected-legend-example.ts | 40 ++ .../stacked-series-chart-demo-data.ts | 157 +++++ .../stacked-series-chart-examples.module.ts | 50 ++ .../stacked-series-chart-filled-example.html | 15 + .../stacked-series-chart-filled-example.ts | 27 + .../stacked-series-chart-generic-example.html | 7 + .../stacked-series-chart-generic-example.ts | 26 + .../stacked-series-chart-single-example.html | 17 + .../stacked-series-chart-single-example.ts | 27 + nx.json | 4 + tsconfig.json | 3 + 61 files changed, 4630 insertions(+), 3 deletions(-) create mode 100644 apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.e2e.ts create mode 100644 apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.html create mode 100644 apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.module.ts create mode 100644 apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.po.ts create mode 100644 apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.ts create mode 100644 apps/dev/src/stacked-series-chart/stacked-series-chart-demo-data.ts create mode 100644 apps/dev/src/stacked-series-chart/stacked-series-chart-demo.component.html create mode 100644 apps/dev/src/stacked-series-chart/stacked-series-chart-demo.component.scss create mode 100644 apps/dev/src/stacked-series-chart/stacked-series-chart-demo.component.ts create mode 100644 libs/barista-components/stacked-series-chart/README.md create mode 100644 libs/barista-components/stacked-series-chart/barista.json create mode 100644 libs/barista-components/stacked-series-chart/index.ts create mode 100644 libs/barista-components/stacked-series-chart/jest.config.js create mode 100644 libs/barista-components/stacked-series-chart/package.json create mode 100644 libs/barista-components/stacked-series-chart/src/_stacked-series-chart-shared.scss create mode 100644 libs/barista-components/stacked-series-chart/src/stacked-series-chart-bar.scss create mode 100644 libs/barista-components/stacked-series-chart/src/stacked-series-chart-column.scss create mode 100644 libs/barista-components/stacked-series-chart/src/stacked-series-chart-overlay.directive.ts create mode 100644 libs/barista-components/stacked-series-chart/src/stacked-series-chart.html create mode 100644 libs/barista-components/stacked-series-chart/src/stacked-series-chart.layout.md create mode 100644 libs/barista-components/stacked-series-chart/src/stacked-series-chart.mock.ts create mode 100644 libs/barista-components/stacked-series-chart/src/stacked-series-chart.module.ts create mode 100644 libs/barista-components/stacked-series-chart/src/stacked-series-chart.scss create mode 100644 libs/barista-components/stacked-series-chart/src/stacked-series-chart.spec.ts create mode 100644 libs/barista-components/stacked-series-chart/src/stacked-series-chart.ts create mode 100644 libs/barista-components/stacked-series-chart/src/stacked-series-chart.util.spec.ts create mode 100644 libs/barista-components/stacked-series-chart/src/stacked-series-chart.util.ts create mode 100644 libs/barista-components/stacked-series-chart/src/test-setup.ts create mode 100644 libs/barista-components/stacked-series-chart/tsconfig.json create mode 100644 libs/barista-components/stacked-series-chart/tsconfig.lib.json create mode 100644 libs/barista-components/stacked-series-chart/tsconfig.lib.prod.json create mode 100644 libs/barista-components/stacked-series-chart/tsconfig.spec.json create mode 100644 libs/barista-components/stacked-series-chart/tslint.json create mode 100644 libs/examples/src/stacked-series-chart/index.ts create mode 100644 libs/examples/src/stacked-series-chart/stacked-series-chart-column-example/stacked-series-chart-column-example.html create mode 100644 libs/examples/src/stacked-series-chart/stacked-series-chart-column-example/stacked-series-chart-column-example.ts create mode 100644 libs/examples/src/stacked-series-chart/stacked-series-chart-connected-legend-example/stacked-series-chart-connected-legend-example.html create mode 100644 libs/examples/src/stacked-series-chart/stacked-series-chart-connected-legend-example/stacked-series-chart-connected-legend-example.scss create mode 100644 libs/examples/src/stacked-series-chart/stacked-series-chart-connected-legend-example/stacked-series-chart-connected-legend-example.ts create mode 100644 libs/examples/src/stacked-series-chart/stacked-series-chart-demo-data.ts create mode 100644 libs/examples/src/stacked-series-chart/stacked-series-chart-examples.module.ts create mode 100644 libs/examples/src/stacked-series-chart/stacked-series-chart-filled-example/stacked-series-chart-filled-example.html create mode 100644 libs/examples/src/stacked-series-chart/stacked-series-chart-filled-example/stacked-series-chart-filled-example.ts create mode 100644 libs/examples/src/stacked-series-chart/stacked-series-chart-generic-example/stacked-series-chart-generic-example.html create mode 100644 libs/examples/src/stacked-series-chart/stacked-series-chart-generic-example/stacked-series-chart-generic-example.ts create mode 100644 libs/examples/src/stacked-series-chart/stacked-series-chart-single-example/stacked-series-chart-single-example.html create mode 100644 libs/examples/src/stacked-series-chart/stacked-series-chart-single-example/stacked-series-chart-single-example.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ffe8d547ab..30ab6c02e9 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-series-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-series-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 9c472a6996..4df0d1e63f 100644 --- a/angular.json +++ b/angular.json @@ -3056,6 +3056,46 @@ }, "schematics": {} }, + "stacked-series-chart": { + "projectType": "library", + "root": "libs/barista-components/stacked-series-chart", + "sourceRoot": "libs/barista-components/stacked-series-chart/src", + "prefix": "dt", + "architect": { + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "libs/barista-components/stacked-series-chart/tsconfig.lib.json", + "libs/barista-components/stacked-series-chart/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**", + "!libs/barista-components/stacked-series-chart/**" + ] + } + }, + "lint-styles": { + "builder": "./dist/libs/workspace:stylelint", + "options": { + "stylelintConfig": ".stylelintrc", + "reportFile": "dist/stylelint/report.xml", + "exclude": ["**/node_modules/**"], + "files": ["libs/barista-components/stacked-series-chart/**/*.scss"] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "options": { + "jestConfig": "libs/barista-components/stacked-series-chart/jest.config.js", + "tsConfig": "libs/barista-components/stacked-series-chart/tsconfig.spec.json", + "setupFile": "libs/barista-components/stacked-series-chart/src/test-setup.ts", + "passWithNoTests": true + } + } + }, + "schematics": {} + }, "stepper": { "projectType": "library", "root": "libs/barista-components/stepper", diff --git a/apps/components-e2e/src/app/app.routing.module.ts b/apps/components-e2e/src/app/app.routing.module.ts index 321a2cd56b..dc4958a748 100644 --- a/apps/components-e2e/src/app/app.routing.module.ts +++ b/apps/components-e2e/src/app/app.routing.module.ts @@ -173,6 +173,13 @@ export const routes: Routes = [ (module) => module.DtE2EShowMoreModule, ), }, + { + path: 'stacked-series-chart', + loadChildren: () => + import( + '../components/stacked-series-chart/stacked-series-chart.module' + ).then((module) => module.DtE2EStackedSeriesChartModule), + }, { path: 'sunburst-chart', loadChildren: () => diff --git a/apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.e2e.ts b/apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.e2e.ts new file mode 100644 index 0000000000..a88eefaba3 --- /dev/null +++ b/apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.e2e.ts @@ -0,0 +1,254 @@ +/** + * @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 { resetWindowSizeToDefault, waitForAngular } from '../../utils'; +import { absoluteBtn, percentBtn } from '../sunburst-chart/sunburst-chart.po'; +import { + barBtn, + barChart, + body, + columnBtn, + columnChart, + fullTrackBtn, + getLabel, + getLegendItem, + getSlice, + getTick, + getTrack, + labels, + legend, + legendItems, + max10Btn, + noMaxBtn, + noneBtn, + nonSelectableBtn, + nonVisibleLabelBtn, + nonVisibleLegendBtn, + nonVisibleTrackBkgBtn, + nonVisibleValueAxisBtn, + overlay, + resetBtn, + selectableBtn, + selectBtn, + setLegendsBtn, + singleTrackBtn, + slices, + ticks, + tracks, + unselectBtn, + valueAxis, +} from './stacked-series-chart.po'; + +// Reduced speed of hovering should get our e2e tests stable. +// We should think about removing the dtOverlay and using the cdk one, +// that is not flaky on other e2e tests #86 +const hover: MouseActionOptions = { + // Slowest possible speed should help as workaround til the issue is fixed. + // The issue #646 is opened for this. + speed: 0.6, +}; + +const selectedSliceClassname = 'dt-stacked-series-chart-slice-selected'; + +fixture('Stacked series chart') + .page('http://localhost:4200/stacked-series-chart') + .beforeEach(async () => { + await resetWindowSizeToDefault(); + await waitForAngular(); + }); + +test('should have the defaults', async (testController) => { + await testController + .click(resetBtn) + .expect(barChart) + .ok() + .expect(tracks.count) + .eql(4) + .expect(labels.count) + .eql(4) + .expect(slices.count) + .eql(8) + .expect(legend) + .ok() + .expect(valueAxis) + .ok() + .expect(ticks.count) + .eql(6) + .expect(getLabel(0).textContent) + .match(/Espresso/) + .expect(getSlice(0, 0).clientWidth) + .within(135, 150) + .expect(getSlice(0, 0).clientHeight) + .eql(16) + .expect(getSlice(0, 0).getStyleProperty('background-color')) + .eql('rgb(0, 158, 96)') + .expect(legendItems.count) + .eql(4); +}); + +test('should change the mode and fillMode', async (testController) => { + await testController + .click(resetBtn) + // mode + .click(columnBtn) + .expect(columnChart) + .ok() + .expect(tracks.count) + .eql(4) + .expect(labels.count) + .eql(4) + .expect(slices.count) + .eql(8) + .expect(ticks.count) + .eql(6) + .expect(getLabel(0).textContent) + .match(/Espresso/) + .expect(getSlice(0, 0).clientWidth) + .eql(16) + .expect(getSlice(0, 0).clientHeight) + .within(60, 70) + .expect(legendItems.count) + .eql(4) + // fillMode + .click(fullTrackBtn) + .expect(getSlice(0, 0).clientHeight) + .within(315, 325) + .click(barBtn) + .expect(getSlice(0, 0).clientWidth) + .within(705, 720); +}); + +test('should change to single and multitrack with corresponding value display modes', async (testController) => { + await testController + .click(resetBtn) + // multi + .expect(getTick(0).textContent) + .match(/0/) + .expect(getLegendItem(0).textContent) + .match(/Coffee/) + + .click(percentBtn) + .expect(getTick(0).textContent) + .match(/0 %/) + .expect(getLegendItem(0).textContent) + .match(/Coffee/) + // single + .click(singleTrackBtn) + .click(noneBtn) + .expect(getTick(0).textContent) + .match(/0/) + .expect(getLegendItem(0).textContent) + .match(/Coffee/) + + .click(absoluteBtn) + .expect(getTick(0).textContent) + .match(/0/) + .expect(getLegendItem(0).textContent) + .match(/2 Coffee/) + + .click(percentBtn) + .expect(getTick(0).textContent) + .match(/0 %/) + .expect(getLegendItem(0).textContent) + .match(/66.7 % Coffee/); +}); + +test('should enable selection and select by input', async (testController) => { + await testController + // by click + .click(resetBtn) + .click(selectableBtn) + .click(getSlice(0, 0)) + .expect(getSlice(0, 0).classNames) + .contains(selectedSliceClassname) + .click(getSlice(0, 0)) + .expect(getSlice(0, 0).classNames) + .notContains(selectedSliceClassname) + + // by input + .click(resetBtn) + .click(selectableBtn) + .click(selectBtn) + .expect(getSlice(0, 0).classNames) + .contains(selectedSliceClassname) + .click(unselectBtn) + .expect(getSlice(0, 0).classNames) + .notContains(selectedSliceClassname) + + // non selectable + .click(resetBtn) + .click(nonSelectableBtn) + .click(getSlice(0, 0)) + .expect(getSlice(0, 0).classNames) + .notContains(selectedSliceClassname) + .click(selectBtn) + .expect(getSlice(0, 0).classNames) + .notContains(selectedSliceClassname); +}); + +test('should toggle legend and use an external one', async (testController) => { + await testController + .click(resetBtn) + .click(setLegendsBtn) + .expect(getSlice(0, 0).clientWidth) + .eql(0) + .click(nonVisibleLegendBtn) + .expect(legend.count) + .eql(0); +}); + +test('should toggle valueAxis and label', async (testController) => { + await testController + .click(resetBtn) + .click(nonVisibleValueAxisBtn) + .expect(valueAxis.count) + .eql(0) + .click(nonVisibleLabelBtn) + .expect(labels.count) + .eql(0); +}); + +test('should accept a max and toggle track background', async (testController) => { + await testController + .click(resetBtn) + // max + .click(max10Btn) + .expect(getSlice(0, 0).clientWidth) + .within(68, 75) + .click(noMaxBtn) + .expect(getSlice(0, 0).clientWidth) + .within(135, 150) + + // track background + .expect(getTrack(0).getStyleProperty('background-color')) + .eql('rgb(230, 230, 230)') + .click(nonVisibleTrackBkgBtn) + .expect(getTrack(0).getStyleProperty('background-color')) + .eql('rgba(0, 0, 0, 0)'); +}); + +test('should show overlay on hover', async (testController: TestController) => { + await testController + .click(resetBtn) + .hover(getSlice(0, 0), hover) + .expect(overlay.exists) + .ok() + .expect(overlay.textContent) + .match(/EspressoCoffee: 1/) + .hover(body, { ...hover, offsetX: 10, offsetY: 10 }) + .expect(overlay.exists) + .notOk(); +}); diff --git a/apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.html b/apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.html new file mode 100644 index 0000000000..d68927cdc4 --- /dev/null +++ b/apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.html @@ -0,0 +1,161 @@ + + + {{ tooltip.seriesOrigin.label }} +
{{ tooltip.origin.label }}: {{ tooltip.origin.value }}
+
+
+ +
+ +
+ +
+ + +
+ +
+ + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
diff --git a/apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.module.ts b/apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.module.ts new file mode 100644 index 0000000000..e43670ceb6 --- /dev/null +++ b/apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.module.ts @@ -0,0 +1,35 @@ +/** + * @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 { Route, RouterModule } from '@angular/router'; +import { DtStackedSeriesChartModule } from '@dynatrace/barista-components/stacked-series-chart'; +import { DtE2EStackedSeriesChart } from './stacked-series-chart'; + +const routes: Route[] = [{ path: '', component: DtE2EStackedSeriesChart }]; + +@NgModule({ + declarations: [DtE2EStackedSeriesChart], + imports: [ + CommonModule, + RouterModule.forChild(routes), + DtStackedSeriesChartModule, + ], + exports: [], + providers: [], +}) +export class DtE2EStackedSeriesChartModule {} diff --git a/apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.po.ts b/apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.po.ts new file mode 100644 index 0000000000..0036c0f187 --- /dev/null +++ b/apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.po.ts @@ -0,0 +1,90 @@ +/** + * @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 { Selector } from 'testcafe'; + +export const body = Selector('body'); +export const chart = Selector('test-stacked-series-chart'); +export const overlay = Selector('.dt-overlay-container'); +export const columnChart = Selector('.dt-stacked-series-chart-column'); +export const barChart = Selector('.dt-stacked-series-chart-bar'); + +export const tracks = Selector('.dt-stacked-series-chart-track'); +export const slices = Selector('.dt-stacked-series-chart-slice'); +export const labels = Selector('.dt-stacked-series-chart-track-label'); +export const valueAxis = Selector('.dt-stacked-series-chart-value-axis'); +export const ticks = Selector('.dt-stacked-series-chart-axis-tick'); +export const legend = Selector('.dt-stacked-series-chart-legend'); +export const legendItems = Selector('dt-legend-item'); + +export const getTrack = (track: number) => + Selector(`.dt-stacked-series-chart-track `).nth(track); +export const getSlice = (track: number, slice: number) => + Selector('.dt-stacked-series-chart-track') + .nth(track) + .child('.dt-stacked-series-chart-slice') + .nth(slice); +export const getLabel = (label: number) => + Selector(`.dt-stacked-series-chart-track-label`).nth(label); +export const getLegendItem = (item: number) => + Selector(`dt-legend-item`).nth(item); +export const getTick = (tick: number) => + Selector(`.dt-stacked-series-chart-axis-tick`).nth(tick); + +// controls +export const resetBtn = Selector('#chart-reset'); + +export const barBtn = Selector('#chart-mode-bar'); +export const columnBtn = Selector('#chart-mode-column'); + +export const noneBtn = Selector('#chart-value-mode-none'); +export const absoluteBtn = Selector('#chart-value-mode-absolute'); +export const percentBtn = Selector('#chart-value-mode-percent'); + +export const fullTrackBtn = Selector('#chart-fill-mode-full'); +export const relativeTrackBtn = Selector('#chart-fill-mode-relative'); + +export const multiTrackBtn = Selector('#chart-track-multi'); +export const singleTrackBtn = Selector('#chart-track-single'); + +export const visibleValueAxisBtn = Selector('#chart-visible-value-axis'); +export const nonVisibleValueAxisBtn = Selector('#chart-non-visible-value-axis'); + +export const visibleLegendBtn = Selector('#chart-visible-legend'); +export const nonVisibleLegendBtn = Selector('#chart-non-visible-legend'); + +export const visibleLabelBtn = Selector('#chart-visible-label'); +export const nonVisibleLabelBtn = Selector('#chart-non-visible-label'); + +export const visibleTrackBkgBtn = Selector('#chart-visible-track-background'); +export const nonVisibleTrackBkgBtn = Selector( + '#chart-non-visible-track-background', +); + +export const smallTrackBtn = Selector('#chart-track-size-8'); +export const bigTrackBtn = Selector('#chart-track-size-32'); + +export const noMaxBtn = Selector('#chart-no-max'); +export const max10Btn = Selector('#chart-max-10'); + +export const setLegendsBtn = Selector('#chart-set-legends'); +export const unsetLegendsBtn = Selector('#chart-unset-legends'); + +export const selectableBtn = Selector('#chart-selectable'); +export const nonSelectableBtn = Selector('#chart-non-selectable'); + +export const selectBtn = Selector('#chart-select'); +export const unselectBtn = Selector('#chart-unselect'); diff --git a/apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.ts b/apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.ts new file mode 100644 index 0000000000..47ee29933a --- /dev/null +++ b/apps/components-e2e/src/components/stacked-series-chart/stacked-series-chart.ts @@ -0,0 +1,131 @@ +/** + * @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 { + DtStackedSeriesChartNode, + DtStackedSeriesChartSeries, + DtStackedSeriesChartFillMode, + DtStackedSeriesChartValueDisplayMode, + DtStackedSeriesChartLegend, + DtStackedSeriesChartMode, +} from '@dynatrace/barista-components/stacked-series-chart'; +import { DtColors } from '@dynatrace/barista-components/theming'; + +@Component({ + selector: 'dt-e2e-stacked-series-chart', + templateUrl: 'stacked-series-chart.html', +}) +export class DtE2EStackedSeriesChart { + selected: [DtStackedSeriesChartSeries, DtStackedSeriesChartNode] | []; + selectable: boolean; + valueDisplayMode: DtStackedSeriesChartValueDisplayMode; + mode: DtStackedSeriesChartMode; + fillMode: DtStackedSeriesChartFillMode; + visibleValueAxis: boolean = true; + visibleLegend: boolean = true; + visibleLabel: boolean = true; + visibleTrackBackground: boolean = true; + maxTrackSize: number = 16; + max: number | undefined; + usedLegends: DtStackedSeriesChartLegend[] | undefined; + + series: DtStackedSeriesChartSeries[] = [ + { + label: 'Espresso', + nodes: [ + { + value: 1, + color: DtColors.SHAMROCKGREEN_700, + 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', + }, + ], + }, + ]; + + legends: DtStackedSeriesChartLegend[] = [ + { label: 'Coffee', color: '#7c38a1', visible: false }, + { label: 'Milk', color: '#fff29a', visible: true }, + { label: 'Water', color: '#4fd5e0', visible: true }, + { label: 'Chocolate', color: '#debbf3', visible: true }, + ]; + + usedSeries: DtStackedSeriesChartSeries[] = this.series; + + constructor() { + this.reset(); + } + + reset() { + this.selected = []; + this.selectable; + this.valueDisplayMode; + this.mode = 'bar'; + this.fillMode = 'relative'; + this.visibleValueAxis = true; + this.visibleLegend = true; + this.visibleLabel = true; + this.visibleTrackBackground = true; + this.maxTrackSize = 16; + this.max = undefined; + this.usedLegends = undefined; + // force recalculation of legends + this.usedSeries = this.series.slice(); + } +} diff --git a/apps/dev/src/app.module.ts b/apps/dev/src/app.module.ts index f75fde9a87..8e596218e3 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 { StackedSeriesChartDemo } from './stacked-series-chart/stacked-series-chart-demo.component'; import { SidenavDemo } from './sidenav/sidenav-demo.component'; import { StepperDemo } from './stepper/stepper-demo.component'; import { SliderDemo } from './slider/slider-demo.component'; @@ -148,6 +149,7 @@ export class NoopRouteComponent {} SecondaryNavDemo, SelectDemo, ShowMoreDemo, + StackedSeriesChartDemo, SunburstChartDemo, SwitchDemo, TableDemo, diff --git a/apps/dev/src/devapp-routing.module.ts b/apps/dev/src/devapp-routing.module.ts index 6c91d8cc8a..6839231d61 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 { StackedSeriesChartDemo } from './stacked-series-chart/stacked-series-chart-demo.component'; import { SidenavDemo } from './sidenav/sidenav-demo.component'; import { StepperDemo } from './stepper/stepper-demo.component'; import { SliderDemo } from './slider/slider-demo.component'; @@ -123,6 +124,7 @@ const routes: Routes = [ { path: 'secondary-nav', component: SecondaryNavDemo }, { path: 'select', component: SelectDemo }, { path: 'show-more', component: ShowMoreDemo }, + { path: 'stacked-series-chart', component: StackedSeriesChartDemo }, { 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 060b8f70b0..ba4dc2d5dd 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-series-chart', route: '/stacked-series-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 75c7a3715c..8bf87add70 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 { DtStackedSeriesChartModule } from '@dynatrace/barista-components/stacked-series-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, + DtStackedSeriesChartModule, DtSunburstChartModule, DtSwitchModule, DtTableModule, diff --git a/apps/dev/src/stacked-series-chart/stacked-series-chart-demo-data.ts b/apps/dev/src/stacked-series-chart/stacked-series-chart-demo-data.ts new file mode 100644 index 0000000000..013468ef3b --- /dev/null +++ b/apps/dev/src/stacked-series-chart/stacked-series-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 stackedSeriesChartDemoData = [ + { + 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-series-chart/stacked-series-chart-demo.component.html b/apps/dev/src/stacked-series-chart/stacked-series-chart-demo.component.html new file mode 100644 index 0000000000..f146ba22f1 --- /dev/null +++ b/apps/dev/src/stacked-series-chart/stacked-series-chart-demo.component.html @@ -0,0 +1,82 @@ + + + {{ tooltip.seriesOrigin.label }} +
{{ tooltip.origin.label }}: {{ tooltip.origin.value }}
+
+
+ +
+
+

Mode

+

Orientation

+ + Bar + Column + + +

+ Value display mode +

+ + None + Absolute + Percent + + +

Fill mode

+ + Full + Relative + + +

Amount of series

+ Multiple +
+ +
+

Visibility

+

Value axis

+ Visible + +

Label

+ Visible + +

Track

+

Background

+ Visible + +

Max size

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

Legend

+ Visible +
+ +
+

Selection

+ Selectable + +
{{ selected | json }}
+
+
diff --git a/apps/dev/src/stacked-series-chart/stacked-series-chart-demo.component.scss b/apps/dev/src/stacked-series-chart/stacked-series-chart-demo.component.scss new file mode 100644 index 0000000000..50313dd913 --- /dev/null +++ b/apps/dev/src/stacked-series-chart/stacked-series-chart-demo.component.scss @@ -0,0 +1,17 @@ +:host { + display: block; +} + +dt-stacked-series-chart { + margin-bottom: 40px; + + &.column { + min-height: 200px; + } +} + +.grid { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 16px; +} diff --git a/apps/dev/src/stacked-series-chart/stacked-series-chart-demo.component.ts b/apps/dev/src/stacked-series-chart/stacked-series-chart-demo.component.ts new file mode 100644 index 0000000000..5f8b0ba256 --- /dev/null +++ b/apps/dev/src/stacked-series-chart/stacked-series-chart-demo.component.ts @@ -0,0 +1,56 @@ +/** + * @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 { + DtStackedSeriesChartFillMode, + DtStackedSeriesChartMode, + DtStackedSeriesChartNode, + DtStackedSeriesChartSeries, + DtStackedSeriesChartValueDisplayMode, +} from '@dynatrace/barista-components/stacked-series-chart'; +import { stackedSeriesChartDemoData } from './stacked-series-chart-demo-data'; + +@Component({ + selector: 'stacked-series-chart-dev-app-demo', + templateUrl: './stacked-series-chart-demo.component.html', + styleUrls: ['./stacked-series-chart-demo.component.scss'], +}) +export class StackedSeriesChartDemo { + selectable: boolean = true; + selected: [DtStackedSeriesChartSeries, DtStackedSeriesChartNode] = [ + stackedSeriesChartDemoData[3], + stackedSeriesChartDemoData[3].nodes[1], + ]; + valueDisplayMode: DtStackedSeriesChartValueDisplayMode = 'absolute'; + visibleLabel: boolean = true; + visibleLegend: boolean = true; + fillMode: DtStackedSeriesChartFillMode = 'relative'; + multiSeries = true; + mode: DtStackedSeriesChartMode = 'bar'; + maxTrackSize: number = 16; + visibleTrackBackground: boolean = true; + visibleValueAxis: boolean = true; + + series = stackedSeriesChartDemoData; + + toggleSeries(multi: boolean): void { + this.multiSeries = multi; + this.series = multi + ? stackedSeriesChartDemoData + : [stackedSeriesChartDemoData[3]]; + } +} diff --git a/apps/universal/src/app/barista.module.ts b/apps/universal/src/app/barista.module.ts index a31ef04a6f..766638ada6 100644 --- a/apps/universal/src/app/barista.module.ts +++ b/apps/universal/src/app/barista.module.ts @@ -47,6 +47,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 { DtStackedSeriesChartModule } from '@dynatrace/barista-components/stacked-series-chart'; import { DtStepperModule } from '@dynatrace/barista-components/stepper'; import { DtSliderModule } from '@dynatrace/barista-components/slider'; import { DtSunburstChartModule } from '@dynatrace/barista-components/sunburst-chart'; @@ -97,6 +98,7 @@ import { DtTreeTableModule } from '@dynatrace/barista-components/tree-table'; DtSelectModule, DtShowMoreModule, DtSliderModule, + DtStackedSeriesChartModule, 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..46175fb654 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..bb63c5ef43 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 { ], }, ]; + + stackedSeriesChartSeries = [ + { + 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-series-chart/README.md b/libs/barista-components/stacked-series-chart/README.md new file mode 100644 index 0000000000..27e73f2d33 --- /dev/null +++ b/libs/barista-components/stacked-series-chart/README.md @@ -0,0 +1,169 @@ +# Stacked bar chart + + + +## Imports + +You have to import the `DtStackedSeriesChartModule` when you want to use the +``: + +```typescript +@NgModule({ + imports: [DtStackedSeriesChartModule], +}) +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 `dtStackedSeriesChart` in a minimal configuration, only `series` +attribute is required to create a valid output. For multiple series, slices +follow the same order given by the developer + +## Options & Properties + +### DtStackedSeriesChart + +#### Inputs + +| Name | Type | Default | Description | +| ------------------------ | -------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `mode` | `DtStackedSeriesChartMode` | `'bar'` | Display mode. | +| `series` | `DtStackedSeriesChartSeries[]` | - | Array of series with their nodes. | +| `selectable` | `boolean` | false | Allow selections to be made on chart | +| `selected` | `[DtStackedSeriesChartSeries, DtStackedSeriesChartNode]` | - | Current selection [series, node] | +| `max` | `number | undefined` | - | Max value in the chart. Useful when binding multiple stacked-series-chart. | +| `fillMode` | `DtStackedSeriesChartFillMode` | - | Whether each bar should be filled completely or should take into account their siblings and max. | +| `valueDisplayMode` | `DtStackedSeriesChartValueDisplayMode` | `'none'` | Sets the display mode for the stacked-series-chart values in legend to either 'none' 'percent' or 'absolute'. In single track chart value is displayed also in legend. For axis value 'none' falls back to 'absolute' | +| `legends` | `DtStackedSeriesChartLegend[]` | 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. | +| `visibleValueAxis` | `boolean` | true | Visibility of value axis. | +| `maxTrackSize` | `number` | 16 | Maximum size of the track. | + +#### Outputs + +| Name | Type | Description | +| ---------------- | ---------------------------------------- | --------------------------------------- | +| `selectedChange` | `EventEmitter` | Event that fires when a node is clicked | + +### DtStackedSeriesChartOverlay + +The `dtStackedSeriesChartOverlay` 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-series-chart. The implicit context +passed to the template follows the `DtStackedSeriesChartTooltipData` interface. + +```html + + + +``` + +## Models + +### DtStackedSeriesChartMode + +| Value | Description | +| -------- | ----------------- | +| `bar` | Horizontal tracks | +| `column` | Vertical tracks | + +### DtStackedSeriesChartFillMode + +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 | + +### DtStackedSeriesChartValueDisplayMode + +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 DtStackedSeriesChartNode | +| `percent` | Display the percentage of the node within that series | + +### DtStackedSeriesChartSeries + +This `DtStackedSeriesChartSeries` holds the information for one series. + +| Name | Type | Description | +| ------- | ---------------------------- | ------------------------------------- | +| `label` | `string` | Name of the series to be shown. | +| `nodes` | `DtStackedSeriesChartNode[]` | Array of node for the current series. | + +### DtStackedSeriesChartNode + +This `DtStackedSeriesChartNode` 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). | + +### DtStackedSeriesChartTooltipData + +The context of the overlay will be set to DtStackedSeriesChartTooltipData object +containing useful information that can be used inside the overlay's template + +| Name | Type | Description | +| --------------- | -------------------------- | ---------------------------------------------------------------- | +| `origin` | `DtStackedSeriesChartNode` | 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-series-chart. | +| `selected` | `boolean` | If node is currently selected. | +| `width` | `string` | Current width in percentage given only the visible nodes. | + +### DtStackedSeriesChartLegend + +This `DtStackedSeriesChartLegend` 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 + + + +### 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-series-chart/barista.json b/libs/barista-components/stacked-series-chart/barista.json new file mode 100644 index 0000000000..6c644b0664 --- /dev/null +++ b/libs/barista-components/stacked-series-chart/barista.json @@ -0,0 +1,34 @@ +{ + "title": "StackedSeriesChart", + "description": "The stacked-series-chart chart is used to display one level of related data in a horizontal stacked bar", + "postid": "stacked-series-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-series-chart/index.ts b/libs/barista-components/stacked-series-chart/index.ts new file mode 100644 index 0000000000..f3194f7d73 --- /dev/null +++ b/libs/barista-components/stacked-series-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-series-chart.module'; +export * from './src/stacked-series-chart'; +export { + DtStackedSeriesChartNode, + DtStackedSeriesChartSeries, + DtStackedSeriesChartTooltipData, + DtStackedSeriesChartValueDisplayMode, + DtStackedSeriesChartMode, + DtStackedSeriesChartFillMode, + DtStackedSeriesChartLegend, +} from './src/stacked-series-chart.util'; diff --git a/libs/barista-components/stacked-series-chart/jest.config.js b/libs/barista-components/stacked-series-chart/jest.config.js new file mode 100644 index 0000000000..5dd991f0ab --- /dev/null +++ b/libs/barista-components/stacked-series-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-series-chart', + preset: '../../../jest.config.js', + coverageDirectory: + '../../../coverage/libs/barista-components/stacked-series-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-series-chart/package.json b/libs/barista-components/stacked-series-chart/package.json new file mode 100644 index 0000000000..dedb72ce9c --- /dev/null +++ b/libs/barista-components/stacked-series-chart/package.json @@ -0,0 +1,7 @@ +{ + "ngPackage": { + "lib": { + "entryFile": "index.ts" + } + } +} diff --git a/libs/barista-components/stacked-series-chart/src/_stacked-series-chart-shared.scss b/libs/barista-components/stacked-series-chart/src/_stacked-series-chart-shared.scss new file mode 100644 index 0000000000..849284ea25 --- /dev/null +++ b/libs/barista-components/stacked-series-chart/src/_stacked-series-chart-shared.scss @@ -0,0 +1,25 @@ +@import '../../core/src/style/variables'; + +$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; +$tick-gutter: 16px; +$tick-length: 8px; +$hidden-transition-time: 0.2s; +$hover-opacity: 0.8; + +@mixin gridPosition($property, $label, $chart) { + .dt-stacked-series-chart-track-label { + #{$property}: $label; + } + .dt-stacked-series-chart-track { + #{$property}: $chart; + } +} diff --git a/libs/barista-components/stacked-series-chart/src/stacked-series-chart-bar.scss b/libs/barista-components/stacked-series-chart/src/stacked-series-chart-bar.scss new file mode 100644 index 0000000000..e2075cb8c6 --- /dev/null +++ b/libs/barista-components/stacked-series-chart/src/stacked-series-chart-bar.scss @@ -0,0 +1,61 @@ +@import './stacked-series-chart-shared'; + +/** HOW TO of layout + +See stacked-series-chart.layout.md +*/ + +:host(.dt-stacked-series-chart-bar) { + .dt-stacked-series-chart-container { + align-items: center; + grid-template-columns: auto 1fr; + @include gridPosition('grid-column', 1, 2); + } + + /* TRACK */ + .dt-stacked-series-chart-track { + min-height: 1px; + height: var(--dt-stacked-series-chart-max-bar-size); + grid-column: 2; + } + + /* SLICE */ + .dt-stacked-series-chart-slice { + width: var(--dt-stacked-series-chart-length); + } + + .dt-stacked-series-chart-slice-selected::before { + 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; + } + + /* AXIS */ + .dt-stacked-series-chart-series-axis { + display: none; + } + + .dt-stacked-series-chart-value-axis { + grid-column: 2; + border-top: 1px solid $axis-color; + grid-row: calc(1 + var(--dt-stacked-series-chart-track-amount)); + height: 40px; + } + + .dt-stacked-series-chart-axis-tick { + padding-top: $tick-gutter; + text-align: center; + width: 100px; + left: (calc(var(--dt-stacked-series-chart-tick-position) - 50px)); + + &::after { + top: -1px; + left: 50%; + height: $tick-length; + } + } +} diff --git a/libs/barista-components/stacked-series-chart/src/stacked-series-chart-column.scss b/libs/barista-components/stacked-series-chart/src/stacked-series-chart-column.scss new file mode 100644 index 0000000000..62cd36bd58 --- /dev/null +++ b/libs/barista-components/stacked-series-chart/src/stacked-series-chart-column.scss @@ -0,0 +1,93 @@ +@import './stacked-series-chart-shared'; + +/** HOW TO of layout + +See stacked-series-chart.layout.md +*/ + +:host(.dt-stacked-series-chart-column) { + .dt-stacked-series-chart-container { + justify-items: center; + grid-template-rows: 1fr auto; + @include gridPosition('grid-row', 2, 1); + grid-template-columns: auto repeat( + var(--dt-stacked-series-chart-track-amount), + 1fr + ); + } + + /* TRACK + LABEL */ + .dt-stacked-series-chart-track-label, + .dt-stacked-series-chart-track { + grid-column: calc(1 + var(--dt-stacked-series-chart-track-index)); + } + + .dt-stacked-series-chart-track-label { + align-self: center; + } + + .dt-stacked-series-chart-track { + grid-row: 1; + flex-direction: column-reverse; + max-width: var(--dt-stacked-series-chart-max-bar-size); + width: 100%; + } + + .dt-stacked-series-chart-label-none { + gap: 0; + grid-template-rows: 1fr; + } + + /* SLICE */ + .dt-stacked-series-chart-slice { + height: var(--dt-stacked-series-chart-length); + } + + /* AXIS */ + .dt-stacked-series-chart-series-axis { + grid-column: 1/-1; + border-bottom: 1px solid $axis-color; + width: 100%; + grid-row: 1; + } + + .dt-stacked-series-chart-value-axis { + grid-column: 1; + border-right: 1px solid $axis-color; + height: 100%; + grid-row: 1/2; + grid-auto-rows: 1fr; + flex-direction: column-reverse; + text-align: right; + } + + .dt-stacked-series-chart-axis-tick { + padding-right: $tick-gutter; + text-align: right; + height: 24px; + width: 64px; + + top: calc(var(--dt-stacked-series-chart-tick-position) - 12px); + right: -1px; + + &::after { + right: 0; + bottom: 50%; + width: $tick-length; + } + } + + .dt-stacked-series-chart-slice-selected::before { + box-shadow: $selected-size 0 $selected-color inset, + -$selected-size 0 $selected-color inset; + right: -$selected-size; + left: -$selected-size; + top: 0; + bottom: 0; + } +} + +:host(.dt-stacked-series-chart-with-value-axis.dt-stacked-series-chart-column) + .dt-stacked-series-chart-container { + padding-left: var(--dt-stacked-series-chart-value-axis-size); +} diff --git a/libs/barista-components/stacked-series-chart/src/stacked-series-chart-overlay.directive.ts b/libs/barista-components/stacked-series-chart/src/stacked-series-chart-overlay.directive.ts new file mode 100644 index 0000000000..1466f3096d --- /dev/null +++ b/libs/barista-components/stacked-series-chart/src/stacked-series-chart-overlay.directive.ts @@ -0,0 +1,31 @@ +/** + * @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 StackedSeriesChart. + * + * @example + * + * + * + */ +@Directive({ + selector: 'ng-template[dtStackedSeriesChartOverlay]', + exportAs: 'dtStackedSeriesChartOverlay', +}) +export class DtStackedSeriesChartOverlay {} diff --git a/libs/barista-components/stacked-series-chart/src/stacked-series-chart.html b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.html new file mode 100644 index 0000000000..1a5dfbaece --- /dev/null +++ b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.html @@ -0,0 +1,95 @@ +

+
+ + + + {{ track.origin.label }} + + +
+ + +
+
+ +
+ {{ + valueDisplayMode === 'percent' + ? (tick.valueRelative * 100 | dtPercent) + : (tick.value | dtCount) + }} +
+
+ + + + + + + {{ _tracks[0].nodes[i].origin.value | dtCount }} + + + {{ _tracks[0].nodes[i].valueRelative * 100 | dtPercent }} + + {{ legend.label }} + + diff --git a/libs/barista-components/stacked-series-chart/src/stacked-series-chart.layout.md b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.layout.md new file mode 100644 index 0000000000..be1dafa8af --- /dev/null +++ b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.layout.md @@ -0,0 +1,228 @@ +# 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 DtStackedSeriesChartLabelPosition = + | '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-series-chart-container { + display: grid; + grid-auto-flow: dense; + align-self: stretch; + gap: $gap; +} + +.dt-stacked-series-chart-bar { + align-items: center; + + .dt-stacked-series-chart-series-axis { + display: none; + } + + .dt-stacked-series-chart-track { + min-height: 1px; + height: var(--dt-stacked-series-chart-max-bar-size); + } + + .dt-stacked-series-chart-slice { + width: var(--dt-stacked-series-chart-length); + } +} + +.dt-stacked-series-chart-column { + justify-items: center; + .dt-stacked-series-chart-series-label { + align-self: center; + } + + .dt-stacked-series-chart-series-label, + .dt-stacked-series-chart-track { + grid-row: 1; + } + + .dt-stacked-series-chart-track { + flex-direction: column-reverse; + max-width: var(--dt-stacked-series-chart-max-bar-size); + width: 100%; + } + + .dt-stacked-series-chart-slice { + height: var(--dt-stacked-series-chart-length); + } + + .dt-stacked-series-chart-series-axis { + grid-column: 1/-1; + border-bottom: 1px solid $axis-color; + width: 100%; + grid-row: 1; + } +} + +/** Bar */ +.dt-stacked-series-chart-bar.dt-stacked-series-chart-label-none { + gap: $gap; + grid-template-columns: 1fr; +} + +.dt-stacked-series-chart-bar.dt-stacked-series-chart-label-top { + gap: 0; + grid-template-columns: 1fr; + @include gridPosition( + 'grid-row', + calc(2 * var(--dt-stacked-series-chart-track-index) - 1), + calc(2 * var(--dt-stacked-series-chart-track-index)) + ); + .dt-stacked-series-chart-track { + margin-bottom: $gap; + } +} + +.dt-stacked-series-chart-bar.dt-stacked-series-chart-label-right { + grid-template-columns: 1fr auto; + @include gridPosition('grid-column', 2, 1); +} + +.dt-stacked-series-chart-bar.dt-stacked-series-chart-label-bottom { + gap: 0; + grid-template-columns: 1fr; + @include gridPosition( + 'grid-row', + calc(2 * var(--dt-stacked-series-chart-track-index)), + calc(2 * var(--dt-stacked-series-chart-track-index) - 1) + ); + // all but last + .dt-stacked-series-chart-series-label:nth-last-of-type(n + 2) { + margin-bottom: $gap; + } +} + +.dt-stacked-series-chart-bar.dt-stacked-series-chart-label-left { + grid-template-columns: auto 1fr; + @include gridPosition('grid-column', 1, 2); +} + +/** Column */ +.dt-stacked-series-chart-column.dt-stacked-series-chart-label-none { + gap: 0; + grid-template-rows: 1fr; + grid-template-columns: repeat( + var(--dt-stacked-series-chart-track-amount), + 1fr + ); +} + +.dt-stacked-series-chart-column.dt-stacked-series-chart-label-top { + grid-template-rows: auto 1fr; + @include gridPosition('grid-row', 1, 2); + grid-template-columns: repeat( + var(--dt-stacked-series-chart-track-amount), + 1fr + ); + + .dt-stacked-series-chart-series-axis { + grid-row: 2; + } + + .dt-stacked-series-chart-track { + grid-column: var(--dt-stacked-series-chart-track-index); + } +} + +.dt-stacked-series-chart-column.dt-stacked-series-chart-label-right { + @include gridPosition( + 'grid-column', + calc(2 * var(--dt-stacked-series-chart-track-index)), + calc(2 * var(--dt-stacked-series-chart-track-index) - 1) + ); + grid-template-columns: repeat( + calc(2 * var(--dt-stacked-series-chart-track-amount)), + 1fr + ); + + .dt-stacked-series-chart-series-label { + justify-self: start; + } + .dt-stacked-series-chart-track { + justify-self: end; + } +} + +.dt-stacked-series-chart-column.dt-stacked-series-chart-label-bottom { + grid-template-rows: 1fr auto; + @include gridPosition('grid-row', 2, 1); + grid-template-columns: repeat( + var(--dt-stacked-series-chart-track-amount), + 1fr + ); + + .dt-stacked-series-chart-track { + grid-column: var(--dt-stacked-series-chart-track-index); + } +} + +.dt-stacked-series-chart-column.dt-stacked-series-chart-label-left { + @include gridPosition( + 'grid-column', + calc(2 * var(--dt-stacked-series-chart-track-index) - 1), + calc(2 * var(--dt-stacked-series-chart-track-index)) + ); + grid-template-columns: repeat( + calc(2 * var(--dt-stacked-series-chart-track-amount)), + 1fr + ); + + .dt-stacked-series-chart-series-label { + justify-self: end; + } + .dt-stacked-series-chart-track { + justify-self: start; + } +} +``` diff --git a/libs/barista-components/stacked-series-chart/src/stacked-series-chart.mock.ts b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.mock.ts new file mode 100644 index 0000000000..ebd48c3e62 --- /dev/null +++ b/libs/barista-components/stacked-series-chart/src/stacked-series-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 { DtStackedSeriesChartSeries } from './stacked-series-chart.util'; + +export const stackedSeriesChartDemoDataCoffee: DtStackedSeriesChartSeries[] = [ + { + 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 stackedSeriesChartDemoDataShows: DtStackedSeriesChartSeries[] = [ + { + 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-series-chart/src/stacked-series-chart.module.ts b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.module.ts new file mode 100644 index 0000000000..4f681af682 --- /dev/null +++ b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.module.ts @@ -0,0 +1,34 @@ +/** + * @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 { DtStackedSeriesChart } from './stacked-series-chart'; +import { DtStackedSeriesChartOverlay } from './stacked-series-chart-overlay.directive'; +import { + DtFormattersModule, + DtCount, +} from '@dynatrace/barista-components/formatters'; +import { DtLegendModule } from '@dynatrace/barista-components/legend'; +import { DtOverlayModule } from '@dynatrace/barista-components/overlay'; + +@NgModule({ + imports: [CommonModule, DtFormattersModule, DtLegendModule, DtOverlayModule], + exports: [DtStackedSeriesChart, DtStackedSeriesChartOverlay], + declarations: [DtStackedSeriesChart, DtStackedSeriesChartOverlay], + providers: [DtCount], +}) +export class DtStackedSeriesChartModule {} diff --git a/libs/barista-components/stacked-series-chart/src/stacked-series-chart.scss b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.scss new file mode 100644 index 0000000000..c5e8b875dd --- /dev/null +++ b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.scss @@ -0,0 +1,118 @@ +@import '../../core/src/style/overlay'; +@import './stacked-series-chart-shared'; +/** HOW TO of layout + +See stacked-series-chart.layout.md +*/ + +:host { + display: grid; + align-items: center; + grid-template-rows: 1fr auto; + + &.dt-stacked-series-chart-with-legend { + gap: $gap; + } +} + +/** Chart orientation */ +.dt-stacked-series-chart-container { + display: grid; + grid-auto-flow: dense; + align-self: stretch; + gap: $gap; +} + +/** Track */ +.dt-stacked-series-chart-track { + display: flex; +} +.dt-stacked-series-chart-track-background { + background: $track-color; +} + +.dt-stacked-series-chart-track-selectable .dt-stacked-series-chart-slice { + cursor: pointer; +} + +.dt-stacked-series-chart-slice { + background: var(--dt-stacked-series-chart-color); + box-sizing: content-box; + transition: width $hidden-transition-time, height $hidden-transition-time, + opacity $hover-transition-time; +} + +.dt-stacked-series-chart-track-hoverable { + .dt-stacked-series-chart-slice:hover { + opacity: $hover-opacity; + } +} + +/** Slice **/ +.dt-stacked-series-chart-slice-selected { + cursor: default; + position: relative; + + &::before { + content: ' '; + display: block; + position: absolute; + border: $selected-border-size solid $selected-border-color; + border-radius: 3px; + } +} + +/** Axis */ +.dt-stacked-series-chart-value-axis { + position: relative; +} + +.dt-stacked-series-chart-axis-tick { + position: absolute; + + &::after { + position: absolute; + display: block; + background: $axis-color; + width: 1px; + height: 1px; + content: ' '; + } +} + +/** Legend */ +.dt-stacked-series-chart-legend { + display: flex; + padding: 0; + justify-self: center; +} + +.dt-stacked-series-chart-legend-symbol { + display: block; + height: $bullet-height; + width: $bullet-height; + background: var(--dt-stacked-series-chart-color); + transition: background $hidden-transition-time; + + &:hover { + opacity: $hover-opacity; + } +} + +.dt-legend-item { + cursor: pointer; +} + +.dt-stacked-series-chart-legend-item-hidden { + .dt-stacked-series-chart-legend-symbol { + background: $hidden-color; + } +} + +/** Stacked series chart 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-series-chart-overlay-panel { + @include dt-overlay-container(); +} diff --git a/libs/barista-components/stacked-series-chart/src/stacked-series-chart.spec.ts b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.spec.ts new file mode 100644 index 0000000000..74ac933675 --- /dev/null +++ b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.spec.ts @@ -0,0 +1,650 @@ +/** + * @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, DebugElement } 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 { DtStackedSeriesChart } from './stacked-series-chart'; +import { stackedSeriesChartDemoDataCoffee } from './stacked-series-chart.mock'; +import { DtStackedSeriesChartModule } from './stacked-series-chart.module'; +import { + DtStackedSeriesChartFillMode, + DtStackedSeriesChartLegend, + DtStackedSeriesChartMode, + DtStackedSeriesChartNode, + DtStackedSeriesChartSeries, + DtStackedSeriesChartValueDisplayMode, +} from './stacked-series-chart.util'; + +describe('DtStackedSeriesChart', () => { + let fixture: ComponentFixture; + let rootComponent: TestApp; + let component: DtStackedSeriesChart; + let overlayContainer: OverlayContainer; + let overlayContainerElement: HTMLElement; + const overlayConfig: DtUiTestConfiguration = { + attributeName: 'dt-ui-test-id', + constructOverlayAttributeValue(attributeName: string): string { + return `${attributeName}-overlay`; + }, + }; + + let selectedChangeSpy; + + /** Gets all tracks within the rendered chart. */ + function getAllTracks(): DebugElement[] { + return fixture.debugElement.queryAll( + By.css('.dt-stacked-series-chart-track'), + ); + } + + function getAllTrackBackgrounds(): DebugElement[] { + return fixture.debugElement.queryAll( + By.css('.dt-stacked-series-chart-track-background'), + ); + } + + /** Get all slices */ + function getAllSlices(): DebugElement[] { + return fixture.debugElement.queryAll( + By.css('.dt-stacked-series-chart-slice'), + ); + } + + /** Gets a specific slice at a specific position */ + function getSliceByPositionWithinTrack( + trackIndex: number, + sliceIndex: number, + ): DebugElement { + const track = getAllTracks()[trackIndex]; + return track.queryAll(By.css('.dt-stacked-series-chart-slice'))[sliceIndex]; + } + + /** Gets the selected slice */ + function getSelectedSlice(): DebugElement { + return fixture.debugElement.query( + By.css('.dt-stacked-series-chart-slice-selected'), + ); + } + + /** Get all track labels */ + function getAllTrackLabels(): DebugElement[] { + return fixture.debugElement.queryAll( + By.css('.dt-stacked-series-chart-track-label'), + ); + } + + /** Get the legend for the current chart */ + function getLegend(): DebugElement { + return fixture.debugElement.query(By.css('dt-legend')); + } + + /** Gets the legend items */ + function getAllLegendItems(): DebugElement[] { + return fixture.debugElement.queryAll(By.css('dt-legend-item')); + } + + /** Get the hidden legend items */ + function getAllHiddenLegendItems(): DebugElement[] { + return fixture.debugElement.queryAll( + By.css('.dt-stacked-series-chart-legend-item-hidden'), + ); + } + + /** Gets the value axis element */ + function getValueAxis(): DebugElement { + return fixture.debugElement.query( + By.css('.dt-stacked-series-chart-value-axis'), + ); + } + + /** Get the series axis element */ + function getSeriesAxis(): DebugElement { + return fixture.debugElement.query( + By.css('.dt-stacked-series-chart-series-axis'), + ); + } + + /** Gets the ticks from the axis */ + function getAxisTicks(): DebugElement[] { + return fixture.debugElement.queryAll( + By.css('.dt-stacked-series-chart-value-axis'), + ); + } + + /** Get the overlay container. */ + function getOverlay(): Element | null { + return overlayContainerElement.querySelector( + '.dt-stacked-series-chart-overlay-panel', + ); + } + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + DtIconModule.forRoot({ svgIconLocation: `{{name}}.svg` }), + DtStackedSeriesChartModule, + 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(DtStackedSeriesChart)) + .componentInstance; + + selectedChangeSpy = jest.spyOn(component.selectedChange, 'emit'); + })); + + describe('should have defaults', () => { + let defComponent; + + beforeEach(() => { + const defFixture = createComponent(DefaultsTestApp); + defComponent = defFixture.debugElement.query( + By.directive(DtStackedSeriesChart), + ).componentInstance; + }); + + const propsAndDefaults = { + selectable: false, + selected: [], + valueDisplayMode: 'none', + _max: undefined, + fillMode: 'relative', + visibleLegend: true, + visibleTrackBackground: true, + visibleLabel: true, + mode: 'bar', + maxTrackSize: 16, + visibleValueAxis: true, + }; + + 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 = getAllTracks(); + expect(tracks.length).toEqual(stackedSeriesChartDemoDataCoffee.length); + const slice = getSliceByPositionWithinTrack(0, 0); + expect(slice.nativeElement.getAttribute('style')).toContain( + '--dt-stacked-series-chart-length: 20%', + ); + }); + + it('should update data after series input change', () => { + const oldTracks = getAllTracks(); + + const newSeries = stackedSeriesChartDemoDataCoffee.slice(1); + rootComponent.series = newSeries; + fixture.detectChanges(); + + const newTracks = getAllTracks(); + const newSlice = getSliceByPositionWithinTrack(0, 0); + + expect(newTracks.length).not.toEqual(oldTracks.length); + expect(newTracks.length).toEqual(newSeries.length); + + expect(newSlice.nativeElement.getAttribute('style')).toContain( + '--dt-stacked-series-chart-length: 40%', + ); + }); + }); + + describe('Selectable + Selected', () => { + it('should select nodes when input', () => { + rootComponent.selectable = true; + rootComponent.selected = [ + stackedSeriesChartDemoDataCoffee[1], + stackedSeriesChartDemoDataCoffee[1].nodes[1], + ]; + fixture.detectChanges(); + + const sliceByPosition = getSliceByPositionWithinTrack(1, 1); + const selected = getSelectedSlice(); + + expect(selected).toBe(sliceByPosition); + }); + + it('should make a selection', () => { + const sliceByPosition = getSliceByPositionWithinTrack(1, 1); + dispatchFakeEvent(sliceByPosition.nativeElement, 'click'); + fixture.detectChanges(); + + const selected = getSelectedSlice(); + + 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 = [ + stackedSeriesChartDemoDataCoffee[1], + stackedSeriesChartDemoDataCoffee[1].nodes[1], + ]; + fixture.detectChanges(); + + const selected = getSelectedSlice(); + expect(selected).toBe(null); + }); + + it('should not allow selection from template if disabled', () => { + rootComponent.selectable = false; + fixture.detectChanges(); + selectedChangeSpy.mockClear(); + + const sliceByPosition = getSliceByPositionWithinTrack(1, 1); + dispatchFakeEvent(sliceByPosition.nativeElement, 'click'); + + expect(selectedChangeSpy).not.toHaveBeenCalled(); + }); + }); + + describe('Value Display Mode', () => { + describe('Single tracked', () => { + beforeEach(() => { + rootComponent.visibleLegend = true; + + rootComponent.series = [stackedSeriesChartDemoDataCoffee[3]]; + fixture.detectChanges(); + }); + + it('should switch to ABSOLUTE display when set', () => { + rootComponent.valueDisplayMode = 'absolute'; + fixture.detectChanges(); + + const legendItem = getAllLegendItems()[1]; + expect(legendItem.nativeElement.textContent.trim()).toBe( + '2 Chocolate', + ); + }); + + it('should switch to PERCENT display when set', () => { + rootComponent.valueDisplayMode = 'percent'; + fixture.detectChanges(); + + const legendItem = getAllLegendItems()[1]; + expect(legendItem.nativeElement.textContent.trim()).toBe( + '40 % Chocolate', + ); + }); + + it('should switch to NONE display when set', () => { + rootComponent.valueDisplayMode = 'none'; + fixture.detectChanges(); + + const legendItem = getAllLegendItems()[1]; + expect(legendItem.nativeElement.textContent.trim()).toBe('Chocolate'); + }); + }); + + it('should not display value if multi series', () => { + rootComponent.valueDisplayMode = 'percent'; + fixture.detectChanges(); + + const legendItem = getAllLegendItems()[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 = getSliceByPositionWithinTrack(0, 0); + + expect(sliceByPosition.nativeElement.getAttribute('style')).toContain( + '--dt-stacked-series-chart-length: 1%', + ); + }); + + it('should ignore max with fillMode=full', () => { + rootComponent.fillMode = 'full'; + rootComponent.max = 100; + fixture.detectChanges(); + + const sliceByPosition = getSliceByPositionWithinTrack(0, 0); + + expect(sliceByPosition.nativeElement.getAttribute('style')).toContain( + '--dt-stacked-series-chart-length: 100%', + ); + }); + + it('should fill the whole bar if fillMode=full', () => { + rootComponent.fillMode = 'full'; + fixture.detectChanges(); + + const sliceByPosition = getSliceByPositionWithinTrack(0, 0); + + expect(sliceByPosition.nativeElement.getAttribute('style')).toContain( + '--dt-stacked-series-chart-length: 100%', + ); + }); + + it('should take into account the rest of series when no max if fillMode=relative', () => { + const sliceByPosition = getSliceByPositionWithinTrack(0, 0); + + expect(sliceByPosition.nativeElement.getAttribute('style')).toContain( + '--dt-stacked-series-chart-length: 20%', + ); + }); + }); + + describe('Axis', () => { + it('should not show value axis if hidden', () => { + rootComponent.visibleValueAxis = false; + fixture.detectChanges(); + + const axis = getValueAxis(); + expect(axis).toBeNull(); + }); + + it('should show value axis in column mode', () => { + rootComponent.mode = 'column'; + rootComponent.visibleValueAxis = true; + fixture.detectChanges(); + + const axis = getValueAxis(); + expect(axis).not.toBeNull(); + }); + + it('should show value axis in bar mode', () => { + rootComponent.mode = 'bar'; + rootComponent.visibleValueAxis = true; + fixture.detectChanges(); + + const axis = getValueAxis(); + + expect(axis).not.toBeNull(); + }); + + it('should show have ticks', () => { + const ticks = getAxisTicks(); + expect(ticks.length).toBeGreaterThan(0); + }); + }); + + describe('Legends', () => { + it('should show the legend', () => { + rootComponent.visibleLegend = true; + fixture.detectChanges(); + + const legend = getLegend(); + expect(legend).toBeTruthy(); + }); + + it('should hide the legend', () => { + rootComponent.visibleLegend = false; + fixture.detectChanges(); + + const legend = getLegend(); + 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 = getAllHiddenLegendItems()[0]; + const tracks = getAllTracks(); + const hiddenSlices = getAllSlices().filter((slice) => + slice.nativeElement + .getAttribute('style') + .includes('--dt-stacked-series-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 = getAllLegendItems()[0]; + dispatchFakeEvent(firstLegendItem.nativeElement, 'click'); + fixture.detectChanges(); + + const hiddenLegendItems = getAllHiddenLegendItems().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 = getAllLegendItems()[0]; + dispatchFakeEvent(firstLegendItem.nativeElement, 'click'); + fixture.detectChanges(); + + const hiddenLegendItems = getAllHiddenLegendItems().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 = getAllTrackBackgrounds(); + expect(tracksWithBackground.length).toBe(4); + }); + + it('should hide the track background when indicated', () => { + rootComponent.visibleTrackBackground = false; + fixture.detectChanges(); + + const tracksWithBackground = getAllTrackBackgrounds(); + expect(tracksWithBackground.length).toBe(0); + }); + }); + + describe('Mode', () => { + describe('Bar', () => { + it('should display all the tracks and slices', () => { + const tracks = getAllTracks(); + const slices = getAllSlices(); + + expect(tracks.length).toBe(4); + expect(slices.length).toBe(8); + }); + + it('should display left aligned labels', () => { + rootComponent.visibleLabel = true; + fixture.detectChanges(); + + const labels = getAllTrackLabels(); + expect(labels.length).toBe(4); + }); + + it('should not display the label if required', () => { + rootComponent.visibleLabel = false; + fixture.detectChanges(); + + const labels = getAllTrackLabels(); + expect(labels.length).toBe(0); + }); + + it('should not display the series axis', () => { + const axis = getSeriesAxis(); + expect(axis).toBeFalsy(); + }); + }); + + describe('Column', () => { + beforeEach(function (): void { + rootComponent.mode = 'column'; + fixture.detectChanges(); + }); + + it('should display all the tracks and slices', () => { + const tracks = getAllTracks(); + const slices = getAllSlices(); + + expect(tracks.length).toBe(4); + expect(slices.length).toBe(8); + }); + + it('should display bottom aligned labels', () => { + rootComponent.visibleLabel = true; + fixture.detectChanges(); + + const labels = getAllTrackLabels(); + expect(labels.length).toBe(4); + }); + + it('should not display the label if required', () => { + rootComponent.visibleLabel = false; + fixture.detectChanges(); + + const labels = getAllTrackLabels(); + expect(labels.length).toBe(0); + }); + + it('should display the series axis', () => { + const axis = getSeriesAxis(); + 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 = getAllSlices()[0]; + + dispatchFakeEvent(firstSlice.nativeElement, 'mouseenter'); + fixture.detectChanges(); + + const overlayPane = getOverlay(); + expect(overlayPane).toBeNull(); + }); + }); +}); + +/** Test component that contains an DtStackedSeriesChart. */ +@Component({ + selector: 'dt-test-app', + template: ` + + +
+ {{ tooltip.origin.label }} +
+
+
+ `, +}) +class TestApp { + series: DtStackedSeriesChartSeries[] = stackedSeriesChartDemoDataCoffee; + selectable: boolean = true; + selected: [DtStackedSeriesChartSeries, DtStackedSeriesChartNode] | [] = []; + valueDisplayMode: DtStackedSeriesChartValueDisplayMode; + max: number; + fillMode: DtStackedSeriesChartFillMode = 'relative'; + legends: DtStackedSeriesChartLegend[]; + visibleLegend: boolean = true; + visibleTrackBackground: boolean = true; + visibleLabel: boolean = true; + visibleValueAxis: boolean = true; + mode: DtStackedSeriesChartMode; + maxTrackSize: number; + + theme = 'blue'; + hasOverlay: boolean = true; + @ViewChild(DtStackedSeriesChart) stackedSeriesChart: DtStackedSeriesChart; +} + +/** Test component that contains an DtStackedSeriesChart. */ +@Component({ + selector: 'dt-defaults-test-app', + template: ` + + + `, +}) +class DefaultsTestApp { + series: DtStackedSeriesChartSeries[] = stackedSeriesChartDemoDataCoffee; + theme = 'blue'; + + @ViewChild(DtStackedSeriesChart) stackedSeriesChart: DtStackedSeriesChart; +} diff --git a/libs/barista-components/stacked-series-chart/src/stacked-series-chart.ts b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.ts new file mode 100644 index 0000000000..23afff99d7 --- /dev/null +++ b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.ts @@ -0,0 +1,438 @@ +/** + * @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 { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + EventEmitter, + Input, + NgZone, + OnDestroy, + Optional, + Output, + SkipSelf, + TemplateRef, + ViewChild, + ViewEncapsulation, +} from '@angular/core'; +import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; +import { DtViewportResizer } from '@dynatrace/barista-components/core'; +import { formatCount } from '@dynatrace/barista-components/formatters'; +import { DtColors, DtTheme } from '@dynatrace/barista-components/theming'; +import { scaleLinear } from 'd3-scale'; +import { merge, Subject } from 'rxjs'; +import { first, switchMapTo, takeUntil } from 'rxjs/operators'; +import { DtStackedSeriesChartNode } from '..'; +import { DtStackedSeriesChartOverlay } from './stacked-series-chart-overlay.directive'; +import { + DtStackedSeriesChartFilledSeries, + DtStackedSeriesChartFillMode, + DtStackedSeriesChartLegend, + DtStackedSeriesChartMode, + DtStackedSeriesChartSeries, + DtStackedSeriesChartTooltipData, + DtStackedSeriesChartValueDisplayMode, + fillSeries, + getLegends, + getSeriesWithState, + getTotalMaxValue, + updateNodesVisibility, +} from './stacked-series-chart.util'; +import { DtOverlayRef, DtOverlay } from '@dynatrace/barista-components/overlay'; + +// horizontal ticks +const TICK_BAR_SPACING = 160; +// vertical ticks +const TICK_COLUMN_SPACING = 80; + +@Component({ + selector: 'dt-stacked-series-chart', + exportAs: 'dtStackedSeriesChart', + templateUrl: 'stacked-series-chart.html', + styleUrls: [ + 'stacked-series-chart.scss', + 'stacked-series-chart-column.scss', + 'stacked-series-chart-bar.scss', + ], + preserveWhitespaces: false, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.Emulated, + host: { + '[class.dt-stacked-series-chart-with-legend]': 'visibleLegend', + '[class.dt-stacked-series-chart-with-value-axis]': 'visibleValueAxis', + '[class.dt-stacked-series-chart-bar]': "mode === 'bar'", + '[class.dt-stacked-series-chart-column]': "mode === 'column'", + }, +}) +export class DtStackedSeriesChart implements OnDestroy { + /** Array of series with their nodes. */ + @Input() + get series(): DtStackedSeriesChartSeries[] { + return this._series; + } + set series(value: DtStackedSeriesChartSeries[]) { + if (value !== this._series) { + this._series = value; + this._updateFilledSeries(); + this._render(); + } + } + private _series: DtStackedSeriesChartSeries[]; + /** Series with filled nodes */ + private _filledSeries: DtStackedSeriesChartFilledSeries[]; + + /** 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 ?? false; + } + } + _selectable: boolean = false; + + /** 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(): DtStackedSeriesChartFillMode { + return this._fillMode; + } + set fillMode(value: DtStackedSeriesChartFillMode) { + if (value !== this._fillMode) { + this._fillMode = value ?? 'relative'; + this._render(); + } + } + private _fillMode: DtStackedSeriesChartFillMode = 'relative'; + + /** + * Sets the display mode for the stacked-series-chart values + * in legend to either 'none' 'percent' or 'absolute'. + * Only valid for single track chart. + */ + @Input() valueDisplayMode: DtStackedSeriesChartValueDisplayMode = '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(): DtStackedSeriesChartLegend[] | undefined { + return this._legends ?? []; + } + set legends(value: DtStackedSeriesChartLegend[] | undefined) { + if (value !== this._legends) { + this._legends = + value && value.length > 0 ? value : getLegends(this._series); + + updateNodesVisibility(this._filledSeries, this._legends); + + this._render(); + } + } + _legends: DtStackedSeriesChartLegend[]; + + /** 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() + get mode(): DtStackedSeriesChartMode { + return this._mode; + } + set mode(value: DtStackedSeriesChartMode) { + if (this._mode !== value) { + this._mode = value ?? 'bar'; + // as template changes and we rely on dimensions we have to use a lifecycle hook + this._shouldUpdateTicks.next(); + } + } + _mode: DtStackedSeriesChartMode = 'bar'; + + /** Maximum size of the track */ + @Input() maxTrackSize: number = 16; + + /** Visibility of value axis */ + @Input() visibleValueAxis: boolean = true; + + /** @internal Ticks for value axis */ + _axisTicks: { pos: number; value: number; valueRelative: number }[] = []; + + /** @internal Value axis width to allow it inside the boundaries of the component */ + _valueAxisSize: { absolute: number; relative: number } = { + absolute: 0, + relative: 0, + }; + + /** Current selection [series, node] */ + @Input() + get selected(): [DtStackedSeriesChartSeries, DtStackedSeriesChartNode] | [] { + return this._selected; + } + set selected([series, node]: + | [DtStackedSeriesChartSeries, DtStackedSeriesChartNode] + | []) { + // if selected node is different than current + if (this._selected[1] !== node) { + this._toggleSelect(series, node); + } + } + private _selected: + | [DtStackedSeriesChartSeries, DtStackedSeriesChartNode] + | [] = []; + + /** Event that fires when a node is clicked with an array of [series, node] */ + @Output() selectedChange: EventEmitter< + [DtStackedSeriesChartSeries, DtStackedSeriesChartNode] | [] + > = new EventEmitter(); + + /** @internal Template reference for the DtStackedSeriesChart */ + @ContentChild(DtStackedSeriesChartOverlay, { read: TemplateRef }) + _overlay: TemplateRef; + + /** @internal Reference to the root svgElement. */ + @ViewChild('valueAxis') _valueAxis; + + /** Reference to the open overlay. */ + private _overlayRef: DtOverlayRef | null; + + /** @internal Slices to be painted */ + _tracks: DtStackedSeriesChartFilledSeries[] = []; + + /** Indicates when ticks should be recalculated */ + private _shouldUpdateTicks = new Subject(); + + /** Subject to be called upon component destroy to remove pending subscriptions */ + private readonly _destroy$ = new Subject(); + + constructor( + private readonly _changeDetectorRef: ChangeDetectorRef, + private _resizer: DtViewportResizer, + private _zone: NgZone, + private _overlayService: DtOverlay, + /** + * @deprecated Remove the sanitizer when we don't have to support ivy anymore. + * @breaking-change Remove the DomSanitizer. (Version: TBD) + */ + private readonly _sanitizer: DomSanitizer, + @Optional() @SkipSelf() private readonly _theme: DtTheme, + ) { + if (this._theme) { + this._theme._stateChanges + .pipe(takeUntil(this._destroy$)) + .subscribe(() => { + this._updateFilledSeries(); + this._render(); + this._changeDetectorRef.markForCheck(); + }); + } + + merge(this._shouldUpdateTicks, this._resizer.change()) + .pipe( + // Shift the updating/rendering to the next CD cycle, + // because we need the dimensions of axis first, which is rendered in the main cycle. + switchMapTo(this._zone.onStable.pipe(first())), + takeUntil(this._destroy$), + ) + .subscribe(() => { + // Because we are waiting for the next zoneStable cycle to actually update + // the template, we need to explicitly run this inside the zone + // otherwise, the zone will not care about any events emitted from + // the template bindings. + this._zone.run(() => { + this._updateTicks(); + this._changeDetectorRef.detectChanges(); + }); + }); + } + + ngOnDestroy(): void { + this._destroy$.next(); + this._destroy$.complete(); + } + + /** @internal Toggle the selection of an element */ + _toggleSelect( + series?: DtStackedSeriesChartSeries, + node?: DtStackedSeriesChartNode, + ): 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 = []; + } + } + + /** @internal Toggle the visibility of an element */ + _toggleLegend(slice: DtStackedSeriesChartLegend): void { + // don't allow hiding last element + if ( + this._legends.filter((node) => node.visible).length > 1 || + !slice.visible + ) { + slice.visible = !slice.visible; + updateNodesVisibility(this._filledSeries, this._legends); + + this._render(); + } + } + + /** @internal Track array by label in order to have transitions we have to track the elements in the list */ + _trackByFn( + _: number, + item: DtStackedSeriesChartTooltipData | DtStackedSeriesChartFilledSeries, + ): string { + return item.origin.label; + } + + /** + * @internal 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('; '), + ); + } + + /** + * @internal + * Handles the mouseEnter on a series slice. + * Creates an overlay if it is necessary. + */ + _handleOnSeriesMouseEnter( + event: MouseEvent, + slice: DtStackedSeriesChartTooltipData, + ): void { + if (this._overlay && !this._overlayRef) { + this._overlayRef = this._overlayService.create< + DtStackedSeriesChartTooltipData + >(event.target as HTMLElement, this._overlay); + this._overlayRef.updateImplicitContext(slice); + this._overlayRef.updatePosition(event.offsetX, event.offsetY); + } + } + + /** + * @internal + * Handles the mouseMove on a series slice. + * Updates the position of the overlay to create a mouseFollow position + */ + _handleOnSeriesMouseMove(event: MouseEvent): void { + if (this._overlayRef) { + this._overlayService._positionStrategy.setOrigin({ + x: event.clientX, + y: event.clientY, + }); + this._overlayRef.updatePosition(); + } + } + /** + * @internal + * Handles the mouseLeave on a series slice. + * Dismisses the overlay if there is one defined. + */ + _handleOnSeriesMouseLeave(): void { + if (this._overlayRef) { + this._overlayRef.dismiss(); + this._overlayRef = null; + } + } + + /** Calculate current state */ + private _render(): void { + this._tracks = getSeriesWithState( + this._filledSeries, + this._selected, + this._fillMode === 'relative' ? this.max : undefined, + ); + } + + /** Calculate legends, colors and fill series */ + private _updateFilledSeries(): void { + this._legends = getLegends(this.series, this._theme); + this._filledSeries = fillSeries(this.series, this._legends); + this._canShowValue = this.series.length === 1; + + this._shouldUpdateTicks.next(); + } + + /** Calculate the ticks used for values */ + private _updateTicks(): void { + if (this._valueAxis) { + const axisBox = this._valueAxis.nativeElement.getBoundingClientRect(); + const axisLength = this.mode === 'bar' ? axisBox.width : axisBox.height; + const tickAmount = + Math.floor( + axisLength / + (this.mode === 'bar' ? TICK_BAR_SPACING : TICK_COLUMN_SPACING), + ) + 1; + + const scale = scaleLinear() + .domain([0, this.max ?? 0]) + .range([0, 100]); + + this._axisTicks = scale.ticks(tickAmount).map((value) => { + return { + // for column scale must be inverted but d3 does not allow a reverse scale + pos: this.mode === 'bar' ? scale(value) : 100 - scale(value), + value: value, + valueRelative: this.max ? value / this.max : 0, + }; + }); + + this._valueAxisSize = { + absolute: + formatCount(this._axisTicks.slice(-1)[0].value).toString().length * + 0.6 + + 1.5, + relative: + (this._axisTicks.slice(-1)[0].valueRelative.toString().length + 1) * + 0.6 + + 1.5, + }; + } + } +} diff --git a/libs/barista-components/stacked-series-chart/src/stacked-series-chart.util.spec.ts b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.util.spec.ts new file mode 100644 index 0000000000..9381ea949c --- /dev/null +++ b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.util.spec.ts @@ -0,0 +1,577 @@ +/** + * @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 { + stackedSeriesChartDemoDataCoffee, + stackedSeriesChartDemoDataShows, +} from './stacked-series-chart.mock'; +import { + fillSeries, + getLegends, + getSeriesWithState, + getTotalMaxValue, + updateNodesVisibility, +} from './stacked-series-chart.util'; + +describe('StackedSeriesChart util', () => { + const series = stackedSeriesChartDemoDataCoffee; + 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: [ + { + ariaLabel: 'Coffee in Espresso is 1 out of 1', + color: palette[0], + origin: { label: 'Coffee', value: 1 }, + selected: false, + seriesOrigin: series[0], + valueRelative: 1, + visible: true, + }, + ], + origin: series[0], + }, + { + nodes: [ + { + ariaLabel: 'Coffee in Macchiato is 2 out of 3', + color: palette[0], + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[1], + valueRelative: 0.6666666666666666, + visible: true, + }, + { + ariaLabel: 'Milk in Macchiato is 1 out of 3', + color: palette[1], + origin: { label: 'Milk', value: 1 }, + selected: false, + seriesOrigin: series[1], + valueRelative: 0.3333333333333333, + visible: true, + }, + ], + origin: series[1], + }, + { + nodes: [ + { + ariaLabel: 'Coffee in Americano is 2 out of 5', + color: palette[0], + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[2], + valueRelative: 0.4, + visible: true, + }, + { + ariaLabel: 'Water in Americano is 3 out of 5', + color: palette[2], + origin: { label: 'Water', value: 3 }, + selected: false, + seriesOrigin: series[2], + valueRelative: 0.6, + visible: true, + }, + ], + origin: series[2], + }, + { + nodes: [ + { + ariaLabel: 'Coffee in Mocha is 2 out of 5', + color: palette[0], + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.4, + visible: true, + }, + { + ariaLabel: 'Chocolate in Mocha is 2 out of 5', + color: palette[3], + origin: { label: 'Chocolate', value: 2 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.4, + visible: true, + }, + { + ariaLabel: 'Milk in Mocha is 1 out of 5', + 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: [ + { + ariaLabel: 'Coffee in Espresso is 1 out of 1', + color: palette[0], + length: '100%', + origin: { label: 'Coffee', value: 1 }, + selected: false, + seriesOrigin: series[0], + valueRelative: 1, + visible: true, + }, + ], + origin: series[0], + }, + { + nodes: [ + { + ariaLabel: 'Coffee in Macchiato is 2 out of 3', + color: palette[0], + length: '66.66666666666667%', + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[1], + valueRelative: 0.6666666666666666, + visible: true, + }, + { + ariaLabel: 'Milk in Macchiato is 1 out of 3', + 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: [ + { + ariaLabel: 'Coffee in Americano is 2 out of 5', + color: palette[0], + length: '40%', + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[2], + valueRelative: 0.4, + visible: true, + }, + { + ariaLabel: 'Water in Americano is 3 out of 5', + color: palette[2], + length: '60%', + origin: { label: 'Water', value: 3 }, + selected: false, + seriesOrigin: series[2], + valueRelative: 0.6, + visible: true, + }, + ], + origin: series[2], + }, + { + nodes: [ + { + ariaLabel: 'Coffee in Mocha is 2 out of 5', + color: palette[0], + length: '40%', + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.4, + visible: true, + }, + { + ariaLabel: 'Chocolate in Mocha is 2 out of 5', + color: palette[3], + length: '40%', + origin: { label: 'Chocolate', value: 2 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.4, + visible: true, + }, + { + ariaLabel: 'Milk in Mocha is 1 out of 5', + 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: [ + { + ariaLabel: 'Coffee in Espresso is 1 out of 1', + color: palette[0], + length: '100%', + origin: { label: 'Coffee', value: 1 }, + selected: false, + seriesOrigin: series[0], + valueRelative: 1, + visible: true, + }, + ], + origin: series[0], + }, + { + nodes: [ + { + ariaLabel: 'Coffee in Macchiato is 2 out of 3', + color: palette[0], + length: '66.66666666666667%', + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[1], + valueRelative: 0.6666666666666666, + visible: true, + }, + { + ariaLabel: 'Milk in Macchiato is 1 out of 3', + 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: [ + { + ariaLabel: 'Coffee in Americano is 2 out of 5', + color: palette[0], + length: '40%', + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[2], + valueRelative: 0.4, + visible: true, + }, + { + ariaLabel: 'Water in Americano is 3 out of 5', + color: palette[2], + length: '60%', + origin: { label: 'Water', value: 3 }, + selected: false, + seriesOrigin: series[2], + valueRelative: 0.6, + visible: true, + }, + ], + origin: series[2], + }, + { + nodes: [ + { + ariaLabel: 'Coffee in Mocha is 2 out of 5', + color: palette[0], + length: '40%', + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.4, + visible: true, + }, + { + ariaLabel: 'Chocolate in Mocha is 2 out of 5', + color: palette[3], + length: '40%', + origin: { label: 'Chocolate', value: 2 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.4, + visible: true, + }, + { + ariaLabel: 'Milk in Mocha is 1 out of 5', + 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: [ + { + ariaLabel: 'Coffee in Espresso is 1 out of 1', + color: palette[0], + origin: { label: 'Coffee', value: 1 }, + selected: false, + seriesOrigin: series[0], + valueRelative: 1, + visible: false, + }, + ], + origin: series[0], + }, + { + nodes: [ + { + ariaLabel: 'Coffee in Macchiato is 2 out of 3', + color: palette[0], + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[1], + valueRelative: 0.6666666666666666, + visible: false, + }, + { + ariaLabel: 'Milk in Macchiato is 1 out of 3', + color: palette[1], + origin: { label: 'Milk', value: 1 }, + selected: false, + seriesOrigin: series[1], + valueRelative: 0.3333333333333333, + visible: true, + }, + ], + origin: series[1], + }, + { + nodes: [ + { + ariaLabel: 'Coffee in Americano is 2 out of 5', + color: palette[0], + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[2], + valueRelative: 0.4, + visible: false, + }, + { + ariaLabel: 'Water in Americano is 3 out of 5', + color: palette[2], + origin: { label: 'Water', value: 3 }, + selected: false, + seriesOrigin: series[2], + valueRelative: 0.6, + visible: true, + }, + ], + origin: series[2], + }, + { + nodes: [ + { + ariaLabel: 'Coffee in Mocha is 2 out of 5', + color: palette[0], + origin: { label: 'Coffee', value: 2 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.4, + visible: false, + }, + { + ariaLabel: 'Chocolate in Mocha is 2 out of 5', + color: palette[3], + origin: { label: 'Chocolate', value: 2 }, + selected: false, + seriesOrigin: series[3], + valueRelative: 0.4, + visible: true, + }, + { + ariaLabel: 'Milk in Mocha is 1 out of 5', + 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: stackedSeriesChartDemoDataShows[0].nodes[0].color, + label: 'Season 1', + visible: true, + }, + { + color: stackedSeriesChartDemoDataShows[0].nodes[1].color, + label: 'Season 2', + visible: true, + }, + { + color: stackedSeriesChartDemoDataShows[0].nodes[2].color, + label: 'Season 3', + visible: true, + }, + { + color: stackedSeriesChartDemoDataShows[0].nodes[3].color, + label: 'Season 4', + visible: true, + }, + { + color: stackedSeriesChartDemoDataShows[0].nodes[4].color, + label: 'Season 5', + visible: true, + }, + { + color: stackedSeriesChartDemoDataShows[0].nodes[5].color, + label: 'Season 6', + visible: true, + }, + ]; + const legendList = getLegends(stackedSeriesChartDemoDataShows); + + 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( + [stackedSeriesChartDemoDataCoffee[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-series-chart/src/stacked-series-chart.util.ts b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.util.ts new file mode 100644 index 0000000000..e28fed973c --- /dev/null +++ b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.util.ts @@ -0,0 +1,258 @@ +/** + * @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 DtStackedSeriesChartSeries { + /** Label of the series */ + label: string; + /** Nodes for this series */ + nodes: DtStackedSeriesChartNode[]; +} + +/** + * @internal Definition of series containing extended information for every mode + */ +export interface DtStackedSeriesChartFilledSeries { + /** Original series */ + origin: DtStackedSeriesChartSeries; + /** Filled nodes for this series */ + nodes: DtStackedSeriesChartTooltipData[]; +} + +/** + * Definition of a legend item + */ +export interface DtStackedSeriesChartLegend { + /** Label of the node */ + label: string; + /** Color assigned */ + color: DtColors | string; + /** Whether it should be visible */ + visible: boolean; +} + +/** + * DtStackedSeriesChartNode represents a single node within the sunburst datastructure. + */ +export interface DtStackedSeriesChartNode { + /** 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 DtStackedSeriesChartNode containing useful information that can be used inside the overlay's template */ +export interface DtStackedSeriesChartTooltipData { + /** Node passed by user in `series` array */ + origin: DtStackedSeriesChartNode; + /** Original parent series */ + seriesOrigin: DtStackedSeriesChartSeries; + + /** 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; + /** Text for a11y */ + ariaLabel?: string; +} + +/** For single track only, format of value to be displayed in legend */ +export type DtStackedSeriesChartValueDisplayMode = + | 'none' + | 'absolute' + | 'percent'; + +/** Whether track should be filled fully or should take into account the rest of tracks for max value */ +export type DtStackedSeriesChartFillMode = 'full' | 'relative'; + +/** Orientation of the chart */ +export type DtStackedSeriesChartMode = '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: DtStackedSeriesChartSeries[], + legends: DtStackedSeriesChartLegend[], +): DtStackedSeriesChartFilledSeries[] => + 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, + ariaLabel: `${node.label} in ${s.label} is ${ + node.value + } out of ${getValue(s.nodes)}`, + })), + })); + +/** + * @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: DtStackedSeriesChartFilledSeries[] = [], + [selectedSeries, selectedNode]: + | [DtStackedSeriesChartSeries, DtStackedSeriesChartNode] + | [], + max?: number, +): DtStackedSeriesChartFilledSeries[] => + 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: DtStackedSeriesChartFilledSeries[], + legends: DtStackedSeriesChartLegend[], +): DtStackedSeriesChartFilledSeries[] => { + 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: DtStackedSeriesChartSeries[], + theme?: DtTheme, +): DtStackedSeriesChartLegend[] => { + 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: DtStackedSeriesChartNode[]): 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: DtStackedSeriesChartTooltipData[]): 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: DtStackedSeriesChartSeries[], +): number => Math.max(...series.map((s) => getValue(s.nodes))); diff --git a/libs/barista-components/stacked-series-chart/src/test-setup.ts b/libs/barista-components/stacked-series-chart/src/test-setup.ts new file mode 100644 index 0000000000..3c66e43d72 --- /dev/null +++ b/libs/barista-components/stacked-series-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-series-chart/tsconfig.json b/libs/barista-components/stacked-series-chart/tsconfig.json new file mode 100644 index 0000000000..7484bd1248 --- /dev/null +++ b/libs/barista-components/stacked-series-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-series-chart/tsconfig.lib.json b/libs/barista-components/stacked-series-chart/tsconfig.lib.json new file mode 100644 index 0000000000..1c600457d3 --- /dev/null +++ b/libs/barista-components/stacked-series-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-series-chart/tsconfig.lib.prod.json b/libs/barista-components/stacked-series-chart/tsconfig.lib.prod.json new file mode 100644 index 0000000000..cbae794224 --- /dev/null +++ b/libs/barista-components/stacked-series-chart/tsconfig.lib.prod.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.lib.json", + "angularCompilerOptions": { + "enableIvy": false + } +} diff --git a/libs/barista-components/stacked-series-chart/tsconfig.spec.json b/libs/barista-components/stacked-series-chart/tsconfig.spec.json new file mode 100644 index 0000000000..fd405a65ef --- /dev/null +++ b/libs/barista-components/stacked-series-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-series-chart/tslint.json b/libs/barista-components/stacked-series-chart/tslint.json new file mode 100644 index 0000000000..95392b22f1 --- /dev/null +++ b/libs/barista-components/stacked-series-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 8109555aa7..26daef9aee 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 { DtExamplesStackedSeriesChartModule } from './stacked-series-chart/stacked-series-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, + DtExamplesStackedSeriesChartModule, DtExamplesStepperModule, DtSunburstChartExamplesModule, DtExamplesSwitchModule, diff --git a/libs/examples/src/index.ts b/libs/examples/src/index.ts index 2342e31e6c..a1dca76c32 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 { DtExampleStackedSeriesChartColumn } from './stacked-series-chart/stacked-series-chart-column-example/stacked-series-chart-column-example'; +import { DtExampleStackedSeriesChartConnectedLegend } from './stacked-series-chart/stacked-series-chart-connected-legend-example/stacked-series-chart-connected-legend-example'; +import { DtExampleStackedSeriesChartFilled } from './stacked-series-chart/stacked-series-chart-filled-example/stacked-series-chart-filled-example'; +import { DtExampleStackedSeriesChartGeneric } from './stacked-series-chart/stacked-series-chart-generic-example/stacked-series-chart-generic-example'; +import { DtExampleStackedSeriesChartSingle } from './stacked-series-chart/stacked-series-chart-single-example/stacked-series-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'; @@ -370,6 +375,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 { DtExamplesStackedSeriesChartModule } from './stacked-series-chart/stacked-series-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'; @@ -617,6 +623,11 @@ export { DtExampleDisabledSlider, DtExampleFractionSlider, DtExampleSimpleSlider, + DtExampleStackedSeriesChartColumn, + DtExampleStackedSeriesChartConnectedLegend, + DtExampleStackedSeriesChartFilled, + DtExampleStackedSeriesChartGeneric, + DtExampleStackedSeriesChartSingle, DtExampleStepperDefault, DtExampleStepperEditable, DtExampleStepperLinear, @@ -841,6 +852,7 @@ export const EXAMPLES_MAP = new Map>([ ], ['DtExampleFilterFieldReadOnlyTags', DtExampleFilterFieldReadOnlyTags], ['DtExampleFilterFieldUnique', DtExampleFilterFieldUnique], + ['DtExampleFilterFieldValidator', DtExampleFilterFieldValidator], ['DtExampleFormFieldDefault', DtExampleFormFieldDefault], [ 'DtExampleFormFieldErrorCustomValidator', @@ -966,6 +978,14 @@ export const EXAMPLES_MAP = new Map>([ ['DtExampleDisabledSlider', DtExampleDisabledSlider], ['DtExampleFractionSlider', DtExampleFractionSlider], ['DtExampleSimpleSlider', DtExampleSimpleSlider], + ['DtExampleStackedSeriesChartColumn', DtExampleStackedSeriesChartColumn], + [ + 'DtExampleStackedSeriesChartConnectedLegend', + DtExampleStackedSeriesChartConnectedLegend, + ], + ['DtExampleStackedSeriesChartFilled', DtExampleStackedSeriesChartFilled], + ['DtExampleStackedSeriesChartGeneric', DtExampleStackedSeriesChartGeneric], + ['DtExampleStackedSeriesChartSingle', DtExampleStackedSeriesChartSingle], ['DtExampleStepperDefault', DtExampleStepperDefault], ['DtExampleStepperEditable', DtExampleStepperEditable], ['DtExampleStepperLinear', DtExampleStepperLinear], diff --git a/libs/examples/src/stacked-series-chart/index.ts b/libs/examples/src/stacked-series-chart/index.ts new file mode 100644 index 0000000000..ee1d614c96 --- /dev/null +++ b/libs/examples/src/stacked-series-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-series-chart-single-example/stacked-series-chart-single-example'; +export * from './stacked-series-chart-connected-legend-example/stacked-series-chart-connected-legend-example'; +export * from './stacked-series-chart-generic-example/stacked-series-chart-generic-example'; +export * from './stacked-series-chart-filled-example/stacked-series-chart-filled-example'; +export * from './stacked-series-chart-examples.module'; diff --git a/libs/examples/src/stacked-series-chart/stacked-series-chart-column-example/stacked-series-chart-column-example.html b/libs/examples/src/stacked-series-chart/stacked-series-chart-column-example/stacked-series-chart-column-example.html new file mode 100644 index 0000000000..ea0bba7dcb --- /dev/null +++ b/libs/examples/src/stacked-series-chart/stacked-series-chart-column-example/stacked-series-chart-column-example.html @@ -0,0 +1,12 @@ + + + {{ tooltip.seriesOrigin.label }} +
+ {{ tooltip.origin.label }}: {{ tooltip.origin.value }} +
+
diff --git a/libs/examples/src/stacked-series-chart/stacked-series-chart-column-example/stacked-series-chart-column-example.ts b/libs/examples/src/stacked-series-chart/stacked-series-chart-column-example/stacked-series-chart-column-example.ts new file mode 100644 index 0000000000..aeb9fcaa4c --- /dev/null +++ b/libs/examples/src/stacked-series-chart/stacked-series-chart-column-example/stacked-series-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 { stackedSeriesChartDemoDataCoffee } from '../stacked-series-chart-demo-data'; + +@Component({ + selector: 'dt-example-stacked-series-chart-column-barista', + templateUrl: './stacked-series-chart-column-example.html', +}) +export class DtExampleStackedSeriesChartColumn { + series = stackedSeriesChartDemoDataCoffee; +} diff --git a/libs/examples/src/stacked-series-chart/stacked-series-chart-connected-legend-example/stacked-series-chart-connected-legend-example.html b/libs/examples/src/stacked-series-chart/stacked-series-chart-connected-legend-example/stacked-series-chart-connected-legend-example.html new file mode 100644 index 0000000000..3f96b5c507 --- /dev/null +++ b/libs/examples/src/stacked-series-chart/stacked-series-chart-connected-legend-example/stacked-series-chart-connected-legend-example.html @@ -0,0 +1,43 @@ + + + + + {{ node.label }} + + + + + + + Episodes + + + + + {{ tooltip.origin.label }}: + {{ tooltip.origin.value }} episodes + + + + + + + + + diff --git a/libs/examples/src/stacked-series-chart/stacked-series-chart-connected-legend-example/stacked-series-chart-connected-legend-example.scss b/libs/examples/src/stacked-series-chart/stacked-series-chart-connected-legend-example/stacked-series-chart-connected-legend-example.scss new file mode 100644 index 0000000000..218174133e --- /dev/null +++ b/libs/examples/src/stacked-series-chart/stacked-series-chart-connected-legend-example/stacked-series-chart-connected-legend-example.scss @@ -0,0 +1,17 @@ +@import '../../../../../libs/barista-components/core/src/style/variables'; + +dt-stacked-series-chart { + width: 100%; +} + +.dt-stacked-series-chart-demo-legend-symbol { + background: var(--node-color); + width: 12px; + height: 12px; +} + +.dt-stacked-series-chart-demo-legend-hidden { + .dt-stacked-series-chart-demo-legend-symbol { + background-color: $gray-300; + } +} diff --git a/libs/examples/src/stacked-series-chart/stacked-series-chart-connected-legend-example/stacked-series-chart-connected-legend-example.ts b/libs/examples/src/stacked-series-chart/stacked-series-chart-connected-legend-example/stacked-series-chart-connected-legend-example.ts new file mode 100644 index 0000000000..4958431acd --- /dev/null +++ b/libs/examples/src/stacked-series-chart/stacked-series-chart-connected-legend-example/stacked-series-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 { stackedSeriesChartDemoDataShows } from '../stacked-series-chart-demo-data'; +import { DtTableDataSource } from '@dynatrace/barista-components/table'; +import { DtStackedSeriesChartLegend } from '@dynatrace/barista-components/stacked-series-chart'; + +@Component({ + selector: 'dt-example-stacked-series-chart-connected-legend-barista', + templateUrl: './stacked-series-chart-connected-legend-example.html', + styleUrls: ['./stacked-series-chart-connected-legend-example.scss'], +}) +export class DtExampleStackedSeriesChartConnectedLegend { + shows = stackedSeriesChartDemoDataShows; + dataSource = new DtTableDataSource(stackedSeriesChartDemoDataShows); + legends = this.shows[0].nodes.map((node) => ({ + label: node.label, + color: node.color, + visible: true, + })); + + _toggleNode(node: DtStackedSeriesChartLegend): void { + node.visible = !node.visible; + this.legends = this.legends.slice(); + } +} diff --git a/libs/examples/src/stacked-series-chart/stacked-series-chart-demo-data.ts b/libs/examples/src/stacked-series-chart/stacked-series-chart-demo-data.ts new file mode 100644 index 0000000000..705301cb22 --- /dev/null +++ b/libs/examples/src/stacked-series-chart/stacked-series-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 { DtStackedSeriesChartSeries } from '@dynatrace/barista-components/stacked-series-chart'; + +export const stackedSeriesChartDemoDataCoffee = [ + { + 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 stackedSeriesChartDemoDataShows: DtStackedSeriesChartSeries[] = [ + { + 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-series-chart/stacked-series-chart-examples.module.ts b/libs/examples/src/stacked-series-chart/stacked-series-chart-examples.module.ts new file mode 100644 index 0000000000..911029beae --- /dev/null +++ b/libs/examples/src/stacked-series-chart/stacked-series-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 { DtStackedSeriesChartModule } from '@dynatrace/barista-components/stacked-series-chart'; +import { DtExampleStackedSeriesChartSingle } from './stacked-series-chart-single-example/stacked-series-chart-single-example'; +import { DtExampleStackedSeriesChartConnectedLegend } from './stacked-series-chart-connected-legend-example/stacked-series-chart-connected-legend-example'; +import { DtExampleStackedSeriesChartGeneric } from './stacked-series-chart-generic-example/stacked-series-chart-generic-example'; +import { DtExampleStackedSeriesChartFilled } from './stacked-series-chart-filled-example/stacked-series-chart-filled-example'; +import { DtExampleStackedSeriesChartColumn } from './stacked-series-chart-column-example/stacked-series-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_SERIES_CHART_EXAMPLES = [ + DtExampleStackedSeriesChartSingle, + DtExampleStackedSeriesChartConnectedLegend, + DtExampleStackedSeriesChartGeneric, + DtExampleStackedSeriesChartFilled, + DtExampleStackedSeriesChartColumn, +]; + +@NgModule({ + imports: [ + CommonModule, + DtStackedSeriesChartModule, + DtFormattersModule, + DtButtonGroupModule, + DtLegendModule, + DtTableModule, + ], + declarations: [...DT_SINGLE_STACKED_SERIES_CHART_EXAMPLES], + entryComponents: [...DT_SINGLE_STACKED_SERIES_CHART_EXAMPLES], +}) +export class DtExamplesStackedSeriesChartModule {} diff --git a/libs/examples/src/stacked-series-chart/stacked-series-chart-filled-example/stacked-series-chart-filled-example.html b/libs/examples/src/stacked-series-chart/stacked-series-chart-filled-example/stacked-series-chart-filled-example.html new file mode 100644 index 0000000000..45dc09e32d --- /dev/null +++ b/libs/examples/src/stacked-series-chart/stacked-series-chart-filled-example/stacked-series-chart-filled-example.html @@ -0,0 +1,15 @@ + + + {{ tooltip.origin.label }}: {{ tooltip.valueRelative * 100 | dtPercent }} + + + +
Fill mode
+ + Full + Relative + diff --git a/libs/examples/src/stacked-series-chart/stacked-series-chart-filled-example/stacked-series-chart-filled-example.ts b/libs/examples/src/stacked-series-chart/stacked-series-chart-filled-example/stacked-series-chart-filled-example.ts new file mode 100644 index 0000000000..0c5eb0fbf4 --- /dev/null +++ b/libs/examples/src/stacked-series-chart/stacked-series-chart-filled-example/stacked-series-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 { stackedSeriesChartDemoDataCoffee } from '../stacked-series-chart-demo-data'; + +@Component({ + selector: 'dt-example-stacked-series-chart-filled-barista', + templateUrl: './stacked-series-chart-filled-example.html', +}) +export class DtExampleStackedSeriesChartFilled { + series = stackedSeriesChartDemoDataCoffee; + fillMode = 'full'; +} diff --git a/libs/examples/src/stacked-series-chart/stacked-series-chart-generic-example/stacked-series-chart-generic-example.html b/libs/examples/src/stacked-series-chart/stacked-series-chart-generic-example/stacked-series-chart-generic-example.html new file mode 100644 index 0000000000..1765989e1c --- /dev/null +++ b/libs/examples/src/stacked-series-chart/stacked-series-chart-generic-example/stacked-series-chart-generic-example.html @@ -0,0 +1,7 @@ + + + {{ tooltip.seriesOrigin.label }} +
+ {{ tooltip.origin.label }}: {{ tooltip.origin.value }} +
+
diff --git a/libs/examples/src/stacked-series-chart/stacked-series-chart-generic-example/stacked-series-chart-generic-example.ts b/libs/examples/src/stacked-series-chart/stacked-series-chart-generic-example/stacked-series-chart-generic-example.ts new file mode 100644 index 0000000000..db22163036 --- /dev/null +++ b/libs/examples/src/stacked-series-chart/stacked-series-chart-generic-example/stacked-series-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 { stackedSeriesChartDemoDataCoffee } from '../stacked-series-chart-demo-data'; + +@Component({ + selector: 'dt-example-stacked-series-chart-generic-barista', + templateUrl: './stacked-series-chart-generic-example.html', +}) +export class DtExampleStackedSeriesChartGeneric { + series = stackedSeriesChartDemoDataCoffee; +} diff --git a/libs/examples/src/stacked-series-chart/stacked-series-chart-single-example/stacked-series-chart-single-example.html b/libs/examples/src/stacked-series-chart/stacked-series-chart-single-example/stacked-series-chart-single-example.html new file mode 100644 index 0000000000..6de7b800ba --- /dev/null +++ b/libs/examples/src/stacked-series-chart/stacked-series-chart-single-example/stacked-series-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-series-chart/stacked-series-chart-single-example/stacked-series-chart-single-example.ts b/libs/examples/src/stacked-series-chart/stacked-series-chart-single-example/stacked-series-chart-single-example.ts new file mode 100644 index 0000000000..1041605298 --- /dev/null +++ b/libs/examples/src/stacked-series-chart/stacked-series-chart-single-example/stacked-series-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 { stackedSeriesChartDemoDataCoffee } from '../stacked-series-chart-demo-data'; + +@Component({ + selector: 'dt-example-stacked-series-chart-single-barista', + templateUrl: './stacked-series-chart-single-example.html', +}) +export class DtExampleStackedSeriesChartSingle { + series = [stackedSeriesChartDemoDataCoffee[2]]; + valueDisplayMode = 'percent'; +} diff --git a/nx.json b/nx.json index d8d2f4f9f9..24fc0aba9d 100644 --- a/nx.json +++ b/nx.json @@ -112,6 +112,7 @@ "select", "slider", "show-more", + "stacked-series-chart", "stepper", "sunburst-chart", "switch", @@ -275,6 +276,9 @@ "show-more": { "tags": ["scope:components", "type:library"] }, + "stacked-series-chart": { + "tags": ["scope:components", "type:library"] + }, "stepper": { "tags": ["scope:components", "type:library"] }, diff --git a/tsconfig.json b/tsconfig.json index ba5463e847..5427dc2573 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-series-chart": [ + "libs/barista-components/stacked-series-chart/index.ts" + ], "@dynatrace/barista-components/stepper": [ "libs/barista-components/stepper/index.ts" ],