SfStockChart - Period and Range Selector Breaking on Observable Clear() and Add().

Good morning Team,


I am going to provide the source code on a really simple and elegant solution that works well upon the initial load. My gut tells me that I don't fully understand enough about component lifecycles to solve this problem. Adding a simple List<Datatype>() and binding it at run-time is very easy, but a more interesting problem is when you want to load it dynamically at-will. The issue occurs when I decide to change the dataset to another ticker symbol, for example, from "AAPL" to "NVDA". The stock is able to render the Candlesticks and Volume of the new dataset, but the Period Selector and Range Selector, which are somehow working in tandem, no longer work.

Here is my Razor Web Component:

@page "/stockchart"

@rendermode InteractiveServer

@inject IPolygonApiService PolygonApiService


<div style="margin-bottom: 25px;">

    <!-- Input for Ticker Symbol -->

    <label for="ticker">Ticker Symbol: </label>

    <input type="text" id="ticker" @bind="TickerSymbol" placeholder="Enter ticker symbol" />


    <!-- Button to Fetch Data -->

    <button @onclick="FetchStockData">Fetch Stock Data</button>

</div>


<SfStockChart>

    <!-- Enable crosshair and tooltip -->

    <StockChartCrosshairSettings Enable="true" />

    <StockChartTooltipSettings

        TooltipPosition="TooltipPosition.Nearest"

        Enable="true"

        Format="High : <b>${point.high}</b><br/>

                Low : <b>${point.low}</b><br/>

                Open : <b>${point.open}</b><br/>

                Close : <b>${point.close}</b><br/>

                Volume : <b>${point.volume}</b>" />

    <!-- Define Periods -->

    <StockChartPeriods>

        <StockChartPeriod Text="All" />

        <StockChartPeriod IntervalType=RangeIntervalType.Years Interval="1" Text="1Y" />

        <StockChartPeriod IntervalType=RangeIntervalType.Months Interval="6" Text="6M" />

        <StockChartPeriod IntervalType=RangeIntervalType.Months Interval="3" Text="3M" />

        <StockChartPeriod IntervalType=RangeIntervalType.Months Interval="1" Text="1M" />

    </StockChartPeriods>

    <!-- Candlestick chart series -->

    <StockChartSeriesCollection>

        <StockChartSeries

        DataSource="@StockDetails"

        Type="ChartSeriesType.Candle"

        EnableSolidCandles="true"

        XName="Date"

        Open="Open"

        High="High"

        Low="Low"

        Close="Close"

        Volume="Volume"

        YAxisName="primaryYAxis" />

        <StockChartSeries

        DataSource="@StockDetails"

        Type="ChartSeriesType.Column"

        XName="Date"

        YName="Volume"

        YAxisName="secondaryYAxis" />

    </StockChartSeriesCollection>

    <StockChartAxes>

        <StockChartAxis Name="primaryYAxis" LabelPosition="AxisPosition.Inside" RowIndex="1">

            <StockChartAxisMajorGridLines Width="0" />

            <StockChartAxisMinorGridLines Width="0" />

        </StockChartAxis>

        <StockChartAxis Name="secondaryYAxis" LabelPosition="AxisPosition.Inside" RowIndex="0">

            <StockChartAxisMajorGridLines Width="0" />

            <StockChartAxisMinorGridLines Width="0" />

        </StockChartAxis>

    </StockChartAxes>

    <StockChartRows>

        <StockChartRow Height="20%"></StockChartRow>

        <StockChartRow Height="80%"></StockChartRow>

    </StockChartRows>

    <StockChartPrimaryXAxis ValueType="Syncfusion.Blazor.Charts.ValueType.DateTimeCategory">

        <StockChartAxisMajorGridLines Width="0" />

        <StockChartAxisMinorGridLines Width="0" />

    </StockChartPrimaryXAxis>

</SfStockChart>


@code {

    // Input properties for user selection

    public string TickerSymbol { get; set; } = "AAPL";

    public string StartDate { get; set; } = DateTime.Now.AddYears(-2).ToString("yyyy-MM-dd");

    public string EndDate { get; set; } = DateTime.Now.ToString("yyyy-MM-dd");


    public class StockChartData

    {

        public DateTime Date { get; set; }

        public Double Open { get; set; }

        public Double High { get; set; }

        public Double Low { get; set; }

        public Double Close { get; set; }

        public Double Volume { get; set; }

    }


    public ObservableCollection<StockChartData> StockDetails { get; set; } = new ObservableCollection<StockChartData>();


    protected override async Task OnInitializedAsync()

    {

        await FetchStockData();

    }


    protected async Task FetchStockData()

    {

        var aggregateBars = await PolygonApiService.GetAggregateBars(TickerSymbol, StartDate, EndDate);


        StockDetails.Clear();


        foreach (var bar in aggregateBars)

        {

            StockDetails.Add(new StockChartData

                {

                    Date = DateTimeOffset.FromUnixTimeMilliseconds(bar.Timestamp).DateTime,

                    Open = bar.OpenPrice,

                    Low = bar.LowPrice,

                    Close = bar.ClosePrice,

                    High = bar.HighPrice,

                    Volume = bar.Volume

                });

        }

    }

}



I might add that I am using Dependency Injection whereby I've registered the IPolygonApiService (which is sitting in another Project Solution) inside of my Program.cs, along with registering an API Key to something called PolygonSettings to be used within the PolygonApiService implementation which comes from my appsettings.Development.json. For any of you deep divers, or those that are really curious how I've implemented this, please respond and I'll do my best to walk you through it.


Otherwise my general question is: Why does the Razor Component work perfectly fine upon an initial loading (presumably the Period/Range Selectors, which are working in tandem), but after swapping out the DataSource (via Clear() and Add() methods), that this stops working? 


What might I do to get back on track? This solution is very close to being a fully-functioning stock chart, similar to what you would see on other major platforms.


8 Replies

DG Durga Gopalakrishnan Syncfusion Team December 9, 2024 01:16 PM UTC

Hi Blair,


Greetings from Syncfusion.


We have ensured your reported scenario by changing the periods in period selector after updating the stock chart data. Stock chart periods are changed and range navigator is rendered as expected. We have attached the tested sample and screenshot for your reference.


Period Change with Initial Data



Period Change with New Data


Sample : https://www.syncfusion.com/downloads/support/directtrac/general/ze/StockChartPeriod-8430467.zip


If you are still facing problem, please try to replicate the problem in above sample so that it will be helpful to validate this case further and assist you better. Kindly revert us if you have any concerns.


Regards,

Durga Gopalakrishnan.



BL Blair December 9, 2024 02:46 PM UTC

Good morning Durga,


Firstly, thanks for getting back so quickly to my concern. The problem is that with simple examples, Clear() and Add() are both operating just fine, even with the Observable pattern. The SfStockChart that I posted is slightly more complex as it includes a Candlestick (ChartSeriesType.Candle) type and Volume (ChartSeriesType.Column) type in one single chart, and it also has an API attached to it that is bringing in hundreds of data points of real stock market data. Another nuance that is included in my SfStockChart that is not included in yours is the instance of <StockChartPrimaryXAxis ValueType="Syncfusion.Blazor.Charts.ValueType.DateTimeCategory"> which lives outside of the <StockMarketAxes> to contain the Candle and Column data types. Anyone who looks at Stock Market data regularly would agree that this functionality should be in-place. The reason for this is because otherwise large gaps are plotted onto the SfStockChart without this setting turned on (because the stock market is only open during business days, excluding holidays where it is closed), and with the DateTimeCategory feature, these invalid gaps in the Date X-Axis are no longer present, which is extremely convenient to have.


One more thing to point out is that upon the first initial load, the Period and Range Selectors are working just fine, it's only once the data has changed significantly that the SfStockChart seems to have difficulty re-processing and aligning these components together with the newly plotted chart with data.


Can you consider using my Source Code as a point for validation on the next run?


Thank you!


Reference:

Image_2019_1733751816646



BL Blair December 9, 2024 07:22 PM UTC

Durga,


I went ahead and modified your example to include the necessities that we need to test and understand from my example.


Here is the Source Code for the Razor Web Component:


@page "/"

@rendermode InteractiveServer

@using Syncfusion.Blazor.Charts

@using Syncfusion.Blazor.Buttons

@using System.Collections.ObjectModel;

<SfButton Content="Change Data" @onclick="ChangeData" IsPrimary="true" CssClass="e-flat"></SfButton>


<SfStockChart>

    <!-- Crosshair and Tooltip Settings -->

    <StockChartCrosshairSettings Enable="true" />

    <StockChartTooltipSettings TooltipPosition="TooltipPosition.Nearest"

                               Enable="true"

                               Format="High : <b>${point.high}</b><br/>

            Low : <b>${point.low}</b><br/>

            Open : <b>${point.open}</b><br/>

            Close : <b>${point.close}</b><br/>

            Volume : <b>${point.volume}</b>" />

    <!-- Candlestick chart series -->

    <StockChartSeriesCollection>

        <StockChartSeries

        DataSource="@StockDetails"

        Type="ChartSeriesType.Candle"

        EnableSolidCandles="true"

        XName="Date"

        Open="Open"

        High="High"

        Low="Low"

        Close="Close"

        Volume="Volume"

        YAxisName="primaryYAxis" />

        <StockChartSeries

        DataSource="@StockDetails"

        Type="ChartSeriesType.Column"

        XName="Date"

        YName="Volume"

        YAxisName="secondaryYAxis" />

    </StockChartSeriesCollection>

    <StockChartAxes>

        <StockChartAxis Name="primaryYAxis" LabelPosition="AxisPosition.Inside" RowIndex="1">

            <StockChartAxisMajorGridLines Width="0" />

            <StockChartAxisMinorGridLines Width="0" />

        </StockChartAxis>

        <StockChartAxis Name="secondaryYAxis" LabelPosition="AxisPosition.Inside" RowIndex="0">

            <StockChartAxisMajorGridLines Width="0" />

            <StockChartAxisMinorGridLines Width="0" />

        </StockChartAxis>

    </StockChartAxes>

    <StockChartRows>

        <StockChartRow Height="20%"></StockChartRow>

        <StockChartRow Height="80%"></StockChartRow>

    </StockChartRows>

    <StockChartPrimaryXAxis ValueType="Syncfusion.Blazor.Charts.ValueType.DateTimeCategory" EdgeLabelPlacement="EdgeLabelPlacement.Shift">

        <StockChartAxisMajorGridLines Width="0" />

        <StockChartAxisMinorGridLines Width="0" />

    </StockChartPrimaryXAxis>

</SfStockChart>

@code {

    private Random randomNum = new Random();

    private double high = 112 , low = 106, open = 109, close = 106, volume = 1012634864;

    public async Task ChangeData()

    {

        StockDetails.Clear();

        for (int i = 1; i <= 8; i++)

        {

            if (randomNum.NextDouble() > 0.5)

            {

                high += randomNum.NextDouble();

                low += randomNum.NextDouble();

                open += randomNum.NextDouble();

                close += randomNum.NextDouble();

                volume += randomNum.NextDouble();


            }

            else

            {

                high -= randomNum.NextDouble();

                low -= randomNum.NextDouble();

                open -= randomNum.NextDouble();

                close -= randomNum.NextDouble();

                volume -= randomNum.NextDouble();

            }

            StockDetails.Add(new StockChartData

                {

                    Date = new DateTime(2020, 1, 1).AddMonths(i + 1),

                    High = high,

                    Low = low,

                    Open = open,

                    Close = close,

                    Volume = volume


                });

        }

        //stockObj.UpdateStockChart();

    }

    public class StockChartData

    {

        public DateTime Date { get; set; }

        public Double Open { get; set; }

        public Double Low { get; set; }

        public Double Close { get; set; }

        public Double High { get; set; }

        public Double Volume { get; set; }

    }


    public ObservableCollection<StockChartData> StockDetails = new ObservableCollection<StockChartData>

    {

        new StockChartData { Date = new DateTime(2012, 04, 02), Open = 85.9757, High = 90.6657, Low = 85.7685, Close = 90.5257, Volume = 660187068},

        new StockChartData { Date = new DateTime(2012, 04, 09), Open = 89.4471, High = 92, Low = 86.2157, Close = 86.4614, Volume = 912634864},

        new StockChartData { Date = new DateTime(2012, 04, 16), Open = 87.1514, High = 88.6071, Low = 81.4885, Close = 81.8543, Volume = 1221746066},

        new StockChartData { Date = new DateTime(2012, 04, 23), Open = 81.5157, High = 88.2857, Low = 79.2857, Close = 86.1428, Volume = 965935749},

        new StockChartData { Date = new DateTime(2012, 04, 30), Open = 85.4, High = 85.4857, Low = 80.7385, Close = 80.75, Volume = 615249365},

        new StockChartData { Date = new DateTime(2012, 05, 07), Open = 80.2143, High = 82.2685, Low = 79.8185, Close = 80.9585, Volume = 541742692},

        new StockChartData { Date = new DateTime(2012, 05, 14), Open = 80.3671, High = 81.0728, Low = 74.5971, Close = 75.7685, Volume = 708126233},

        new StockChartData { Date = new DateTime(2012, 05, 21), Open = 76.3571, High = 82.3571, Low = 76.2928, Close = 80.3271, Volume = 682076215},

        new StockChartData { Date = new DateTime(2012, 05, 28), Open = 81.5571, High = 83.0714, Low = 80.0743, Close = 80.1414, Volume = 480059584},

    };


    protected override async Task OnInitializedAsync()

    {

        await ChangeData();

    }

}


If you test this, even without a complex data source, you will see that the Period and Range Selectors are breaking.



BL Blair December 10, 2024 05:44 AM UTC

Team,


I've got some really exciting updates, I recently learned about the SfContextMenu and how this can apply to an SfStockChart. This is extremely powerful and I am very excited to share my simple source code with you:


@page "/stockchart"

@rendermode InteractiveServer

@inject IPolygonApiService PolygonApiService


<div style="margin-bottom: 25px;">

    <!-- Input for Ticker Symbol -->

    <label for="ticker">Ticker Symbol: </label>

    <input type="text" id="ticker" @bind="TickerSymbol" placeholder="Enter ticker symbol" />


    <!-- Button to Fetch Data -->

    <button @onclick="FetchStockData">Fetch Stock Data</button>


    <strong>Open:</strong> @StockDetails.LastOrDefault()?.Open

    <strong>High:</strong> @StockDetails.LastOrDefault()?.High

    <strong>Low:</strong> @StockDetails.LastOrDefault()?.Low

    <strong>Close:</strong> @StockDetails.LastOrDefault()?.Close

    <strong>Volume:</strong> @StockDetails.LastOrDefault()?.Volume

</div>


<SfStockChart Height="600px" ID="stockChart">

    <!-- Crosshair and Tooltip Settings -->

    <StockChartCrosshairSettings Enable="true" />

    <StockChartTooltipSettings

    TooltipPosition="TooltipPosition.Nearest"

    Enable="true"

    Format="Open : <b>${point.open}</b><br/>

            High : <b>${point.high}</b><br/>

            Low : <b>${point.low}</b><br/>

            Close : <b>${point.close}</b><br/>

            Volume : <b>${point.volume}</b>" />

    <!-- Define Periods -->

    <StockChartPeriods>

        <StockChartPeriod Text="All" />

        <StockChartPeriod IntervalType=RangeIntervalType.Years Interval="1" Text="1Y" />

        <StockChartPeriod IntervalType=RangeIntervalType.Months Interval="6" Text="6M" />

        <StockChartPeriod IntervalType=RangeIntervalType.Months Interval="3" Text="3M" />

        <StockChartPeriod IntervalType=RangeIntervalType.Months Interval="1" Text="1M" />

    </StockChartPeriods>

    <!-- Candlestick chart series -->

    <StockChartSeriesCollection>

        <StockChartSeries

        DataSource="@StockDetails"

        Type="ChartSeriesType.Candle"

        EnableSolidCandles="true"

        XName="Date"

        Open="Open"

        High="High"

        Low="Low"

        Close="Close"

        Volume="Volume"

        YAxisName="primaryYAxis" />

        <StockChartSeries

        DataSource="@StockDetails"

        Type="ChartSeriesType.Column"

        XName="Date"

        YName="Volume"

        YAxisName="secondaryYAxis" />

    </StockChartSeriesCollection>

    <StockChartAxes>

        <StockChartAxis Name="primaryYAxis" LabelPosition="AxisPosition.Inside" RowIndex="1">

            <StockChartAxisMajorGridLines Width="0" />

            <StockChartAxisMinorGridLines Width="0" />

        </StockChartAxis>

        <StockChartAxis Name="secondaryYAxis" LabelPosition="AxisPosition.Inside" RowIndex="0">

            <StockChartAxisMajorGridLines Width="0" />

            <StockChartAxisMinorGridLines Width="0" />

        </StockChartAxis>

    </StockChartAxes>

    <StockChartRows>

        <StockChartRow Height="20%"></StockChartRow>

        <StockChartRow Height="80%"></StockChartRow>

    </StockChartRows>

    <StockChartPrimaryXAxis ValueType="Syncfusion.Blazor.Charts.ValueType.DateTimeCategory" EdgeLabelPlacement="EdgeLabelPlacement.Shift">

        <StockChartAxisMajorGridLines Width="0" />

        <StockChartAxisMinorGridLines Width="0" />

    </StockChartPrimaryXAxis>

</SfStockChart>


<SfContextMenu Target="#stockChart" TValue="MenuItem">

    <MenuItems>

        <MenuItem Text="Cut" IconCss="e-cm-icons e-cut"></MenuItem>

        <MenuItem Text="Copy" IconCss="e-cm-icons e-copy"></MenuItem>

        <MenuItem Text="Paste" IconCss="e-cm-icons e-paste">

            <MenuItems>

                <MenuItem Text="Paste Text" IconCss="e-cm-icons e-pastetext"></MenuItem>

                <MenuItem Text="Paste Special" IconCss="e-cm-icons e-pastespecial"></MenuItem>

            </MenuItems>

        </MenuItem>

        <MenuItem Separator="true"></MenuItem>

        <MenuItem Text="Link" IconCss="e-cm-icons e-link"></MenuItem>

        <MenuItem Text="New Comment" IconCss="e-cm-icons e-comment"></MenuItem>

    </MenuItems>

    <MenuEvents TValue="MenuItem" ItemSelected="OnMenuItemSelected" />

</SfContextMenu>


@code {

    // Input properties for user selection

    public string TickerSymbol { get; set; } = "AAPL";

    public string StartDate { get; set; } = DateTime.Now.AddYears(-2).ToString("yyyy-MM-dd");

    public string EndDate { get; set; } = DateTime.Now.ToString("yyyy-MM-dd");


    public class StockChartData

    {

        public DateTime Date { get; set; }

        public Double Open { get; set; }

        public Double High { get; set; }

        public Double Low { get; set; }

        public Double Close { get; set; }

        public Double Volume { get; set; }

    }


    public ObservableCollection<StockChartData> StockDetails { get; set; } = new ObservableCollection<StockChartData>();


    protected override async Task OnInitializedAsync()

    {

        await FetchStockData();

    }


    protected async Task FetchStockData()

    {

        var aggregateBars = await PolygonApiService.GetAggregateBars(TickerSymbol, StartDate, EndDate);


        StockDetails.Clear();


        foreach (var bar in aggregateBars)

        {

            StockDetails.Add(new StockChartData

                {

                    Date = DateTimeOffset.FromUnixTimeMilliseconds(bar.Timestamp).DateTime,

                    Open = bar.OpenPrice,

                    High = bar.HighPrice,

                    Low = bar.LowPrice,

                    Close = bar.ClosePrice,

                    Volume = bar.Volume

                });

        }

    }


    private Dictionary<string, Action> ContextMenuActions = new();


    protected override void OnInitialized()

    {

        ContextMenuActions = new Dictionary<string, Action>

        {

            { "Cut", HandleCutAction },

            { "Copy", HandleCopyAction },

            { "Paste", HandlePasteAction },

            { "Link", HandleLinkAction },

            { "New Comment", HandleNewCommentAction }

        };

    }


    private void OnMenuItemSelected(MenuEventArgs<MenuItem> args)

    {

        Console.WriteLine($"Menu item selected: {args.Item.Text}");


        if (ContextMenuActions.ContainsKey(args.Item.Text))

        {

            ContextMenuActions[args.Item.Text].Invoke();

        }

        else

        {

            Console.WriteLine($"No action mapped for: {args.Item.Text}");

        }

    }


    private void HandleCutAction()

    {

        Console.WriteLine("Cut action triggered.");

        // Add logic to handle "Cut" action

    }


    private void HandleCopyAction()

    {

        Console.WriteLine("Copy action triggered.");

        // Add logic to handle "Copy" action

    }


    private void HandlePasteAction()

    {

        Console.WriteLine("Paste action triggered.");

        // Add logic to handle "Paste" action

    }


    private void HandleLinkAction()

    {

        Console.WriteLine("Link action triggered.");

        // Add logic to handle "Link" action

    }


    private void HandleNewCommentAction()

    {

        Console.WriteLine("New Comment action triggered.");

        // Add logic to handle "New Comment" action

    }

}


The very first use-case would be to take a bar (which presumably will always have a date associated with it) and use that to cast another API to Polgyon for more in-depth data researching (Option Snapshots or Equity Block investigation). It's stupid, but it's a UI/UX concern -- I want to investigate certain days, and by being able to click the actual bar or day, and have it go do some action, that's extremely convenient. I realize this thread will be seen by less than <50 people, but anyone who was curious maybe can see how powerful these SfStockChart's can become can start working on them and troubleshooting them in the same manner that I am to make them very powerful and useful. These provided examples already provide a tremendous leg-up over the typical Chart.js stuff, and by being able to use C#/Blazor (along with the entire suite of Syncfusion Components), we have some very powerful tools at our disposal.



DG Durga Gopalakrishnan Syncfusion Team December 10, 2024 03:41 PM UTC

Blair,


We have considered your reported scenario as bug and logged a defect report for the issue “Period and range selector not working after data update with datetimecategory axis”. This fix will be available in our weekly patch release which is scheduled to be rolled out on 24th December 2024. We appreciate your patience until then. You can keep track of the bug from the below feedback link.


Feedback Link : https://www.syncfusion.com/feedback/63859/period-and-range-selector-not-working-after-data-update-with-datetimecategory-axis


If you have any more specification/precise replication procedure or a scenario to be tested, you can add it as a comment in the portal.


Disclaimer: “Inclusion of this solution in the weekly release may change due to other factors including but not limited to QA checks and works reprioritization.”



BL Blair December 10, 2024 04:07 PM UTC

Durga,


Thanks so much for capturing the feedback. Is it appropriate to capture more bugs that I've found with SfStockChart here or create a new thread?


For example: EnablePan=true or EnablePan=false does not work with these configuration settings as well. It might even be a bug that it's not specifically using string data type either. It should be EnablePan="true" or EnablePan="false".


Image_2224_1733846845455




DG Durga Gopalakrishnan Syncfusion Team December 11, 2024 01:05 PM UTC

Blair,


The EnablePan property in the StockChart is a Boolean type, and its value should be specified as true or false, not as a string. However, if you specify it as a string (e.g., "true" or "false"), it will still be interpreted as a Boolean value. For your reference, we have attached a screenshot to illustrate this behavior.



Sample : https://www.syncfusion.com/downloads/support/directtrac/general/ze/PanStkChart-1235090422.zip


Kindly revert us if you have any concerns.



DG Durga Gopalakrishnan Syncfusion Team December 24, 2024 10:51 AM UTC

Blair,


We are glad to announce that our v28.1.36 patch release is rolled out; we have added the fix for reported issue. You can use the latest Syncfusion.Blazor.Charts NuGet package.


Root Cause:

After updating the data for a stock chart with a datetimecategory axis, the labels stored on the axis overlapped with the previous ones.


Fix:

Corrected the calculation of datetimecategoryaxis labels when the data changes in the stock chart.


NuGet Package : https://www.nuget.org/packages/Syncfusion.Blazor.Charts/


Sample : https://www.syncfusion.com/downloads/support/directtrac/general/ze/StkChart-1740102914.zip


Feedback : https://www.syncfusion.com/feedback/63859/period-and-range-selector-not-working-after-data-update-with-datetimecategory-axis


We thank you for your support and appreciate your patience in waiting for this release. Please get in touch with us if you would require any further assistance.


Loader.
Up arrow icon