Skip to content

Commit 70e563c

Browse files
Merge pull request #1877 from carbon-design-system/bar-chart-editor
feat(dashboard-editor): Bar chart support for dashboard editor
2 parents 6f3e0a0 + e5727e7 commit 70e563c

25 files changed

Lines changed: 1901 additions & 507 deletions

src/components/BarChartCard/BarChartCard.jsx

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import React from 'react';
1+
import React, { useMemo } from 'react';
22
import {
33
SimpleBarChart,
44
StackedBarChart,
55
GroupedBarChart,
66
} from '@carbon/charts-react';
77
import classnames from 'classnames';
88
import isEmpty from 'lodash/isEmpty';
9-
import memoize from 'lodash/memoize';
109

1110
import {
1211
BarChartCardPropTypes,
@@ -31,6 +30,7 @@ import { csvDownloadHandler } from '../../utils/componentUtilityFunctions';
3130

3231
import {
3332
generateSampleValues,
33+
generateSampleValuesForEditor,
3434
formatChartData,
3535
mapValuesToAxes,
3636
formatColors,
@@ -41,24 +41,25 @@ import {
4141

4242
const { iotPrefix } = settings;
4343

44-
const memoizedGenerateSampleValues = memoize(generateSampleValues);
45-
4644
const BarChartCard = ({
4745
title: titleProp,
4846
content,
4947
children,
5048
size: sizeProp,
5149
values: initialValues,
50+
availableDimensions,
5251
locale,
5352
i18n,
5453
isExpanded,
5554
isLazyLoading,
5655
isEditable,
56+
isDashboardPreview,
5757
isLoading,
5858
isResizable,
5959
interval,
6060
className,
6161
domainRange,
62+
timeRange,
6263
...others
6364
}) => {
6465
const { noDataLabel } = i18n;
@@ -84,24 +85,57 @@ const BarChartCard = ({
8485

8586
const resizeHandles = isResizable ? getResizeHandles(children) : [];
8687

88+
const memoizedGenerateSampleValues = useMemo(
89+
() =>
90+
generateSampleValues(
91+
series,
92+
timeDataSourceId,
93+
interval,
94+
timeRange,
95+
categoryDataSourceId
96+
),
97+
// eslint-disable-next-line react-hooks/exhaustive-deps
98+
[series, interval, timeRange]
99+
);
100+
101+
const memoizedGenerateSampleValuesForEditor = useMemo(
102+
() =>
103+
generateSampleValuesForEditor(
104+
valuesProp,
105+
categoryDataSourceId,
106+
timeDataSourceId,
107+
availableDimensions,
108+
interval,
109+
timeRange
110+
),
111+
// eslint-disable-next-line react-hooks/exhaustive-deps
112+
[
113+
availableDimensions,
114+
categoryDataSourceId,
115+
isDashboardPreview,
116+
timeDataSourceId,
117+
interval,
118+
timeRange,
119+
valuesProp,
120+
series.length,
121+
]
122+
);
123+
87124
// If editable, show sample presentation data
88125
// If there is no series defined, there is no datasets to make sample data from
89-
const values =
90-
isEditable && !isEmpty(series)
91-
? memoizedGenerateSampleValues(
92-
series,
93-
timeDataSourceId,
94-
interval,
95-
categoryDataSourceId
96-
)
97-
: valuesProp;
126+
const values = isDashboardPreview
127+
? memoizedGenerateSampleValuesForEditor
128+
: isEditable && !isEmpty(series)
129+
? memoizedGenerateSampleValues
130+
: valuesProp;
98131

99132
const chartData = formatChartData(
100133
series,
101134
values,
102135
categoryDataSourceId,
103136
timeDataSourceId,
104-
type
137+
type,
138+
isDashboardPreview
105139
);
106140

107141
const isAllValuesEmpty = isEmpty(chartData);
@@ -127,7 +161,7 @@ const BarChartCard = ({
127161
? [...new Set(chartData.map((dataset) => dataset.group))]
128162
: [];
129163
const colors = !isAllValuesEmpty
130-
? formatColors(series, uniqueDatasets, isEditable)
164+
? formatColors(series, uniqueDatasets, isDashboardPreview, type)
131165
: null;
132166

133167
let tableColumns = [];
@@ -168,6 +202,7 @@ const BarChartCard = ({
168202
isLoading={isLoading}
169203
isResizable={isResizable}
170204
resizeHandles={resizeHandles}
205+
timeRange={timeRange}
171206
{...others}>
172207
{!isAllValuesEmpty ? (
173208
<div
@@ -176,6 +211,13 @@ const BarChartCard = ({
176211
[`${iotPrefix}--bar-chart-container--editable`]: isEditable,
177212
})}>
178213
<ChartComponent
214+
// When showing the dashboard editor preview, we need to recalculate the chart scale
215+
// because the data is added and removed dynamically
216+
key={
217+
isDashboardPreview
218+
? `bar-chart_preview_${values.length}_${series.length}`
219+
: 'bar-chart'
220+
}
179221
data={chartData}
180222
options={{
181223
animations: false,

src/components/BarChartCard/barChartUtils.js

Lines changed: 146 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import moment from 'moment';
22
import isNil from 'lodash/isNil';
33
import isEmpty from 'lodash/isEmpty';
44
import capitalize from 'lodash/capitalize';
5+
import omit from 'lodash/omit';
56

67
import {
78
BAR_CHART_TYPES,
@@ -21,8 +22,17 @@ export const generateSampleValues = (
2122
series,
2223
timeDataSourceId,
2324
timeGrain = 'day',
25+
timeRange,
2426
categoryDataSourceId
2527
) => {
28+
// determine interval type
29+
const timeRangeType = timeRange?.includes('this')
30+
? 'periodToDate'
31+
: 'rolling';
32+
// for month timeGrains, we need to determine whether to show 3 for a quarter or 12 for a year
33+
const timeRangeInterval = timeRange?.includes('Quarter')
34+
? 'quarter'
35+
: timeRange;
2636
let count = 7;
2737
switch (timeGrain) {
2838
case 'hour':
@@ -35,7 +45,7 @@ export const generateSampleValues = (
3545
count = 4;
3646
break;
3747
case 'month':
38-
count = 12;
48+
count = timeRangeInterval === 'quarter' ? 3 : 12;
3949
break;
4050
case 'year':
4151
count = 5;
@@ -47,7 +57,10 @@ export const generateSampleValues = (
4757

4858
if (timeDataSourceId) {
4959
return series.reduce((sampleData, { dataSourceId }) => {
50-
const now = moment().subtract(count, timeGrain);
60+
const now =
61+
timeRangeType === 'periodToDate' // handle "this" intervals like "this week"
62+
? moment().startOf(timeRangeInterval).subtract(1, timeGrain)
63+
: moment().subtract(count, timeGrain);
5164
// eslint-disable-next-line no-plusplus
5265
for (let i = 0; i < count; i++) {
5366
const nextTimeStamp = now.add(1, timeGrain).valueOf();
@@ -85,6 +98,107 @@ export const generateSampleValues = (
8598
return sampleData;
8699
};
87100

101+
/**
102+
* Generate fake, sample values for isDashboardPreview state. This is needed to preview
103+
* data grouped by the categoryDataSourceId prop
104+
* @param {Array<Object>} values a list of metrics and the dimension values to group them by
105+
*
106+
* @returns {Array} array of value objects to show in the chart
107+
*/
108+
export const generateSampleValuesForEditor = (
109+
data,
110+
categoryDataSourceId,
111+
timeDataSourceId,
112+
availableDimensions,
113+
timeGrain = 'day',
114+
timeRange
115+
) => {
116+
// determine interval type
117+
const timeRangeType = timeRange?.includes('this')
118+
? 'periodToDate'
119+
: 'rolling';
120+
// for month timeGrains, we need to determine whether to show 3 for a quarter or 12 for a year
121+
const timeRangeInterval = timeRange?.includes('Quarter')
122+
? 'quarter'
123+
: timeRange;
124+
let count = 7;
125+
switch (timeGrain) {
126+
case 'hour':
127+
count = 24;
128+
break;
129+
case 'day':
130+
count = 7;
131+
break;
132+
case 'week':
133+
count = 4;
134+
break;
135+
case 'month':
136+
count = timeRangeInterval === 'quarter' ? 3 : 12;
137+
break;
138+
case 'year':
139+
count = 5;
140+
break;
141+
default:
142+
count = 7;
143+
break;
144+
}
145+
146+
// need to remove the label if it is a categorized timeseries bar chart
147+
const metrics = data;
148+
const dimensions = [];
149+
if (availableDimensions && availableDimensions[categoryDataSourceId]) {
150+
availableDimensions[categoryDataSourceId].forEach((value) => {
151+
dimensions.push({
152+
dimension: categoryDataSourceId,
153+
value,
154+
});
155+
});
156+
}
157+
158+
if (timeDataSourceId) {
159+
const sampleData = [];
160+
metrics.forEach(({ dataSourceId }) => {
161+
const now =
162+
timeRangeType === 'periodToDate' // handle "this" intervals like "this week"
163+
? moment().startOf(timeRangeInterval).subtract(1, timeGrain)
164+
: moment().subtract(count, timeGrain);
165+
// create 4 random dataSets
166+
// eslint-disable-next-line no-plusplus
167+
for (let i = 0; i < count; i++) {
168+
const nextTimeStamp = now.add(1, timeGrain).valueOf();
169+
// include dimension category if there is one
170+
if (categoryDataSourceId) {
171+
dimensions.forEach((dimension) => {
172+
sampleData.push({
173+
[timeDataSourceId]: nextTimeStamp,
174+
[dataSourceId]: Math.random() * 100,
175+
[dimension.dimension]: dimension.value,
176+
});
177+
});
178+
} else {
179+
// otherwise we need explicit row
180+
sampleData.push({
181+
[timeDataSourceId]: nextTimeStamp,
182+
[dataSourceId]: Math.random() * 100,
183+
});
184+
}
185+
}
186+
});
187+
return sampleData;
188+
}
189+
190+
// for every dimension value, create a value object that contains sample data for each metric
191+
const valuesGeneratedByDimensions = dimensions.map((dimension) => {
192+
const value = { [dimension.dimension]: dimension.value };
193+
metrics.forEach((metric) => {
194+
value[metric.dataSourceId] = Math.random() * 100;
195+
});
196+
return value;
197+
});
198+
199+
return valuesGeneratedByDimensions;
200+
};
201+
88202
/**
89203
* Translates our raw data into a language the carbon-charts understand
90204
* @param {Array<Object>} series, the definition of the plotted series
@@ -98,12 +212,24 @@ export const generateSampleValues = (
98212
* @returns {array} of formatted values: [group: string, value: number, key: string, date: date]
99213
*/
100214
export const formatChartData = (
101-
series,
215+
seriesArg,
102216
values,
103217
categoryDataSourceId,
104218
timeDataSourceId,
105-
type
219+
type,
220+
isDashboardPreview
106221
) => {
222+
let series = seriesArg;
223+
// need to remove the label if it is a categorized timeseries bar chart in the dashboard editor
224+
if (
225+
type === BAR_CHART_TYPES.STACKED &&
226+
timeDataSourceId &&
227+
categoryDataSourceId &&
228+
isDashboardPreview
229+
) {
230+
series = series.map((item) => omit(item, 'label'));
231+
}
232+
107233
let data = values;
108234
if (!isNil(values) && !isEmpty(series)) {
109235
data = [];
@@ -196,7 +322,7 @@ export const formatChartData = (
196322
}
197323
}
198324

199-
return data;
325+
return isDashboardPreview && isEmpty(series) ? [] : data;
200326
};
201327

202328
/**
@@ -263,10 +389,24 @@ export const mapValuesToAxes = (
263389
*
264390
* @returns {Object} colors - formatted
265391
*/
266-
export const formatColors = (series, datasetNames) => {
392+
export const formatColors = (
393+
series,
394+
datasetNames,
395+
isDashboardPreview,
396+
type
397+
) => {
267398
// first set the carbon charts config defaults
268399
const colors = { scale: {} };
269400

401+
if (isDashboardPreview && type === BAR_CHART_TYPES.SIMPLE) {
402+
datasetNames.forEach((dataset) => {
403+
if (series[0].color) {
404+
colors.scale[dataset] = series[0].color;
405+
}
406+
});
407+
return colors;
408+
}
409+
270410
// if color is an array, order doesn't matter so just map as many as possible
271411
if (series[0] && Array.isArray(series[0].color)) {
272412
series[0].color.forEach((color, index) => {

0 commit comments

Comments
 (0)