Skip to content

Commit 501dd4b

Browse files
authored
feat: Add customizable styles for line and bar series in data browser graph (#3131)
1 parent 0d4925c commit 501dd4b

File tree

3 files changed

+174
-2
lines changed

3 files changed

+174
-2
lines changed

src/components/GraphPanel/GraphPanel.react.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ const GraphPanel = ({
133133
aggregationType,
134134
maxDataPoints,
135135
calculatedValues,
136+
secondaryYAxisType,
136137
} = graphConfig;
137138

138139
// Limit data points for performance
@@ -154,6 +155,81 @@ const GraphPanel = ({
154155
result = processBarLineData(limitedData, xColumn, valueColumn, groupByColumn, aggregationType, calculatedValues);
155156
break;
156157
}
158+
159+
// Apply secondary Y-axis chart type to datasets on secondary axis
160+
if (result && result.datasets && secondaryYAxisType && (chartType === 'bar' || chartType === 'line')) {
161+
result.datasets = result.datasets.map(dataset => {
162+
if (dataset.yAxisID === 'y1') {
163+
return {
164+
...dataset,
165+
type: secondaryYAxisType,
166+
};
167+
}
168+
return dataset;
169+
});
170+
}
171+
172+
// Helper to compute the effective chart type for a dataset
173+
// dataset.type overrides global defaults (set by secondary Y-axis type or other means)
174+
const getEffectiveType = (dataset) => {
175+
if (dataset.type) {
176+
return dataset.type;
177+
}
178+
if (dataset.yAxisID === 'y1' && secondaryYAxisType) {
179+
return secondaryYAxisType;
180+
}
181+
return chartType;
182+
};
183+
184+
// Apply line styles to datasets (convert lineStyle to Chart.js borderDash)
185+
if (result && result.datasets) {
186+
const lineStyleToBorderDash = {
187+
solid: [],
188+
dashed: [8, 4],
189+
dotted: [2, 2],
190+
};
191+
192+
result.datasets = result.datasets.map(dataset => {
193+
const effectiveType = getEffectiveType(dataset);
194+
if (effectiveType === 'line' && dataset.lineStyle && lineStyleToBorderDash[dataset.lineStyle]) {
195+
return {
196+
...dataset,
197+
borderDash: lineStyleToBorderDash[dataset.lineStyle],
198+
};
199+
}
200+
return dataset;
201+
});
202+
}
203+
204+
// Apply bar styles to datasets
205+
if (result && result.datasets) {
206+
result.datasets = result.datasets.map(dataset => {
207+
const effectiveType = getEffectiveType(dataset);
208+
if (effectiveType === 'bar' && dataset.barStyle) {
209+
switch (dataset.barStyle) {
210+
case 'outlined':
211+
// Outlined: transparent fill with thick border
212+
return {
213+
...dataset,
214+
backgroundColor: 'transparent',
215+
borderWidth: 2,
216+
};
217+
case 'striped':
218+
// Striped: lighter fill with dashed border to indicate pattern
219+
return {
220+
...dataset,
221+
backgroundColor: dataset.backgroundColor.replace('0.8', '0.3'),
222+
borderWidth: 2,
223+
borderDash: [4, 4],
224+
};
225+
default:
226+
return dataset;
227+
}
228+
}
229+
return dataset;
230+
});
231+
}
232+
157233
return { processedData: result, validationError: null };
158234
} catch (err) {
159235
console.error('Error processing graph data:', err);

src/dashboard/Data/Browser/GraphDialog.react.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ const CALCULATED_VALUE_OPERATORS = [
4646
{ value: 'formula', label: 'Formula' },
4747
];
4848

49+
const LINE_STYLES = [
50+
{ value: 'solid', label: 'Solid' },
51+
{ value: 'dashed', label: 'Dashed' },
52+
{ value: 'dotted', label: 'Dotted' },
53+
];
54+
55+
const BAR_STYLES = [
56+
{ value: 'solid', label: 'Solid' },
57+
{ value: 'outlined', label: 'Outlined' },
58+
{ value: 'striped', label: 'Striped' },
59+
];
60+
4961
export default class GraphDialog extends React.Component {
5062
constructor(props) {
5163
super(props);
@@ -77,6 +89,7 @@ export default class GraphDialog extends React.Component {
7789
title: initialConfig.title || '',
7890
yAxisTitlePrimary: initialConfig.yAxisTitlePrimary || '',
7991
yAxisTitleSecondary: initialConfig.yAxisTitleSecondary || '',
92+
secondaryYAxisType: initialConfig.secondaryYAxisType || null,
8093
showLegend: initialConfig.showLegend !== undefined ? initialConfig.showLegend : true,
8194
showGrid: initialConfig.showGrid !== undefined ? initialConfig.showGrid : true,
8295
showAxisLabels: initialConfig.showAxisLabels !== undefined ? initialConfig.showAxisLabels : true,
@@ -402,6 +415,10 @@ export default class GraphDialog extends React.Component {
402415
const hasCircular = this.hasCircularReference(index);
403416
// Check for formula errors
404417
const formulaError = this.getFormulaError(index);
418+
// Compute effective chart type for this calculated value
419+
const effectiveType = calc.useSecondaryYAxis && this.state.secondaryYAxisType
420+
? this.state.secondaryYAxisType
421+
: this.state.chartType;
405422

406423
return (
407424
<div key={index} style={{ paddingTop: '10px', paddingLeft: '10px', paddingRight: '10px', paddingBottom: isExpanded ? '0' : '10px', borderTop: '1px solid #e3e3e3', borderLeft: '1px solid #e3e3e3', borderRight: '1px solid #e3e3e3', borderBottom: index === this.state.calculatedValues.length - 1 ? '1px solid #e3e3e3' : 'none' }}>
@@ -547,6 +564,44 @@ export default class GraphDialog extends React.Component {
547564
</div>
548565
</div>
549566
)}
567+
{effectiveType === 'line' && (
568+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', borderTop: '1px solid #e3e3e3' }}>
569+
<div style={{ display: 'flex', alignItems: 'center' }}>
570+
<Label text="Line Style" />
571+
</div>
572+
<div>
573+
<Dropdown
574+
value={calc.lineStyle || 'solid'}
575+
onChange={lineStyle => this.updateCalculatedValue(index, 'lineStyle', lineStyle)}
576+
>
577+
{LINE_STYLES.map(style => (
578+
<Option key={style.value} value={style.value}>
579+
{style.label}
580+
</Option>
581+
))}
582+
</Dropdown>
583+
</div>
584+
</div>
585+
)}
586+
{effectiveType === 'bar' && (
587+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', borderTop: '1px solid #e3e3e3' }}>
588+
<div style={{ display: 'flex', alignItems: 'center' }}>
589+
<Label text="Bar Style" />
590+
</div>
591+
<div>
592+
<Dropdown
593+
value={calc.barStyle || 'solid'}
594+
onChange={barStyle => this.updateCalculatedValue(index, 'barStyle', barStyle)}
595+
>
596+
{BAR_STYLES.map(style => (
597+
<Option key={style.value} value={style.value}>
598+
{style.label}
599+
</Option>
600+
))}
601+
</Dropdown>
602+
</div>
603+
</div>
604+
)}
550605
{hasCircular && (
551606
<div style={{ borderTop: '1px solid #e3e3e3', padding: '12px', background: '#fff3cd', color: '#856404' }}>
552607
<strong>⚠ Circular Reference Detected</strong>
@@ -686,6 +741,25 @@ export default class GraphDialog extends React.Component {
686741
} />
687742
)}
688743

744+
{(this.state.chartType === 'bar' || this.state.chartType === 'line') && (
745+
<Field label={
746+
<Label
747+
text="Secondary Y-Axis Chart Type"
748+
description="Chart type for secondary axis series"
749+
/>
750+
} input={
751+
<Dropdown
752+
value={this.state.secondaryYAxisType || ''}
753+
onChange={secondaryYAxisType => this.setState({ secondaryYAxisType: secondaryYAxisType || null })}
754+
placeHolder="Same as chart type"
755+
>
756+
<Option value="">Same as chart type</Option>
757+
<Option value="bar">Bar</Option>
758+
<Option value="line">Line</Option>
759+
</Dropdown>
760+
} />
761+
)}
762+
689763
<Field label={
690764
<Label
691765
text="Max Data Points"

src/lib/GraphDataUtils.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -720,10 +720,12 @@ export function processBarLineData(data, xColumn, valueColumn, groupByColumn, ag
720720
const sortedXLabels = sortedXKeys.map(key => xValues.get(key));
721721
const groupKeys = Object.keys(groups);
722722

723-
// Create maps for calculated value properties (operator, secondary Y axis)
723+
// Create maps for calculated value properties (operator, secondary Y axis, line style, bar style)
724724
// This handles both simple calc names and grouped calc names like "CalcName (GroupValue)"
725725
const calcValueOperatorMap = new Map();
726726
const calcValueSecondaryYAxisMap = new Map();
727+
const calcValueLineStyleMap = new Map();
728+
const calcValueBarStyleMap = new Map();
727729
if (calculatedValues && Array.isArray(calculatedValues)) {
728730
calculatedValues.forEach(calc => {
729731
if (calc.name && calc.operator) {
@@ -736,6 +738,12 @@ export function processBarLineData(data, xColumn, valueColumn, groupByColumn, ag
736738
if (calc.useSecondaryYAxis) {
737739
calcValueSecondaryYAxisMap.set(groupKey, true);
738740
}
741+
if (calc.lineStyle) {
742+
calcValueLineStyleMap.set(groupKey, calc.lineStyle);
743+
}
744+
if (calc.barStyle) {
745+
calcValueBarStyleMap.set(groupKey, calc.barStyle);
746+
}
739747
}
740748
});
741749
}
@@ -770,14 +778,28 @@ export function processBarLineData(data, xColumn, valueColumn, groupByColumn, ag
770778
}
771779
});
772780

773-
return {
781+
const dataset = {
774782
label: groupKey,
775783
data: values,
776784
backgroundColor: colors[index],
777785
borderColor: colors[index].replace('0.8', '1'),
778786
borderWidth: 1,
779787
yAxisID: calcValueSecondaryYAxisMap.get(groupKey) ? 'y1' : 'y',
780788
};
789+
790+
// Add line style if specified
791+
const lineStyle = calcValueLineStyleMap.get(groupKey);
792+
if (lineStyle) {
793+
dataset.lineStyle = lineStyle;
794+
}
795+
796+
// Add bar style if specified
797+
const barStyle = calcValueBarStyleMap.get(groupKey);
798+
if (barStyle) {
799+
dataset.barStyle = barStyle;
800+
}
801+
802+
return dataset;
781803
});
782804

783805
return {

0 commit comments

Comments
 (0)