diff --git a/BlazorExpress.ChartJS.Demo.RCL/Pages/Demos/BarChart/BarChartDocumentation.razor b/BlazorExpress.ChartJS.Demo.RCL/Pages/Demos/BarChart/BarChartDocumentation.razor index 460b946f..f09e3565 100644 --- a/BlazorExpress.ChartJS.Demo.RCL/Pages/Demos/BarChart/BarChartDocumentation.razor +++ b/BlazorExpress.ChartJS.Demo.RCL/Pages/Demos/BarChart/BarChartDocumentation.razor @@ -32,6 +32,23 @@ +
+ + The Combo bar/line demo mixes BarChartDataset and LineChartDataset in the same BarChart. +

+ How to use: +
+
    +
  1. Use the BarChart component as the root chart.
  2. +
  3. Add both BarChartDataset and LineChartDataset instances to the same ChartData.Datasets collection.
  4. +
  5. Configure interaction options such as Mode = InteractionMode.Index and Intersect = false for a combined tooltip experience.
  6. +
  7. Refer to the demo code below for a working example with bar columns and a line overlay.
  8. +
+
+
+ +
+
The Horizontal Bar Chart displays data values as horizontal bars, making it ideal for comparing categories with long labels or when you want to emphasize comparison between values. diff --git a/BlazorExpress.ChartJS.Demo.RCL/Pages/Demos/BarChart/BarChart_Demo_09_Combo_Bar_Line.razor b/BlazorExpress.ChartJS.Demo.RCL/Pages/Demos/BarChart/BarChart_Demo_09_Combo_Bar_Line.razor new file mode 100644 index 00000000..2c851aef --- /dev/null +++ b/BlazorExpress.ChartJS.Demo.RCL/Pages/Demos/BarChart/BarChart_Demo_09_Combo_Bar_Line.razor @@ -0,0 +1,52 @@ + + +@code { + private BarChart barChart = default!; + private BarChartOptions barChartOptions = default!; + private ChartData chartData = default!; + + protected override void OnInitialized() + { + var revenueColor = ColorUtility.CategoricalTwelveColors[0].ToColor(); + var trendColor = ColorUtility.CategoricalTwelveColors[1].ToColor(); + + chartData = new ChartData + { + Labels = new List { "January", "February", "March", "April", "May", "June" }, + Datasets = new List + { + new BarChartDataset + { + Label = "Revenue", + Data = new List { 65, 59, 80, 81, 56, 55 }, + BackgroundColor = new List { revenueColor.ToRgbaString() }, + BorderColor = new List { revenueColor.ToRgbString() }, + BorderWidth = new List { 0 }, + }, + new LineChartDataset + { + Label = "Target", + Data = new List { 50, 55, 60, 70, 72, 78 }, + BackgroundColor = trendColor.ToRgbaString(), + BorderColor = trendColor.ToRgbString(), + PointRadius = new List { 4 }, + PointHoverRadius = new List { 6 }, + }, + }, + }; + + barChartOptions = new BarChartOptions + { + Responsive = true, + Interaction = new Interaction { Mode = InteractionMode.Index, Intersect = false }, + }; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + await barChart.InitializeAsync(chartData, barChartOptions); + + await base.OnAfterRenderAsync(firstRender); + } +} \ No newline at end of file diff --git a/BlazorExpress.ChartJS.Demo.RCL/Pages/Demos/LineChart/LineChartDocumentation.razor b/BlazorExpress.ChartJS.Demo.RCL/Pages/Demos/LineChart/LineChartDocumentation.razor index 2484f93a..86edb6e2 100644 --- a/BlazorExpress.ChartJS.Demo.RCL/Pages/Demos/LineChart/LineChartDocumentation.razor +++ b/BlazorExpress.ChartJS.Demo.RCL/Pages/Demos/LineChart/LineChartDocumentation.razor @@ -37,6 +37,23 @@
+
+ + The Combo bar/line demo mixes LineChartDataset and BarChartDataset in the same LineChart. +

+ How to use: +
+
    +
  1. Use the LineChart component as the root chart.
  2. +
  3. Add both LineChartDataset and BarChartDataset instances to the same ChartData.Datasets collection.
  4. +
  5. Configure interaction options such as Mode = InteractionMode.Index and Intersect = false so bar and line points share tooltips cleanly.
  6. +
  7. Refer to the demo code below for a working example with a line series and bar columns in the same chart area.
  8. +
+
+
+ +
+
The Line Chart component supports data labels, allowing you to display values directly on each data point in the chart. diff --git a/BlazorExpress.ChartJS.Demo.RCL/Pages/Demos/LineChart/LineChart_Demo_06_Combo_Bar_Line.razor b/BlazorExpress.ChartJS.Demo.RCL/Pages/Demos/LineChart/LineChart_Demo_06_Combo_Bar_Line.razor new file mode 100644 index 00000000..e5302018 --- /dev/null +++ b/BlazorExpress.ChartJS.Demo.RCL/Pages/Demos/LineChart/LineChart_Demo_06_Combo_Bar_Line.razor @@ -0,0 +1,52 @@ + + +@code { + private LineChart lineChart = default!; + private LineChartOptions lineChartOptions = default!; + private ChartData chartData = default!; + + protected override void OnInitialized() + { + var forecastColor = ColorUtility.CategoricalTwelveColors[2].ToColor(); + var actualColor = ColorUtility.CategoricalTwelveColors[3].ToColor(); + + chartData = new ChartData + { + Labels = new List { "January", "February", "March", "April", "May", "June" }, + Datasets = new List + { + new LineChartDataset + { + Label = "Forecast", + Data = new List { 45, 52, 61, 66, 73, 79 }, + BackgroundColor = forecastColor.ToRgbaString(), + BorderColor = forecastColor.ToRgbString(), + PointRadius = new List { 4 }, + PointHoverRadius = new List { 6 }, + }, + new BarChartDataset + { + Label = "Actual", + Data = new List { 41, 49, 58, 70, 75, 82 }, + BackgroundColor = new List { actualColor.ToRgbaString() }, + BorderColor = new List { actualColor.ToRgbString() }, + BorderWidth = new List { 0 }, + }, + }, + }; + + lineChartOptions = new LineChartOptions + { + Responsive = true, + Interaction = new Interaction { Mode = InteractionMode.Index, Intersect = false }, + }; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + await lineChart.InitializeAsync(chartData, lineChartOptions); + + await base.OnAfterRenderAsync(firstRender); + } +} \ No newline at end of file diff --git a/BlazorExpress.ChartJS/ChartComponents/BarChart.razor.cs b/BlazorExpress.ChartJS/ChartComponents/BarChart.razor.cs index 928a73db..13118e35 100644 --- a/BlazorExpress.ChartJS/ChartComponents/BarChart.razor.cs +++ b/BlazorExpress.ChartJS/ChartComponents/BarChart.razor.cs @@ -31,15 +31,30 @@ public override async Task AddDataAsync(ChartData chartData, string d if (chartData.Datasets is null) throw new ArgumentNullException(nameof(chartData.Datasets)); + if (chartData.Labels is null) + throw new ArgumentNullException(nameof(chartData.Labels)); + + if (dataLabel is null) + throw new ArgumentNullException(nameof(dataLabel)); + + if (string.IsNullOrWhiteSpace(dataLabel)) + throw new Exception($"{nameof(dataLabel)} cannot be empty."); + if (data is null) throw new ArgumentNullException(nameof(data)); - foreach (var dataset in chartData.Datasets) - if (dataset is BarChartDataset barChartDataset && barChartDataset.Label == dataLabel) - if (data is BarChartDatasetData barChartDatasetData) - barChartDataset.Data?.Add(barChartDatasetData.Data as double?); + var chartDatasetData = BarLineChartSupport.GetSupportedDatasetData(data); - await JSRuntime.InvokeVoidAsync(BarChartInterop.AddDatasetData, Id, dataLabel, data); + if (chartDatasetData is null) + return chartData; + + if (!chartData.Labels.Contains(dataLabel)) + chartData.Labels.Add(dataLabel); + + foreach (var dataset in chartData.Datasets.Where(BarLineChartSupport.IsSupportedDataset)) + BarLineChartSupport.AppendDataPoint(dataset, chartDatasetData); + + await JSRuntime.InvokeVoidAsync(BarChartInterop.AddDatasetData, Id, dataLabel, chartDatasetData); return chartData; } @@ -78,7 +93,13 @@ public override async Task AddDataAsync(ChartData chartData, string d if (!data.Any()) throw new Exception($"{nameof(data)} cannot be empty."); - if (chartData.Datasets.Count != data.Count) + var supportedDatasets = chartData.Datasets.Where(BarLineChartSupport.IsSupportedDataset).ToList(); + var supportedData = BarLineChartSupport.GetSupportedDatasetData(data); + + if (!supportedDatasets.Any() || !supportedData.Any()) + return chartData; + + if (supportedDatasets.Count != supportedData.Count) throw new InvalidDataException("The chart dataset count and the new data points count do not match."); if (chartData.Labels.Contains(dataLabel)) @@ -86,16 +107,22 @@ public override async Task AddDataAsync(ChartData chartData, string d chartData.Labels.Add(dataLabel); - foreach (var dataset in chartData.Datasets) - if (dataset is BarChartDataset barChartDataset) + foreach (var dataset in supportedDatasets) + { + var chartDataset = dataset switch { - var chartDatasetData = data.FirstOrDefault(x => x is BarChartDatasetData barChartDatasetData && barChartDatasetData.DatasetLabel == barChartDataset.Label); + BarChartDataset barChartDataset => barChartDataset.Label, + LineChartDataset lineChartDataset => lineChartDataset.Label, + _ => null, + }; - if (chartDatasetData is BarChartDatasetData barChartDatasetData) - barChartDataset.Data?.Add(barChartDatasetData.Data as double?); - } + var chartDatasetData = supportedData.FirstOrDefault(x => x.DatasetLabel == chartDataset); + + if (chartDatasetData is not null) + BarLineChartSupport.AppendDataPoint(dataset, chartDatasetData); + } - await JSRuntime.InvokeVoidAsync(BarChartInterop.AddDatasetsData, Id, dataLabel, data?.Select(x => (BarChartDatasetData)x)); + await JSRuntime.InvokeVoidAsync(BarChartInterop.AddDatasetsData, Id, dataLabel, supportedData); return chartData; } @@ -122,10 +149,10 @@ public override async Task AddDatasetAsync(ChartData chartData, IChar if (chartDataset is null) throw new ArgumentNullException(nameof(chartDataset)); - if (chartDataset is BarChartDataset) + if (BarLineChartSupport.IsSupportedDataset(chartDataset)) { chartData.Datasets.Add(chartDataset); - await JSRuntime.InvokeVoidAsync(BarChartInterop.AddDataset, Id, (BarChartDataset)chartDataset); + await JSRuntime.InvokeVoidAsync(BarChartInterop.AddDataset, Id, chartDataset); } return chartData; @@ -148,7 +175,7 @@ public override async Task InitializeAsync(ChartData chartData, IChartOptions ch { if (chartData is not null && chartData.Datasets is not null) { - var datasets = chartData.Datasets.OfType(); + var datasets = BarLineChartSupport.GetSupportedDatasets(chartData); var data = new { chartData.Labels, Datasets = datasets }; await JSRuntime.InvokeVoidAsync(BarChartInterop.Initialize, Id, GetChartType(), data, (BarChartOptions)chartOptions, plugins); } @@ -169,7 +196,7 @@ public override async Task UpdateAsync(ChartData chartData, IChartOptions chartO { if (chartData is not null && chartData.Datasets is not null) { - var datasets = chartData.Datasets.OfType(); + var datasets = BarLineChartSupport.GetSupportedDatasets(chartData); var data = new { chartData.Labels, Datasets = datasets }; await JSRuntime.InvokeVoidAsync(BarChartInterop.Update, Id, GetChartType(), data, (BarChartOptions)chartOptions); } diff --git a/BlazorExpress.ChartJS/ChartComponents/Core/BarLineChartSupport.cs b/BlazorExpress.ChartJS/ChartComponents/Core/BarLineChartSupport.cs new file mode 100644 index 00000000..73fa434e --- /dev/null +++ b/BlazorExpress.ChartJS/ChartComponents/Core/BarLineChartSupport.cs @@ -0,0 +1,66 @@ +namespace BlazorExpress.ChartJS; + +internal static class BarLineChartSupport +{ + internal static List GetSupportedDatasets(ChartData chartData) + { + var datasets = new List(); + + if (chartData?.Datasets?.Any() ?? false) + foreach (var dataset in chartData.Datasets) + if (dataset is BarChartDataset barChartDataset) + datasets.Add(barChartDataset); + else if (dataset is LineChartDataset lineChartDataset) + datasets.Add(lineChartDataset); + + return datasets; + } + + internal static List GetSupportedDatasetData(IEnumerable data) => + data.Select(GetSupportedDatasetData) + .Where(x => x is not null) + .Cast() + .ToList(); + + internal static ChartDatasetData? GetSupportedDatasetData(IChartDatasetData data) => + data switch + { + BarChartDatasetData barChartDatasetData => barChartDatasetData, + LineChartDatasetData lineChartDatasetData => lineChartDatasetData, + _ => null, + }; + + internal static bool IsSupportedDataset(IChartDataset dataset) => + dataset is BarChartDataset or LineChartDataset; + + internal static void AppendDataPoint(IChartDataset dataset, ChartDatasetData chartDatasetData) + { + if (!TryGetDataValue(chartDatasetData, out var value)) + return; + + switch (dataset) + { + case BarChartDataset barChartDataset when barChartDataset.Label == chartDatasetData.DatasetLabel: + barChartDataset.Data ??= new List(); + barChartDataset.Data.Add(value); + break; + case LineChartDataset lineChartDataset when lineChartDataset.Label == chartDatasetData.DatasetLabel: + lineChartDataset.Data ??= new List(); + lineChartDataset.Data.Add(value); + break; + } + } + + private static bool TryGetDataValue(ChartDatasetData chartDatasetData, out double? value) + { + switch (chartDatasetData.Data) + { + case double number: + value = number; + return true; + default: + value = null; + return false; + } + } +} \ No newline at end of file diff --git a/BlazorExpress.ChartJS/ChartComponents/LineChart.razor.cs b/BlazorExpress.ChartJS/ChartComponents/LineChart.razor.cs index 33baa22f..8c9c7f0d 100644 --- a/BlazorExpress.ChartJS/ChartComponents/LineChart.razor.cs +++ b/BlazorExpress.ChartJS/ChartComponents/LineChart.razor.cs @@ -31,15 +31,30 @@ public override async Task AddDataAsync(ChartData chartData, string d if (chartData.Datasets is null) throw new ArgumentNullException(nameof(chartData.Datasets)); + if (chartData.Labels is null) + throw new ArgumentNullException(nameof(chartData.Labels)); + + if (dataLabel is null) + throw new ArgumentNullException(nameof(dataLabel)); + + if (string.IsNullOrWhiteSpace(dataLabel)) + throw new Exception($"{nameof(dataLabel)} cannot be empty."); + if (data is null) throw new ArgumentNullException(nameof(data)); - foreach (var dataset in chartData.Datasets) - if (dataset is LineChartDataset lineChartDataset && lineChartDataset.Label == dataLabel) - if (data is LineChartDatasetData lineChartDatasetData) - lineChartDataset.Data?.Add(lineChartDatasetData.Data as double?); + var chartDatasetData = BarLineChartSupport.GetSupportedDatasetData(data); - await JSRuntime.InvokeVoidAsync(LineChartInterop.AddDatasetData, Id, dataLabel, data); + if (chartDatasetData is null) + return chartData; + + if (!chartData.Labels.Contains(dataLabel)) + chartData.Labels.Add(dataLabel); + + foreach (var dataset in chartData.Datasets.Where(BarLineChartSupport.IsSupportedDataset)) + BarLineChartSupport.AppendDataPoint(dataset, chartDatasetData); + + await JSRuntime.InvokeVoidAsync(LineChartInterop.AddDatasetData, Id, dataLabel, chartDatasetData); return chartData; } @@ -78,7 +93,13 @@ public override async Task AddDataAsync(ChartData chartData, string d if (!data.Any()) throw new ArgumentException($"{nameof(data)} cannot be empty.", nameof(data)); - if (chartData.Datasets.Count != data.Count) + var supportedDatasets = chartData.Datasets.Where(BarLineChartSupport.IsSupportedDataset).ToList(); + var supportedData = BarLineChartSupport.GetSupportedDatasetData(data); + + if (!supportedDatasets.Any() || !supportedData.Any()) + return chartData; + + if (supportedDatasets.Count != supportedData.Count) throw new InvalidDataException("The chart dataset count and the new data points count do not match."); if (chartData.Labels.Contains(dataLabel)) @@ -86,16 +107,22 @@ public override async Task AddDataAsync(ChartData chartData, string d chartData.Labels.Add(dataLabel); - foreach (var dataset in chartData.Datasets) - if (dataset is LineChartDataset lineChartDataset) + foreach (var dataset in supportedDatasets) + { + var chartDataset = dataset switch { - var chartDatasetData = data.FirstOrDefault(x => x is LineChartDatasetData lineChartDatasetData && lineChartDatasetData.DatasetLabel == lineChartDataset.Label); + BarChartDataset barChartDataset => barChartDataset.Label, + LineChartDataset lineChartDataset => lineChartDataset.Label, + _ => null, + }; - if (chartDatasetData is LineChartDatasetData lineChartDatasetData) - lineChartDataset.Data?.Add(lineChartDatasetData.Data as double?); - } + var chartDatasetData = supportedData.FirstOrDefault(x => x.DatasetLabel == chartDataset); + + if (chartDatasetData is not null) + BarLineChartSupport.AppendDataPoint(dataset, chartDatasetData); + } - await JSRuntime.InvokeVoidAsync(LineChartInterop.AddDatasetsData, Id, dataLabel, data?.Select(x => (LineChartDatasetData)x)); + await JSRuntime.InvokeVoidAsync(LineChartInterop.AddDatasetsData, Id, dataLabel, supportedData); return chartData; } @@ -122,10 +149,10 @@ public override async Task AddDatasetAsync(ChartData chartData, IChar if (chartDataset is null) throw new ArgumentNullException(nameof(chartDataset)); - if (chartDataset is LineChartDataset) + if (BarLineChartSupport.IsSupportedDataset(chartDataset)) { chartData.Datasets.Add(chartDataset); - await JSRuntime.InvokeVoidAsync(LineChartInterop.AddDataset, Id, (LineChartDataset)chartDataset); + await JSRuntime.InvokeVoidAsync(LineChartInterop.AddDataset, Id, chartDataset); } return chartData; @@ -155,7 +182,7 @@ public override async Task InitializeAsync(ChartData chartData, IChartOptions ch if (chartOptions is null) throw new ArgumentNullException(nameof(chartOptions)); - var datasets = chartData.Datasets.OfType(); + var datasets = BarLineChartSupport.GetSupportedDatasets(chartData); var data = new { chartData.Labels, Datasets = datasets }; await JSRuntime.InvokeVoidAsync(LineChartInterop.Initialize, Id, GetChartType(), data, (LineChartOptions)chartOptions, plugins); } @@ -182,7 +209,7 @@ public override async Task UpdateAsync(ChartData chartData, IChartOptions chartO if (chartOptions is null) throw new ArgumentNullException(nameof(chartOptions)); - var datasets = chartData.Datasets.OfType(); + var datasets = BarLineChartSupport.GetSupportedDatasets(chartData); var data = new { chartData.Labels, Datasets = datasets }; await JSRuntime.InvokeVoidAsync(LineChartInterop.Update, Id, GetChartType(), data, (LineChartOptions)chartOptions); } diff --git a/BlazorExpress.ChartJS/Models/ChartDataset/BarChart/BarChartDataset.cs b/BlazorExpress.ChartJS/Models/ChartDataset/BarChart/BarChartDataset.cs index d3664167..15d5ab80 100644 --- a/BlazorExpress.ChartJS/Models/ChartDataset/BarChart/BarChartDataset.cs +++ b/BlazorExpress.ChartJS/Models/ChartDataset/BarChart/BarChartDataset.cs @@ -12,6 +12,15 @@ /// public class BarChartDataset : ChartDataset { + #region Constructors + + public BarChartDataset() + { + Type = "bar"; + } + + #endregion + #region Properties, Indexers /// diff --git a/BlazorExpress.ChartJS/Models/ChartDataset/LineChart/LineChartDataset.cs b/BlazorExpress.ChartJS/Models/ChartDataset/LineChart/LineChartDataset.cs index d9641dfb..3959c700 100644 --- a/BlazorExpress.ChartJS/Models/ChartDataset/LineChart/LineChartDataset.cs +++ b/BlazorExpress.ChartJS/Models/ChartDataset/LineChart/LineChartDataset.cs @@ -12,6 +12,15 @@ namespace BlazorExpress.ChartJS; /// public class LineChartDataset : ChartDataset { + #region Constructors + + public LineChartDataset() + { + Type = "line"; + } + + #endregion + #region Methods /// diff --git a/README.md b/README.md index c61ab50a..d8aa06d9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

An open-source, production-ready Blazor charts component library built on the Blazor and Chart.js JavaScript library.
- Getting Started » + Getting Started �

@@ -49,6 +49,49 @@ Get started any way you want | Charts: Radar chart | [Demos](https://chartjs.blazorexpress.com/demos/radar-chart) | [Docs](https://chartjs.blazorexpress.com/docs/radar-chart) | | Charts: Scatter chart | [Demos](https://chartjs.blazorexpress.com/demos/scatter-chart) | [Docs](https://chartjs.blazorexpress.com/docs/scatter-chart) | +## Combo bar/line + +`BarChart` and `LineChart` both support mixed bar/line datasets. Add `BarChartDataset` and `LineChartDataset` instances to the same `ChartData.Datasets` collection and initialize either chart component as the root chart. + +```razor + + +@code { + private BarChart chart = default!; + private readonly BarChartOptions options = new() + { + Responsive = true, + Interaction = new Interaction { Mode = InteractionMode.Index, Intersect = false }, + }; + + private readonly ChartData chartData = new() + { + Labels = new List { "January", "February", "March" }, + Datasets = new List + { + new BarChartDataset + { + Label = "Revenue", + Data = new List { 65, 59, 80 }, + }, + new LineChartDataset + { + Label = "Target", + Data = new List { 50, 55, 60 }, + }, + }, + }; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + await chart.InitializeAsync(chartData, options); + } +} +``` + +Use `LineChart` the same way when you want the line configuration to be the root chart type. + More components coming... ## Creators