Skip to content

Commit 8c6368b

Browse files
authored
Merge pull request #28 from BlazorExpress/features/27-combo-bar-and-line-chart
Add support for combo bar/line charts and enhance dataset handling
2 parents 77d0ffd + 02c6492 commit 8c6368b

10 files changed

Lines changed: 354 additions & 35 deletions

File tree

BlazorExpress.ChartJS.Demo.RCL/Pages/Demos/BarChart/BarChartDocumentation.razor

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,23 @@
3232
<Demo Type="typeof(BarChart_Demo_01_Examples)" Tabs="true" />
3333
</Section>
3434

35+
<Section Class="p-0" Size="HeadingSize.H4" Name="Combo bar/line" PageUrl="@pageUrl" Link="combo-bar-line">
36+
<Block>
37+
The <strong>Combo bar/line</strong> demo mixes <code>BarChartDataset</code> and <code>LineChartDataset</code> in the same <code>BarChart</code>.
38+
<br /><br />
39+
<strong>How to use:</strong>
40+
<div class="content">
41+
<ol>
42+
<li>Use the <code>BarChart</code> component as the root chart.</li>
43+
<li>Add both <code>BarChartDataset</code> and <code>LineChartDataset</code> instances to the same <code>ChartData.Datasets</code> collection.</li>
44+
<li>Configure interaction options such as <code>Mode = InteractionMode.Index</code> and <code>Intersect = false</code> for a combined tooltip experience.</li>
45+
<li>Refer to the demo code below for a working example with bar columns and a line overlay.</li>
46+
</ol>
47+
</div>
48+
</Block>
49+
<Demo Type="typeof(BarChart_Demo_09_Combo_Bar_Line)" Tabs="true" />
50+
</Section>
51+
3552
<Section Class="p-0" Size="HeadingSize.H4" Name="Horizontal bar chart" PageUrl="@pageUrl" Link="horizontal-bar-chart">
3653
<Block>
3754
The <strong>Horizontal Bar Chart</strong> displays data values as horizontal bars, making it ideal for comparing categories with long labels or when you want to emphasize comparison between values.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<BarChart @ref="barChart" Width="600" />
2+
3+
@code {
4+
private BarChart barChart = default!;
5+
private BarChartOptions barChartOptions = default!;
6+
private ChartData chartData = default!;
7+
8+
protected override void OnInitialized()
9+
{
10+
var revenueColor = ColorUtility.CategoricalTwelveColors[0].ToColor();
11+
var trendColor = ColorUtility.CategoricalTwelveColors[1].ToColor();
12+
13+
chartData = new ChartData
14+
{
15+
Labels = new List<string> { "January", "February", "March", "April", "May", "June" },
16+
Datasets = new List<IChartDataset>
17+
{
18+
new BarChartDataset
19+
{
20+
Label = "Revenue",
21+
Data = new List<double?> { 65, 59, 80, 81, 56, 55 },
22+
BackgroundColor = new List<string> { revenueColor.ToRgbaString() },
23+
BorderColor = new List<string> { revenueColor.ToRgbString() },
24+
BorderWidth = new List<double> { 0 },
25+
},
26+
new LineChartDataset
27+
{
28+
Label = "Target",
29+
Data = new List<double?> { 50, 55, 60, 70, 72, 78 },
30+
BackgroundColor = trendColor.ToRgbaString(),
31+
BorderColor = trendColor.ToRgbString(),
32+
PointRadius = new List<double> { 4 },
33+
PointHoverRadius = new List<double> { 6 },
34+
},
35+
},
36+
};
37+
38+
barChartOptions = new BarChartOptions
39+
{
40+
Responsive = true,
41+
Interaction = new Interaction { Mode = InteractionMode.Index, Intersect = false },
42+
};
43+
}
44+
45+
protected override async Task OnAfterRenderAsync(bool firstRender)
46+
{
47+
if (firstRender)
48+
await barChart.InitializeAsync(chartData, barChartOptions);
49+
50+
await base.OnAfterRenderAsync(firstRender);
51+
}
52+
}

BlazorExpress.ChartJS.Demo.RCL/Pages/Demos/LineChart/LineChartDocumentation.razor

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,23 @@
3737
<Demo Type="typeof(LineChart_Demo_01_B_Examples)" Tabs="true" />
3838
</Section>
3939

40+
<Section Class="p-0" Size="HeadingSize.H4" Name="Combo bar/line" PageUrl="@pageUrl" Link="combo-bar-line">
41+
<Block>
42+
The <strong>Combo bar/line</strong> demo mixes <code>LineChartDataset</code> and <code>BarChartDataset</code> in the same <code>LineChart</code>.
43+
<br /><br />
44+
<strong>How to use:</strong>
45+
<div class="content">
46+
<ol>
47+
<li>Use the <code>LineChart</code> component as the root chart.</li>
48+
<li>Add both <code>LineChartDataset</code> and <code>BarChartDataset</code> instances to the same <code>ChartData.Datasets</code> collection.</li>
49+
<li>Configure interaction options such as <code>Mode = InteractionMode.Index</code> and <code>Intersect = false</code> so bar and line points share tooltips cleanly.</li>
50+
<li>Refer to the demo code below for a working example with a line series and bar columns in the same chart area.</li>
51+
</ol>
52+
</div>
53+
</Block>
54+
<Demo Type="typeof(LineChart_Demo_06_Combo_Bar_Line)" Tabs="true" />
55+
</Section>
56+
4057
<Section Class="p-0" Size="HeadingSize.H4" Name="Data labels" PageUrl="@pageUrl" Link="data-labels">
4158
<Block>
4259
The <strong>Line Chart</strong> component supports data labels, allowing you to display values directly on each data point in the chart.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<LineChart @ref="lineChart" Width="600" />
2+
3+
@code {
4+
private LineChart lineChart = default!;
5+
private LineChartOptions lineChartOptions = default!;
6+
private ChartData chartData = default!;
7+
8+
protected override void OnInitialized()
9+
{
10+
var forecastColor = ColorUtility.CategoricalTwelveColors[2].ToColor();
11+
var actualColor = ColorUtility.CategoricalTwelveColors[3].ToColor();
12+
13+
chartData = new ChartData
14+
{
15+
Labels = new List<string> { "January", "February", "March", "April", "May", "June" },
16+
Datasets = new List<IChartDataset>
17+
{
18+
new LineChartDataset
19+
{
20+
Label = "Forecast",
21+
Data = new List<double?> { 45, 52, 61, 66, 73, 79 },
22+
BackgroundColor = forecastColor.ToRgbaString(),
23+
BorderColor = forecastColor.ToRgbString(),
24+
PointRadius = new List<double> { 4 },
25+
PointHoverRadius = new List<double> { 6 },
26+
},
27+
new BarChartDataset
28+
{
29+
Label = "Actual",
30+
Data = new List<double?> { 41, 49, 58, 70, 75, 82 },
31+
BackgroundColor = new List<string> { actualColor.ToRgbaString() },
32+
BorderColor = new List<string> { actualColor.ToRgbString() },
33+
BorderWidth = new List<double> { 0 },
34+
},
35+
},
36+
};
37+
38+
lineChartOptions = new LineChartOptions
39+
{
40+
Responsive = true,
41+
Interaction = new Interaction { Mode = InteractionMode.Index, Intersect = false },
42+
};
43+
}
44+
45+
protected override async Task OnAfterRenderAsync(bool firstRender)
46+
{
47+
if (firstRender)
48+
await lineChart.InitializeAsync(chartData, lineChartOptions);
49+
50+
await base.OnAfterRenderAsync(firstRender);
51+
}
52+
}

BlazorExpress.ChartJS/ChartComponents/BarChart.razor.cs

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,30 @@ public override async Task<ChartData> AddDataAsync(ChartData chartData, string d
3131
if (chartData.Datasets is null)
3232
throw new ArgumentNullException(nameof(chartData.Datasets));
3333

34+
if (chartData.Labels is null)
35+
throw new ArgumentNullException(nameof(chartData.Labels));
36+
37+
if (dataLabel is null)
38+
throw new ArgumentNullException(nameof(dataLabel));
39+
40+
if (string.IsNullOrWhiteSpace(dataLabel))
41+
throw new Exception($"{nameof(dataLabel)} cannot be empty.");
42+
3443
if (data is null)
3544
throw new ArgumentNullException(nameof(data));
3645

37-
foreach (var dataset in chartData.Datasets)
38-
if (dataset is BarChartDataset barChartDataset && barChartDataset.Label == dataLabel)
39-
if (data is BarChartDatasetData barChartDatasetData)
40-
barChartDataset.Data?.Add(barChartDatasetData.Data as double?);
46+
var chartDatasetData = BarLineChartSupport.GetSupportedDatasetData(data);
4147

42-
await JSRuntime.InvokeVoidAsync(BarChartInterop.AddDatasetData, Id, dataLabel, data);
48+
if (chartDatasetData is null)
49+
return chartData;
50+
51+
if (!chartData.Labels.Contains(dataLabel))
52+
chartData.Labels.Add(dataLabel);
53+
54+
foreach (var dataset in chartData.Datasets.Where(BarLineChartSupport.IsSupportedDataset))
55+
BarLineChartSupport.AppendDataPoint(dataset, chartDatasetData);
56+
57+
await JSRuntime.InvokeVoidAsync(BarChartInterop.AddDatasetData, Id, dataLabel, chartDatasetData);
4358

4459
return chartData;
4560
}
@@ -78,24 +93,36 @@ public override async Task<ChartData> AddDataAsync(ChartData chartData, string d
7893
if (!data.Any())
7994
throw new Exception($"{nameof(data)} cannot be empty.");
8095

81-
if (chartData.Datasets.Count != data.Count)
96+
var supportedDatasets = chartData.Datasets.Where(BarLineChartSupport.IsSupportedDataset).ToList();
97+
var supportedData = BarLineChartSupport.GetSupportedDatasetData(data);
98+
99+
if (!supportedDatasets.Any() || !supportedData.Any())
100+
return chartData;
101+
102+
if (supportedDatasets.Count != supportedData.Count)
82103
throw new InvalidDataException("The chart dataset count and the new data points count do not match.");
83104

84105
if (chartData.Labels.Contains(dataLabel))
85106
throw new Exception($"{dataLabel} already exists.");
86107

87108
chartData.Labels.Add(dataLabel);
88109

89-
foreach (var dataset in chartData.Datasets)
90-
if (dataset is BarChartDataset barChartDataset)
110+
foreach (var dataset in supportedDatasets)
111+
{
112+
var chartDataset = dataset switch
91113
{
92-
var chartDatasetData = data.FirstOrDefault(x => x is BarChartDatasetData barChartDatasetData && barChartDatasetData.DatasetLabel == barChartDataset.Label);
114+
BarChartDataset barChartDataset => barChartDataset.Label,
115+
LineChartDataset lineChartDataset => lineChartDataset.Label,
116+
_ => null,
117+
};
93118

94-
if (chartDatasetData is BarChartDatasetData barChartDatasetData)
95-
barChartDataset.Data?.Add(barChartDatasetData.Data as double?);
96-
}
119+
var chartDatasetData = supportedData.FirstOrDefault(x => x.DatasetLabel == chartDataset);
120+
121+
if (chartDatasetData is not null)
122+
BarLineChartSupport.AppendDataPoint(dataset, chartDatasetData);
123+
}
97124

98-
await JSRuntime.InvokeVoidAsync(BarChartInterop.AddDatasetsData, Id, dataLabel, data?.Select(x => (BarChartDatasetData)x));
125+
await JSRuntime.InvokeVoidAsync(BarChartInterop.AddDatasetsData, Id, dataLabel, supportedData);
99126

100127
return chartData;
101128
}
@@ -122,10 +149,10 @@ public override async Task<ChartData> AddDatasetAsync(ChartData chartData, IChar
122149
if (chartDataset is null)
123150
throw new ArgumentNullException(nameof(chartDataset));
124151

125-
if (chartDataset is BarChartDataset)
152+
if (BarLineChartSupport.IsSupportedDataset(chartDataset))
126153
{
127154
chartData.Datasets.Add(chartDataset);
128-
await JSRuntime.InvokeVoidAsync(BarChartInterop.AddDataset, Id, (BarChartDataset)chartDataset);
155+
await JSRuntime.InvokeVoidAsync(BarChartInterop.AddDataset, Id, chartDataset);
129156
}
130157

131158
return chartData;
@@ -148,7 +175,7 @@ public override async Task InitializeAsync(ChartData chartData, IChartOptions ch
148175
{
149176
if (chartData is not null && chartData.Datasets is not null)
150177
{
151-
var datasets = chartData.Datasets.OfType<BarChartDataset>();
178+
var datasets = BarLineChartSupport.GetSupportedDatasets(chartData);
152179
var data = new { chartData.Labels, Datasets = datasets };
153180
await JSRuntime.InvokeVoidAsync(BarChartInterop.Initialize, Id, GetChartType(), data, (BarChartOptions)chartOptions, plugins);
154181
}
@@ -169,7 +196,7 @@ public override async Task UpdateAsync(ChartData chartData, IChartOptions chartO
169196
{
170197
if (chartData is not null && chartData.Datasets is not null)
171198
{
172-
var datasets = chartData.Datasets.OfType<BarChartDataset>();
199+
var datasets = BarLineChartSupport.GetSupportedDatasets(chartData);
173200
var data = new { chartData.Labels, Datasets = datasets };
174201
await JSRuntime.InvokeVoidAsync(BarChartInterop.Update, Id, GetChartType(), data, (BarChartOptions)chartOptions);
175202
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
namespace BlazorExpress.ChartJS;
2+
3+
internal static class BarLineChartSupport
4+
{
5+
internal static List<object> GetSupportedDatasets(ChartData chartData)
6+
{
7+
var datasets = new List<object>();
8+
9+
if (chartData?.Datasets?.Any() ?? false)
10+
foreach (var dataset in chartData.Datasets)
11+
if (dataset is BarChartDataset barChartDataset)
12+
datasets.Add(barChartDataset);
13+
else if (dataset is LineChartDataset lineChartDataset)
14+
datasets.Add(lineChartDataset);
15+
16+
return datasets;
17+
}
18+
19+
internal static List<ChartDatasetData> GetSupportedDatasetData(IEnumerable<IChartDatasetData> data) =>
20+
data.Select(GetSupportedDatasetData)
21+
.Where(x => x is not null)
22+
.Cast<ChartDatasetData>()
23+
.ToList();
24+
25+
internal static ChartDatasetData? GetSupportedDatasetData(IChartDatasetData data) =>
26+
data switch
27+
{
28+
BarChartDatasetData barChartDatasetData => barChartDatasetData,
29+
LineChartDatasetData lineChartDatasetData => lineChartDatasetData,
30+
_ => null,
31+
};
32+
33+
internal static bool IsSupportedDataset(IChartDataset dataset) =>
34+
dataset is BarChartDataset or LineChartDataset;
35+
36+
internal static void AppendDataPoint(IChartDataset dataset, ChartDatasetData chartDatasetData)
37+
{
38+
if (!TryGetDataValue(chartDatasetData, out var value))
39+
return;
40+
41+
switch (dataset)
42+
{
43+
case BarChartDataset barChartDataset when barChartDataset.Label == chartDatasetData.DatasetLabel:
44+
barChartDataset.Data ??= new List<double?>();
45+
barChartDataset.Data.Add(value);
46+
break;
47+
case LineChartDataset lineChartDataset when lineChartDataset.Label == chartDatasetData.DatasetLabel:
48+
lineChartDataset.Data ??= new List<double?>();
49+
lineChartDataset.Data.Add(value);
50+
break;
51+
}
52+
}
53+
54+
private static bool TryGetDataValue(ChartDatasetData chartDatasetData, out double? value)
55+
{
56+
switch (chartDatasetData.Data)
57+
{
58+
case double number:
59+
value = number;
60+
return true;
61+
default:
62+
value = null;
63+
return false;
64+
}
65+
}
66+
}

0 commit comments

Comments
 (0)