radzenhq / radzen-blazor

Radzen Blazor is a set of 90+ free native Blazor UI components packed with DataGrid, Scheduler, Charts and robust theming including Material design and FluentUI.
https://www.radzen.com
MIT License
3.56k stars 794 forks source link

RadzenChart creates deadlock #246

Closed MarvinKlein1508 closed 3 years ago

MarvinKlein1508 commented 3 years ago

Describe the bug The RadzenChart will create a deadlock OnMouseOver when you have not specified Tooltips or disabled them.

To Reproduce Consider this example code:

<RadzenChart Style="width: 100%"
            MouseEnter="@(async () => { await  jsRuntime.ShowToast(ToastType.warning, "Test Enter"); })"
            MouseLeave="@(async () => { await  jsRuntime.ShowToast(ToastType.warning, "Test Leave"); })">
    @foreach (KeyValuePair<int, Jahresumsatz> umsatz in Verbrauchsdaten)
    {
        <RadzenLineSeries Smooth="true" Data="@umsatz.Value.ToDataItem(umsatz.Key)" Title="@String.Format(LabelText, umsatz.Key)" CategoryProperty="Month" LineType="LineType.Solid" ValueProperty="Revenue">
            <RadzenMarkers MarkerType="MarkerType.Square" />
        </RadzenLineSeries>
    }
    <Radzen.Blazor.RadzenCategoryAxis FormatString="{0:MMM}" Step="1" Formatter="@FormatAsMonth" />
    <RadzenValueAxis FormatString="@ValueFormat">
        <RadzenGridLines Visible="true" />
        <RadzenAxisTitle />
    </RadzenValueAxis>
</RadzenChart>

This chart shows revenue on a monthly base. When I trigger the MouseOver event, the custom EventCallback is being executed just fine. After it has finished the application is frozen. When I add this line to my RadzenChart <RadzenChartTooltipOptions Visible="false" /> everything works as expected then.

Desktop (please complete the following information):

Additional context This can be fixed by reloading the entire webpage. However once you mouse with the mouse over the chart again, it results in another deadlock.

akorchev commented 3 years ago

I don't understand what the problem is based on the provided information. Can you provide a reproduction?

brcaswell commented 3 years ago

I attempted to reproduce this in LineChartPage.razor in Demo -- replacing jsRuntime.ShowToast with dialog.Confirm. I cannot reproduce this.

@inject DialogService dialog
    <RadzenChart Style="width: 100%"
            MouseEnter="@(async () => { await dialog.Confirm("Test Enter"); })"
            MouseLeave="@(async () => { await dialog.Confirm("Test Leave"); })">
        <RadzenLineSeries Smooth="@smooth" Data="@revenue2019" CategoryProperty="Date" Title="2019" LineType="LineType.Dashed" ValueProperty="Revenue">
            <RadzenMarkers MarkerType="MarkerType.Square" />
        </RadzenLineSeries>
        <RadzenLineSeries Smooth="@smooth" Data="@revenue2020" CategoryProperty="Date" Title="2020" ValueProperty="Revenue">
            <RadzenMarkers MarkerType="MarkerType.Circle" />
        </RadzenLineSeries>
        <Radzen.Blazor.RadzenCategoryAxis FormatString="{0:MMM}" Step="1" />
        <RadzenValueAxis Formatter="@FormatAsUSD">
            <RadzenGridLines Visible="true" />
            <RadzenAxisTitle />
        </RadzenValueAxis>
    </RadzenChart>

As such, the issue you're observing seems to relate to jsRuntime.ShowToast usage.

Note: there is no ShowToast on IJSRuntime, so I'm not exactly sure what jsRuntime type is here -- and how this ShowToast method is defined. Is it an extension on IJSRuntime that is calling to a custom ToastService?

MarvinKlein1508 commented 3 years ago

As such, the issue you're observing seems to relate to jsRuntime.ShowToast usage. No I just used this extension method to test the MouseEvents.

Is it an extension on IJSRuntime that is calling to a custom ToastService? Yes. I tjust creates a new toastr toast via JS.

I think it has somethine todo with my foreach within the RadzenChart component. The class Jahresumsatz is defined as:

public class Jahresumsatz
    {
        private readonly Dictionary<int, decimal> _umsatzWerte;

        public int Jahr { get; set; }
        public decimal Januar => GetValueAt(1);
        public decimal Februar => GetValueAt(2);
        public decimal März => GetValueAt(3);
        public decimal April => GetValueAt(4);
        public decimal Mai => GetValueAt(5);
        public decimal Juni => GetValueAt(6);
        public decimal Juli => GetValueAt(7);
        public decimal August => GetValueAt(8);
        public decimal September => GetValueAt(9);
        public decimal Oktober => GetValueAt(10);
        public decimal November => GetValueAt(11);
        public decimal Dezember => GetValueAt(12);

        public decimal Total => Januar + Februar + März + April + Mai + Juni + Juli + August + September + Oktober + November + Dezember;

        private decimal this[int i]
        {
            get
            {
                return GetValueAt(i);
            }
        }

        public decimal GetValueAt(int index)
        {
            if (_umsatzWerte.ContainsKey(index))
            {
                return _umsatzWerte[index];
            }
            return 0.0m;
        }

        private void SetValueAt(int index, decimal value)
        {
            if (_umsatzWerte.ContainsKey(index))
            {
                _umsatzWerte[index] = value;
            }
            else
            {
                _umsatzWerte.Add(index, value);
            }
        }

        public Jahresumsatz(Dictionary<int, decimal> werte)
        {
            _umsatzWerte = werte;
        }

        private Jahresumsatz()
        {
            _umsatzWerte = new Dictionary<int, decimal>();
        }

        public static Jahresumsatz FromList(List<Jahresumsatz> umsätze)
        {
            Jahresumsatz tmp = new Jahresumsatz();
            foreach (var umsatz in umsätze)
            {
                tmp += umsatz;
            }
            return tmp;
        }

        public IEnumerable<decimal> ToIEnumerable()
        {
            List<decimal> values = new List<decimal>();
            for (int i = 1; i <= 12; i++)
            {
                values.Add(GetValueAt(i));
            }
            return values.AsEnumerable();
        }
        public List<JahresumsatzDataItem> ToDataItem(int year)
        {
            List<JahresumsatzDataItem> data = new List<JahresumsatzDataItem>();
            for (int i = 1; i <= 12; i++)
            {
                data.Add(new JahresumsatzDataItem
                {
                    Month = i,
                    Revenue = GetValueAt(i)
                });
            }
            return data;
        }

        public static Jahresumsatz operator +(Jahresumsatz a, Jahresumsatz b)
        {
            Jahresumsatz tmp = new Jahresumsatz() { Jahr = a.Jahr == b.Jahr ? a.Jahr : 0 };
            for (int i = 1; i <= 12; i++)
            {
                tmp.SetValueAt(i, a.GetValueAt(i) + b.GetValueAt(i));
            }
            return tmp;
        }

        public static Jahresumsatz operator -(Jahresumsatz a, Jahresumsatz b)
        {
            Jahresumsatz tmp = new Jahresumsatz() { Jahr = a.Jahr == b.Jahr ? a.Jahr : 0 };
            for (int i = 1; i <= 12; i++)
            {
                tmp.SetValueAt(i, a.GetValueAt(i) - b.GetValueAt(i));
            }
            return tmp;
        }
    }

    public class JahresumsatzDataItem
    {
        public int Month { get; set; }
        public decimal Revenue { get; set; }
    }

i = 1 is the revenue for January i = 2 is the revenue for February and so on.

Dictionary<int, Jahresumsatz> Verbrauchsdaten holds the data for the last 4 years. Which gets casted into a JahresumsatzDataItem which is being used by the RadzenChart.

@foreach (KeyValuePair<int, Jahresumsatz> umsatz in Verbrauchsdaten)
                {
                    <RadzenLineSeries Smooth="true" Data="@umsatz.Value.ToDataItem(umsatz.Key)" Title="@String.Format(LabelText, umsatz.Key)" CategoryProperty="Month" LineType="LineType.Solid" ValueProperty="Revenue">
                        <RadzenMarkers MarkerType="MarkerType.Square" />
                    </RadzenLineSeries>
                }
brcaswell commented 3 years ago

I am able to reproduce using this class you provided.

    Dictionary<int, Jahresumsatz> Verbrauchsdaten => new Dictionary<int, Jahresumsatz>
    {
        { 1, new LineChartPage.Jahresumsatz(new Dictionary<int, decimal> { {1, 0.1m }, {2, 0.5m } }) },
        { 2, new LineChartPage.Jahresumsatz(new Dictionary<int, decimal> { {1, 0.4m }, {2, 1.5m } }) },
        { 3, new LineChartPage.Jahresumsatz(new Dictionary<int, decimal> { {1, 0.2m }, {2, 1.3m } }) },
        { 4, new LineChartPage.Jahresumsatz(new Dictionary<int, decimal> { {1, 0.7m }, {2, 2.1m } }) }
    };

Though I'm uncertain as to the underline cause, it seems there is an infinite loop created with CartesianSeries<TItem> conditional call to Chart.DisplayTooltip() in SetParametersAsync.

Commenting out that Chart.DisplayTooltip() in that scope resolves properly and displays the tooltip.

@akorchev from git history I can't determine the merit to that line to call DisplayTooltip -- The file changes were all introduced in a broader push. Note: MouseMove event calls to DisplayTooltip in this scenario/control.


note: that I wasn't able to reproduce this based simply on this being a foreach.

I foreach looped over this property with no issues (binding where applicable)

    IEnumerable<(string Title, MarkerType MarkerType, LineType LineType, DataItem[] DataItems)> revenues => new List<(string Title, MarkerType markerType, LineType LineType, DataItem[] DataItems)>
    {
        ("2019", MarkerType.Square, LineType.Dashed, revenue2019),
        ("2020", MarkerType.Circle, LineType.Solid, revenue2020),
    };
akorchev commented 3 years ago

I think this could happen if the Data of the series changes on every render. Here is a forum thread describing a similar sounding issue: https://forum.radzen.com/t/pie-chart-tool-tip-problem/8590

The tooltitp should redisplay if the Data of the series changes intentionally.

brcaswell commented 3 years ago

thanks for the insight @akorchev

in regards to

The problem is the definition of the processedData property - it returns completely new data every time it is accessed.

that does seem to be the case here as well; and as a workaround that is good advice;

However, the observed issue there and here is one that can and probably should be addressed in Razden -- as I don't think this is an issue inherent to Blazor.

EDIT: merged I created the PR above; but if this is a matter that needs more investigation on, feel free to close/abandon it and we'll just advise using this convention until the matter can be fully determined on.