apexcharts / Blazor-ApexCharts

A blazor wrapper for ApexCharts.js
https://apexcharts.github.io/Blazor-ApexCharts
MIT License
825 stars 94 forks source link

Donut and Pie charts display `series-%` in `XValue` #351

Closed slyvain closed 9 months ago

slyvain commented 11 months ago

The donut and pie charts display series-% instead of the correct values in XValue.

This issue does not occur with Bar or Line charts.

The data is fetched on demand from the user on the page.

Example:

<ApexChart @ref="_donutChart" 
                     TItem="ChartItem"
                     Options="_options"
                     Title="Order Gross Value">

                    <ApexPointSeries TItem="ChartItem"
                                     Items="_chartData "
                                     SeriesType="SeriesType.Donut"
                                     Name="Total"
                                     XValue="e => e.Name"
                                     YValue="e => e.Total"
                                     OrderByDescending="e=> e.Y" />
</ApexChart>
@code {
  private ApexChart<ChartItem> _donutChart;
  private ApexChartOptions<ChartItem> _options { get; set; } = new();
  private List<ChartItem> _chartData = new();

  public class ChartItem
  {
        public string Name { get; set; }
        public decimal Total { get; set; }
  }

  protected override async Task OnInitializedAsync()
  {
        _options.NoData = new NoData() { Text = "No data" };
  }

  private async Task SomeDataOnThePageChanged()
  {
      await LoadChartData();
  }

  private async Task LoadChartData()
  {
       _chartData = await GetChartDataFromDatastore();

       StateHasChanged();
       await _donutChart.UpdateSeriesAsync();
    }
}

2023-12-04 09_09_57-BudgetWise

Additional info If I replace the following lines:

StateHasChanged();
await _donutChart.UpdateSeriesAsync();

by this line:

await _donutChart.RenderAsync();

Then the chart does not render at the first call of SomeDataOnThePageChanged but it does on the second call, correctly showing data gathered from the previous call. I know it's not supposed to be used that way, but that's interesting

2023-12-04 09_12_19-BudgetWise

joadan commented 11 months ago

Hello,

I believe that % is the default for labels in Pie/Donut charts in apexcharts.js https://apexcharts.com/docs/chart-types/pie-donut/

slyvain commented 11 months ago

Hi @joadan

When the data is loaded from the OnInitializedAsync method, the chart and its legend are displayed correctly.

joadan commented 11 months ago

Hi, Not sure I understand..

Is the chart showing labels with absolute values when you load from OnInitializedAsync? That sounds really strange, is it possible for you create a minimal repo where this is reproduced?

slyvain commented 11 months ago

So I did a bit more investigation on the behavior, and here a sample to reproduce what I experience.

I re-used the code in the UpdateSeries method.

Here's what I changed:

What we can see:

series_issue

<DemoContainer>
    <Button class="mb-2" OnClick=UpdateChartSeries BackgroundColor="TablerColor.Primary">Update Series</Button>

    @if (forecasts != null)
    {
        <ApexChart TItem="WeatherForecast"
                   Title="Temp C"
                   @ref="chart">

            <ApexPointSeries TItem="WeatherForecast"
                             Items="forecasts"
                             Name="Temp C"
                             XValue="@(e => e.Summary)"
                             YValue="@(e => e.TemperatureC)"
                             SeriesType="SeriesType.Donut" />
        </ApexChart>
    }
</DemoContainer>

<div>
    <Table Items="forecasts">
        <Column Item="WeatherForecast" Property="e=> e.Summary" />
        <Column Item="WeatherForecast" Property="e=> e.TemperatureC" />
    </Table>
</div>

@code {
    private List<WeatherForecast> forecasts { get; set; }
    private ApexChart<WeatherForecast> chart;

    protected override async Task OnInitializedAsync()
    {
        await LoadDataAsync(2); // get small sample first

        await base.OnInitializedAsync();
    }

    private async Task LoadDataAsync(int? limit)
    {
        var tempForecast = await SampleData.GetForecastAsync(DateTime.Today);

        var groupedData = tempForecast.GroupBy(x => x.Summary)
                          .Select(x => new WeatherForecast()
                              {
                                  Date = x.First().Date,
                                  Summary = x.First().Summary,
                                  TemperatureC = x.Sum(y => Math.Abs(y.TemperatureC)) // easier to compare with positive values
                              })
                          .ToList();

        forecasts = limit.HasValue
            ? groupedData.Take(limit.Value).ToList()
            : groupedData.ToList();
    }

    private async Task UpdateChartSeries()
    {
        await LoadDataAsync(null); // get full sample on update
        await chart.UpdateSeriesAsync(true);
    }
}

Going further, I could fix the names and the chart with correct values by adding this line after chart.UpdateSeriesAsync

await chart.UpdateOptionsAsync(true, true, false);

This however does not animate the graph despite the true parameters.

I fixed it, somewhat (see gif animation) by adding:

await chart.RenderAsync();

Final method:

    private async Task UpdateChartSeries()
    {
        await LoadDataAsync(null); // get full sample on update
        await chart.UpdateSeriesAsync(true);

        // fixes the values
        await chart.UpdateOptionsAsync(true, true, false);

        // kinda fixes the animation: not always smooth
        await chart.RenderAsync();
    }

animation_issue

slyvain commented 11 months ago

I then when further and tried to simulate a 50ms datastore call. I added await Task.Delay(50) to the LoadDataAsync method:

private async Task LoadDataAsync(int? limit)
    {
        await Task.Delay(50);

        var tempForecast = await SampleData.GetForecastAsync(DateTime.Today);

        var groupedData = tempForecast.GroupBy(x => x.Summary)
                          .Select(x => new WeatherForecast()
                              {
                                  Date = x.First().Date,
                                  Summary = x.First().Summary,
                                  TemperatureC = x.Sum(y => Math.Abs(y.TemperatureC)) // easier to compare with positive values
                              })
                          .ToList();

        forecasts = limit.HasValue
            ? groupedData.Take(limit.Value).ToList()
            : groupedData.ToList();
    }

Now, when pressing the UpdateSeries button, the chart does not update anything at all. The table proves the data has been updated though. Then press the button several times and the chart data is always 1 iteration behind the table.

animation_behind_issue

joadan commented 10 months ago

Hello,

please check https://apexcharts.github.io/Blazor-ApexCharts/donut-charts#dynamic

Here I use UpdateOptionsAsync, UpdateSeriesAsync wont work because it is only updates the actually values. This is documented here https://apexcharts.github.io/Blazor-ApexCharts/methods/update-series

slyvain commented 10 months ago

I still had an issue with that and I think it's because there is slight delay in fetching the data. Since this application is for my use only, I decided to load all my data when loading the page (then I can still get a coffee while waiting) and filter it when needed. Doing so, the chart works perfectly.