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/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..c120fe5788
--- /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..e6848c4d07
--- /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..2f464683ab
--- /dev/null
+++ b/libs/barista-components/stacked-series-chart/src/stacked-series-chart-overlay.directive.ts
@@ -0,0 +1,29 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Directive } from '@angular/core';
+
+/**
+ * Overlay directive to be used alongside with StackedSeriesChart.
+ *
+ * [content of overlay here]
+ *
+ */
+@Directive({
+ selector: 'ng-template[dtStackedSeriesChartOverlay]',
+ exportAs: 'dtStackedSeriesChartOverlay',
+})
+export class DtStackedSeriesChartOverlay {}
diff --git a/libs/barista-components/stacked-series-chart/src/stacked-series-chart-path.ts b/libs/barista-components/stacked-series-chart/src/stacked-series-chart-path.ts
new file mode 100644
index 0000000000..a4d110cc9e
--- /dev/null
+++ b/libs/barista-components/stacked-series-chart/src/stacked-series-chart-path.ts
@@ -0,0 +1,62 @@
+/**
+ * @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 { AfterContentInit, Directive, Input, TemplateRef } from '@angular/core';
+import { DtOverlayTrigger } from '@dynatrace/barista-components/overlay';
+import { DtStackedSeriesChartTooltipData } from './stacked-series-chart.util';
+
+export interface DtStackedSeriesChartOverlayData {
+ name: string;
+ value: number;
+ totalValue: number;
+}
+
+@Directive({
+ selector: '[dt-stacked-series-chart-path]',
+ host: {
+ '(mouseenter)': 'overlayTemplate && _handleMouseEnter($event)',
+ '(mouseleave)': 'overlayTemplate && _handleMouseLeave($event)',
+ },
+})
+export class DtStackedSeriesChartPath
+ extends DtOverlayTrigger<{ $implicit: DtStackedSeriesChartOverlayData }>
+ implements AfterContentInit {
+ /**
+ * @internal
+ * All data needed to render the path that visualizes
+ * the given node.
+ */
+ @Input() overlayData: DtStackedSeriesChartTooltipData;
+
+ /**
+ * @internal
+ * The template ref for the overlay template.
+ * Overlay is displayed when hovering over
+ * the chart node.
+ */
+ @Input() overlayTemplate: TemplateRef<{
+ $implicit: DtStackedSeriesChartOverlayData;
+ }>;
+
+ ngAfterContentInit(): void {
+ if (this.overlayTemplate) {
+ this.overlay = this.overlayTemplate;
+ this.dtOverlayConfig = {
+ data: this.overlayData,
+ };
+ }
+ }
+}
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..764bf32496
--- /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..b05ce8b5ea
--- /dev/null
+++ b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.module.ts
@@ -0,0 +1,39 @@
+/**
+ * @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';
+import { DtStackedSeriesChartPath } from './stacked-series-chart-path';
+
+@NgModule({
+ imports: [CommonModule, DtFormattersModule, DtLegendModule, DtOverlayModule],
+ exports: [DtStackedSeriesChart, DtStackedSeriesChartOverlay],
+ declarations: [
+ DtStackedSeriesChart,
+ DtStackedSeriesChartOverlay,
+ DtStackedSeriesChartPath,
+ ],
+ 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..453093626c
--- /dev/null
+++ b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.scss
@@ -0,0 +1,110 @@
+@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;
+ }
+}
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..0aab8a4c8d
--- /dev/null
+++ b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.spec.ts
@@ -0,0 +1,640 @@
+/**
+ * @license
+ * Copyright 2020 Dynatrace LLC
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { OverlayContainer } from '@angular/cdk/overlay';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { Component, ViewChild } from '@angular/core';
+import {
+ async,
+ ComponentFixture,
+ inject,
+ TestBed,
+} from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import {
+ DtUiTestConfiguration,
+ DT_UI_TEST_CONFIG,
+} from '@dynatrace/barista-components/core';
+import { DtIconModule } from '@dynatrace/barista-components/icon';
+import { DtThemingModule } from '@dynatrace/barista-components/theming';
+import { createComponent, dispatchFakeEvent } from '@dynatrace/testing/browser';
+import { 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;
+ const getSliceByPosition = (track, slice) => {
+ return fixture.debugElement
+ .queryAll(By.css(selectors.track))
+ [track].queryAll(By.css(selectors.slice))[slice];
+ };
+
+ const selectors = {
+ overlay: '.dt-stacked-series-chart-overlay-panel',
+ track: '.dt-stacked-series-chart-track',
+ trackLabel: '.dt-stacked-series-chart-track-label',
+ trackWithBackground: '.dt-stacked-series-chart-track-background',
+ slice: '.dt-stacked-series-chart-slice',
+ sliceSelected: '.dt-stacked-series-chart-slice-selected',
+ seriesAxis: '.dt-stacked-series-chart-series-axis',
+ valueAxis: '.dt-stacked-series-chart-value-axis',
+ valueAxisTicks: '.dt-stacked-series-chart-axis-tick',
+ legend: 'dt-legend',
+ legendItem: 'dt-legend-item',
+ legendItemHidden: '.dt-stacked-series-chart-legend-item-hidden',
+ };
+
+ 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 = fixture.debugElement.queryAll(By.css(selectors.track));
+ const slice = getSliceByPosition(0, 0);
+
+ expect(tracks.length).toEqual(stackedSeriesChartDemoDataCoffee.length);
+
+ expect(slice.properties.style).toContain(
+ '--dt-stacked-series-chart-length: 20%',
+ );
+ });
+
+ it('should update data after series input change', () => {
+ const oldTracks = fixture.debugElement.queryAll(By.css(selectors.track));
+
+ const newSeries = stackedSeriesChartDemoDataCoffee.slice(1);
+ rootComponent.series = newSeries;
+ fixture.detectChanges();
+
+ const newTracks = fixture.debugElement.queryAll(By.css(selectors.track));
+ const newSlice = getSliceByPosition(0, 0);
+
+ expect(newTracks.length).not.toEqual(oldTracks.length);
+ expect(newTracks.length).toEqual(newSeries.length);
+
+ expect(newSlice.properties.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 = getSliceByPosition(1, 1);
+ const selected = fixture.debugElement.query(
+ By.css(selectors.sliceSelected),
+ );
+
+ expect(selected).toBe(sliceByPosition);
+ });
+
+ it('should make a selection', () => {
+ const sliceByPosition = getSliceByPosition(1, 1);
+ dispatchFakeEvent(sliceByPosition.nativeElement, 'click');
+ fixture.detectChanges();
+
+ const selected = fixture.debugElement.query(
+ By.css(selectors.sliceSelected),
+ );
+
+ expect(selectedChangeSpy).toHaveBeenCalledWith([
+ component._tracks[1].origin,
+ component._tracks[1].nodes[1].origin,
+ ]);
+ expect(selected).toBe(sliceByPosition);
+ });
+
+ it('should not allow selection from input if disabled', () => {
+ rootComponent.selectable = false;
+ rootComponent.selected = [
+ stackedSeriesChartDemoDataCoffee[1],
+ stackedSeriesChartDemoDataCoffee[1].nodes[1],
+ ];
+ fixture.detectChanges();
+
+ const selected = fixture.debugElement.query(
+ By.css(selectors.sliceSelected),
+ );
+
+ expect(selected).toBe(null);
+ });
+
+ it('should not allow selection from template if disabled', () => {
+ rootComponent.selectable = false;
+ fixture.detectChanges();
+ selectedChangeSpy.mockClear();
+
+ const sliceByPosition = getSliceByPosition(1, 1);
+ dispatchFakeEvent(sliceByPosition.nativeElement, 'click');
+
+ expect(selectedChangeSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Value Display Mode', () => {
+ describe('Single tracked', () => {
+ beforeEach(() => {
+ rootComponent.visibleLegend = true;
+
+ rootComponent.series = [stackedSeriesChartDemoDataCoffee[3]];
+ fixture.detectChanges();
+ });
+
+ it('should switch to ABSOLUTE display when set', () => {
+ rootComponent.valueDisplayMode = 'absolute';
+ fixture.detectChanges();
+
+ const legendItem = fixture.debugElement.queryAll(
+ By.css(selectors.legendItem),
+ )[1];
+
+ expect(legendItem.nativeElement.textContent.trim()).toBe(
+ '2 Chocolate',
+ );
+ });
+
+ it('should switch to PERCENT display when set', () => {
+ rootComponent.valueDisplayMode = 'percent';
+ fixture.detectChanges();
+
+ const legendItem = fixture.debugElement.queryAll(
+ By.css(selectors.legendItem),
+ )[1];
+
+ expect(legendItem.nativeElement.textContent.trim()).toBe(
+ '40 % Chocolate',
+ );
+ });
+
+ it('should switch to NONE display when set', () => {
+ rootComponent.valueDisplayMode = 'none';
+ fixture.detectChanges();
+
+ const legendItem = fixture.debugElement.queryAll(
+ By.css(selectors.legendItem),
+ )[1];
+
+ expect(legendItem.nativeElement.textContent.trim()).toBe('Chocolate');
+ });
+ });
+
+ it('should not display value if multi series', () => {
+ rootComponent.valueDisplayMode = 'percent';
+ fixture.detectChanges();
+
+ const legendItem = fixture.debugElement.queryAll(
+ By.css(selectors.legendItem),
+ )[1];
+
+ expect(legendItem.nativeElement.textContent.trim()).toBe('Milk');
+ });
+ });
+
+ describe('Max + Fill mode', () => {
+ it('should allow a max with fillMode=relative', () => {
+ rootComponent.max = 100;
+ fixture.detectChanges();
+
+ const sliceByPosition = getSliceByPosition(0, 0);
+
+ expect(sliceByPosition.properties.style).toContain(
+ '--dt-stacked-series-chart-length: 1%',
+ );
+ });
+
+ it('should ignore max with fillMode=full', () => {
+ rootComponent.fillMode = 'full';
+ rootComponent.max = 100;
+ fixture.detectChanges();
+
+ const sliceByPosition = getSliceByPosition(0, 0);
+
+ expect(sliceByPosition.properties.style).toContain(
+ '--dt-stacked-series-chart-length: 100%',
+ );
+ });
+
+ it('should fill the whole bar if fillMode=full', () => {
+ rootComponent.fillMode = 'full';
+ fixture.detectChanges();
+
+ const sliceByPosition = getSliceByPosition(0, 0);
+
+ expect(sliceByPosition.properties.style).toContain(
+ '--dt-stacked-series-chart-length: 100%',
+ );
+ });
+
+ it('should take into account the rest of series when no max if fillMode=relative', () => {
+ const sliceByPosition = getSliceByPosition(0, 0);
+
+ expect(sliceByPosition.properties.style).toContain(
+ '--dt-stacked-series-chart-length: 20%',
+ );
+ });
+ });
+
+ describe('Axis', () => {
+ it('should not show value axis if hidden', () => {
+ rootComponent.visibleValueAxis = false;
+ fixture.detectChanges();
+
+ const axis = fixture.debugElement.query(By.css(selectors.valueAxis));
+
+ expect(axis).toBeNull();
+ });
+
+ it('should show value axis in column mode', () => {
+ rootComponent.mode = 'column';
+ rootComponent.visibleValueAxis = true;
+ fixture.detectChanges();
+
+ const axis = fixture.debugElement.query(By.css(selectors.valueAxis));
+
+ expect(axis).not.toBeNull();
+ });
+
+ it('should show value axis in bar mode', () => {
+ rootComponent.mode = 'bar';
+ rootComponent.visibleValueAxis = true;
+ fixture.detectChanges();
+
+ const axis = fixture.debugElement.query(By.css(selectors.valueAxis));
+
+ expect(axis).not.toBeNull();
+ });
+
+ it('should show have ticks', () => {
+ const ticks = fixture.debugElement.queryAll(
+ By.css(selectors.valueAxisTicks),
+ );
+
+ expect(ticks.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe('Legends', () => {
+ it('should show the legend', () => {
+ rootComponent.visibleLegend = true;
+ fixture.detectChanges();
+
+ const legend = fixture.debugElement.query(By.css(selectors.legend));
+
+ expect(legend).toBeTruthy();
+ });
+
+ it('should hide the legend', () => {
+ rootComponent.visibleLegend = false;
+ fixture.detectChanges();
+
+ const legend = fixture.debugElement.query(By.css(selectors.legend));
+
+ expect(legend).toBeFalsy();
+ });
+
+ it('should hide nodes if hidden in legend', () => {
+ const legends = component.legends?.slice() ?? [];
+ // Coffee node
+ legends[0].visible = false;
+ rootComponent.legends = legends;
+ fixture.detectChanges();
+
+ const hiddenLegendItem = fixture.debugElement.query(
+ By.css(selectors.legendItemHidden),
+ );
+ const tracks = fixture.debugElement.queryAll(By.css(selectors.track));
+ const hiddenSlices = fixture.debugElement
+ .queryAll(By.css(selectors.slice))
+ .filter((slice) =>
+ slice.properties.style.includes(
+ '--dt-stacked-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 = fixture.debugElement.query(
+ By.css(selectors.legendItem),
+ );
+ dispatchFakeEvent(firstLegendItem.nativeElement, 'click');
+ fixture.detectChanges();
+
+ const hiddenLegendItems = fixture.debugElement
+ .queryAll(By.css(selectors.legendItemHidden))
+ .map((item) => item.nativeElement.textContent.trim());
+
+ expect(hiddenLegendItems).toEqual(['Coffee']);
+ });
+
+ it('should not allow all legends to be hidden', () => {
+ const legends = component.legends?.slice() ?? [];
+ legends.forEach((legend, i) => (legend.visible = i === 0));
+ rootComponent.legends = legends;
+ fixture.detectChanges();
+
+ const firstLegendItem = fixture.debugElement.query(
+ By.css(selectors.legendItem),
+ );
+ dispatchFakeEvent(firstLegendItem.nativeElement, 'click');
+ fixture.detectChanges();
+
+ const hiddenLegendItems = fixture.debugElement
+ .queryAll(By.css(selectors.legendItemHidden))
+ .map((item) => item.nativeElement.textContent.trim());
+
+ expect(hiddenLegendItems).toEqual(['Milk', 'Water', 'Chocolate']);
+ });
+ });
+
+ describe('Track background', () => {
+ it('should show the track background by default', () => {
+ const tracksWithBackground = fixture.debugElement.queryAll(
+ By.css(selectors.trackWithBackground),
+ );
+
+ expect(tracksWithBackground.length).toBe(4);
+ });
+
+ it('should hide the track background when indicated', () => {
+ rootComponent.visibleTrackBackground = false;
+ fixture.detectChanges();
+
+ const tracksWithBackground = fixture.debugElement.queryAll(
+ By.css(selectors.trackWithBackground),
+ );
+
+ expect(tracksWithBackground.length).toBe(0);
+ });
+ });
+
+ describe('Mode', () => {
+ describe('Bar', () => {
+ it('should display all the tracks and slices', () => {
+ const tracks = fixture.debugElement.queryAll(By.css(selectors.track));
+ const slices = fixture.debugElement.queryAll(By.css(selectors.slice));
+
+ expect(tracks.length).toBe(4);
+ expect(slices.length).toBe(8);
+ });
+
+ it('should display left aligned labels', () => {
+ rootComponent.visibleLabel = true;
+ fixture.detectChanges();
+
+ const labels = fixture.debugElement.queryAll(
+ By.css(selectors.trackLabel),
+ );
+
+ expect(labels.length).toBe(4);
+ });
+
+ it('should not display the label if required', () => {
+ rootComponent.visibleLabel = false;
+ fixture.detectChanges();
+
+ const labels = fixture.debugElement.queryAll(
+ By.css(selectors.trackLabel),
+ );
+
+ expect(labels.length).toBe(0);
+ });
+
+ it('should not display the series axis', () => {
+ const axis = fixture.debugElement.query(By.css(selectors.seriesAxis));
+
+ expect(axis).toBeFalsy();
+ });
+ });
+
+ describe('Column', () => {
+ beforeEach(function (): void {
+ rootComponent.mode = 'column';
+ fixture.detectChanges();
+ });
+
+ it('should display all the tracks and slices', () => {
+ const tracks = fixture.debugElement.queryAll(By.css(selectors.track));
+ const slices = fixture.debugElement.queryAll(By.css(selectors.slice));
+
+ expect(tracks.length).toBe(4);
+ expect(slices.length).toBe(8);
+ });
+
+ it('should display bottom aligned labels', () => {
+ rootComponent.visibleLabel = true;
+ fixture.detectChanges();
+
+ const labels = fixture.debugElement.queryAll(
+ By.css(selectors.trackLabel),
+ );
+
+ expect(labels.length).toBe(4);
+ });
+
+ it('should not display the label if required', () => {
+ rootComponent.visibleLabel = false;
+ fixture.detectChanges();
+
+ const labels = fixture.debugElement.queryAll(
+ By.css(selectors.trackLabel),
+ );
+
+ expect(labels.length).toBe(0);
+ });
+
+ it('should display the series axis', () => {
+ const axis = fixture.debugElement.query(By.css(selectors.seriesAxis));
+
+ expect(axis).toBeTruthy();
+ });
+ });
+ });
+
+ describe('Overlay', () => {
+ beforeEach(inject([OverlayContainer], (oc: OverlayContainer) => {
+ overlayContainer = oc;
+ overlayContainerElement = oc.getContainerElement();
+ }));
+
+ it('should have an overlay container defined', () => {
+ expect(overlayContainer).toBeDefined();
+ });
+
+ it('should not display an overlay if missing', () => {
+ rootComponent.hasOverlay = false;
+ fixture.detectChanges();
+
+ const firstSlice = fixture.debugElement.query(By.css(selectors.slice));
+
+ dispatchFakeEvent(firstSlice.nativeElement, 'mouseenter');
+ fixture.detectChanges();
+
+ const overlayPane = overlayContainerElement.querySelector(
+ selectors.overlay,
+ );
+ expect(overlayPane).toBeNull();
+ });
+ });
+});
+
+/** 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..bcc248200f
--- /dev/null
+++ b/libs/barista-components/stacked-series-chart/src/stacked-series-chart.ts
@@ -0,0 +1,385 @@
+/**
+ * @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,
+ Inject,
+ 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 { DtCount } 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';
+
+// 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 {
+ /** @internal Overlay reference */
+ @ContentChild(DtStackedSeriesChartOverlay, { read: TemplateRef })
+ _overlay: TemplateRef;
+
+ /** @internal Slices to be painted */
+ _tracks: DtStackedSeriesChartFilledSeries[] = [];
+
+ /** 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;
+
+ /** 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();
+
+ /** 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;
+
+ /** @internal Reference to the root svgElement. */
+ @ViewChild('valueAxis') _valueAxis;
+
+ /** 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,
+ };
+
+ /** 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,
+ @Inject(DtCount) private _dtCount: DtCount,
+ // TODO: remove this sanitizer when ivy is no longer opt out
+ 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;
+ }
+
+ /** 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:
+ this._dtCount.transform(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,
+ };
+ }
+ }
+
+ /**
+ * @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('; '),
+ );
+ }
+}
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"
],