Blazor Chart Control — Runtime/Dynamic Filtering Solutions with Pros and Cons

ASP.NET Team Blog
31 August 2023

Use-Case Scenario

Quite often while building a simple report a filter will be applied in markup with a hard-coded value, but what happens when that same filter needs to be dynamic? For example when representing a range of years where the requirement is the report should always show the current year at runtime. This post details ways to implement filters dynamically, including Lambda-based Expression trees in place of the hard-coded string value (an advanced, but very useful approach for complex real-world requirements).

The following example chart displays revenue across a range of years, the goal is to have the series automatically display the current year and the previous 3. This is a modified sample from one of our clients, so this is 100% from the real world.

Basic Blazor Chart Setup

The creation of the chart is straight forward (if you follow our online demos or online documentation): 

  • create the chart
  • set up the series
  • bind to the data class
  • set the filter values
<DxChart Data="@ChartData" Width="100%" Height="250px">  
	<DxChartTooltip Enabled="true" Position="RelativePosition.Outside" Context="x">  
	<div style="margin: 0.75rem">  
	<div class="font-weight-bold">FY: @x.Point.SeriesName</div>  
	<div>Month: @x.Point.Argument</div>  
	<div>Amount: @($"{(double)x.Point.Value:$0,.#K}")</div>  
	</div>  
	</DxChartTooltip>  
	<DxChartBarSeries Name=@Helpers.PrevFY3.ToString()  
		T="Data.BIData.AnnualSales"  
		TArgument="string"  
		TValue="double"  
		ArgumentField="si => si.Month"  
		ValueField="si => si.Amount"  
		SummaryMethod="Enumerable.Sum"  
		Filter="si = 2021" />  
		<DxChartBarSeries Name=@Helpers.PrevFY2.ToString()  
		T="Data.BIData.AnnualSales"  
		TArgument="string"  
		TValue="double"  
		ArgumentField="si => si.Month"  
		ValueField="si => si.Amount"  
		SummaryMethod="Enumerable.Sum"  
		Filter="si = 2022" />  
	<DxChartBarSeries Name=@Helpers.PrevFY1.ToString()  
		T="Data.BIData.AnnualSales"  
		TArgument="string"  
		TValue="double"  
		ArgumentField="si => si.Month"  
		ValueField="si => si.Amount"  
		SummaryMethod="Enumerable.Sum"  
		Filter="si = 2023" />  
	<DxChartBarSeries Name=@Helpers.CurrentFY.ToString()  
		T="Data.BIData.AnnualSales"  
		TArgument="string"  
		TValue="double"  
		ArgumentField="si => si.Month"  
		ValueField="si => si.Amount"  
		SummaryMethod="Enumerable.Sum"  
		Filter="si = 2024" />  
	<DxChartArgumentAxis SideMarginsEnabled="true" />  
	<DxChartLegend Position="RelativePosition.Outside"  
	HorizontalAlignment="HorizontalAlignment.Right" />  
</DxChart>

Challenges / Problems creating Dynamic Filters at Runtime

Catch #1

Notice that in the basic setup the Filter, ArgumentField, ValueField values are set as "strings" and the financial year (2024) is hard-coded in Razor:

ArgumentField="si => si.Month" 
Filter="si = 2024"

This simple approach raises multiple general-programming questions in the real world, though:

  • What should we do as developers to make our solution more mainteinable in the future? 
  • How to make future code extensions easier and not redo things in multiple places, like supporting a new financial year?
  • How to separate concerns/responsibilities (especially presentation/Razor from business logic in code?

Catch #2

While the Filter option looks like a string in a markup, it actually expects an expression enclosed in double quotes. For instance, аttempting to update to a static string in the code will not work:

Filter = Helpers.FinYear4
public static string FinYear4 => "FinYear = 2018";

While syntactically correct, a compile of the project will result in the following error:

Indexed.razor(78, 51): [CS1503] Argument 1: cannot convert from 'string' to 'System.Linq.Expressions.Expression<System.Func<Data.BIData.AnnualSales, bool>>' Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type

Strictly speaking, the property needs to be a 'System.Linq.Expressions.Expression' which is obviously more complex than the humble string.

Solution #1 (Simplest) - Variables Inside the Filter, NameField and Other Series Expressions

Our Autogenerated Series demo shows an approach with a single series (DxChartCommonSeries) where we specify "@((SaleInfo s) => s.Date.Year)" expression - it auto-generates multiple series for years at runtime. This way we do not need to define 4 separate series representing each year in the Razor markup, as demonstrated in the original base chart setup, and instead defines it only once in a generic manner using NameField:

<DxChartCommonSeries 
    NameField="@((SaleInfo s) => s.Date.Year)"
    ArgumentField="@(s => s.City)"
    ValueField="@(s => s.Amount)"
    SummaryMethod="Enumerable.Sum"
    SeriesType="ChartSeriesType.Bar" 
/>

Of course there are situations where it may be required to use 4 manually defined series as in the original chart setup, then we can update our Filter value in Razor to something like below:

Filter="@(si => si.CreatedOn.Year == CurrentFY)"

The `CurrentFY` is a variable declared in the code block of our Razor markup (it may hold the value 2024 or any other dynamic expression we need for our task).

Solution Pros and Cons

The biggest con is that despite the CurrentFY variable in Filter or `s.Date.Year` in NameField, the filter logic is heavily tied to the markup (the `Year` parts remain). 

The obvious pro is that both NameField and Filter are relatively short solutions that may meet the needs of simple project requirements (for instance, this approach is used regularly in the DevExpress Blazor Chart demos).

Solution #2 (Simple) - Prefilter a Data Set Instead of a Chart Control 

We can avoid at least some of the catches if we filter data before binding it to the chart control. In this case, the Filter property (and associated challenges) will not be needed at all - bypass the problem instead of solving it (that is why I called it "the simplest"). Similar advice is given in the famous book "The Visual Display of Quantitative Information" by Edward Tufte or even the application of the 'law of parsimony' aka Occam's razor.

Solution Pros and Cons

Filtering data at the data source level, certainly, has its own pros and cons. For instance, by removing filtering from the Razor and component levels, we have changed the whole architecture and will now need to think about how to make dynamic filters at the data source level, how to pass a dynamic value to our data source filtering method, etc, etc. That's beyond the scope of this post, but hopefully you understand that such engineering decisions come at a cost also.

Interestingly, this approach will not be sufficient for this example, the data is already filtered to just include the required 4 years of records, and the series are being applied as years, so the Filter is being used to separate the records into appropriate years.

Solution #3 (Advanced) - Expression Trees

Talking about Catch #2, what exactly is a 'System.Linq.Expressions.Expression<System.Func<Data.BIData.AnnualSales,bool>>' ?

  • System.Linq.Expressions.Expression - is a Lambda expression to be returned as the parameter.
  • System.Func<Data.BIData.AnnualSales, bool> - is a function created using the built in .NET delegate type, where the first parameter is an input and the second parameter is the return type.

The finished method will be something like this:

private static System.Linq.Expressions.Expression<Func<Data.BIData.AnnualSales, bool>> AnnualSalesExpression(int year)
{
 var parameterExpression = Expression.Parameter(typeof(Data.BIData.AnnualSales), "si");
 var constant = Expression.Constant(year);
 var property = Expression.Property(parameterExpression, "FinYear");
 var expression = Expression.Equal(property, constant);
 return Expression.Lambda<Func<Data.BIData.AnnualSales, bool>>(expression, parameterExpression);
} 

Method Purpose and Its Building Blocks 

The method generates an expression that represents a filtering function for `AnnualSales` based on a given year. When invoked, it returns something that is conceptually similar to

(si) => si.FinYear == year

ParameterExpression

var parameterExpression = Expression.Parameter(typeof(Data.BIData.AnnualSales), "si");

Here, is the definition of the parameter for the expression. Think of this as the `si` in the lambda

(si) => ...

Constant

var constant = Expression.Constant(year);

Encapsulate the year provided to the function into a constant expression. This will be the value to be compared against.

Property 

var property = Expression.Property(parameterExpression, "FinYear");

This extracts the `FinYear` property from `AnnualSales`. Think of it as accessing `si.FinYear`.

Expression

var expression = Expression.Equal(property, constant);

Creates an equality check. It's basically saying "Is `si.FinYear` equal to the provided year?".

Bringing It Together

Finally, the method wraps it all into a complete lambda expression:

Expression.Lambda<Func<Data.BIData.AnnualSales, bool>>(expression, parameterExpression);

This line effectively crafts an expression that represents our desired filter function

(si) => si.FinYear == year

A set of static properties allow the current financial year and previous years to be easily assigned in the markup. In this example, the values of CurrentFY and PrevFYx are calculated based on the server date and position within the financial year calendar. 

The end result is a Sales Revenue by Financial Year that automatically rolls forward at the start of each new financial period.

public static Expression<Func<Data.BIData.AnnualSales, bool>> CurrentFYFilter => AnnualSalesExpression(CurrentFY);
public static Expression<Func<Data.BIData.AnnualSales, bool>> PrevFY1Filter => AnnualSalesExpression(PrevFY1);
public static Expression<Func<Data.BIData.AnnualSales, bool>> PrevFY2Filter => AnnualSalesExpression(PrevFY2);
public static Expression<Func<Data.BIData.AnnualSales, bool>> PrevFY3Filter => AnnualSalesExpression(PrevFY3);
public static Expression<Func<Data.BIData.AnnualSales, bool>> PrevFY4Filter => AnnualSalesExpression(PrevFY4);

Now the chart will continue to update year on year.

Solution Pros and Cons

The main con is difficulty, because you must understand Expression Trees in the first place. This C# language feature is not rocket science as demonstrated above, but some developers may feel uncomfortable with it.

The biggest pro is that this approach is unlimited in terms of runtime/dynamic queries you may support, regardless of any source where filter value is coming from. It is also good for code organization, responsibility separation and general refactoring, keeping the filters out of Razor. For easier future extension and maintenance, developers of enterprise-scale business apps can isolate this filtering logic into a separate layer or a set of helper classes, which are then reused in other parts of the application, not only in charts. That is why this solution was perfect for the project, and the client was happy with the results.

Solution #4 (Advanced) - A Filter Control with a Combination of Previous Solutions

In certain scenarios, where developers delegate a lot of data shaping work to their end-users, if may be beneficial to introduce the Filter Editor control as a great option for any runtime filtering scenario. For instance, this is what the XAF Blazor Team did recently for their customers (see the picture from the Team's v23.1 What's New). Technically, the idea is to take our existing DevExtreme / JavaScript component and then convert its output filter expression to the format needed by the Blazor Chart Control.

Filter Editor - XAF Blazor, DevExpress

Solution Pros and Cons

It may be advanced to implement for some developers, but it is still a very powerful solution that in our experience many end-users loved. Thanks to the rich set of DevExpress runtime UI customization options (for developers and end-users alike), business requirement changes can be implemented without the need for redeployment.

If you would like more information or have similar client requirements, please drop a comment below detailing your needs and we will be happy to consider a more detailed explanation for future blog posts.

Your Feedback Matters

Free DevExpress Products - Get Your Copy Today

The following free DevExpress product offers remain available. Should you have any questions about the free offers below, please submit a ticket via the DevExpress Support Center at your convenience. We'll be happy to follow-up.