mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-07-08 18:16:08 +00:00
1080 lines
37 KiB
C#
1080 lines
37 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using Microsoft.AspNetCore.Components;
|
|
using System.Linq;
|
|
using Radzen.Blazor.Rendering;
|
|
using System.Threading.Tasks;
|
|
using System.Net.Mime;
|
|
using Microsoft.AspNetCore.Components.Rendering;
|
|
|
|
namespace Radzen.Blazor
|
|
{
|
|
/// <summary>
|
|
/// Base class of <see cref="RadzenChart" /> series.
|
|
/// </summary>
|
|
/// <typeparam name="TItem">The type of the series data.</typeparam>
|
|
[UnconditionalSuppressMessage(TrimMessages.Trimming, TrimMessages.IL2026, Justification = TrimMessages.DataTypePreserved)]
|
|
[UnconditionalSuppressMessage(TrimMessages.Trimming, TrimMessages.IL2087, Justification = TrimMessages.DataTypePreserved)]
|
|
public abstract class CartesianSeries<TItem> : RadzenChartComponentBase, IChartSeries, IChartValueAxisSeries, IDisposable
|
|
{
|
|
/// <summary>
|
|
/// Cache for the value returned by <see cref="Category"/> when that value is only dependent on
|
|
/// <see cref="CategoryProperty"/>.
|
|
/// </summary>
|
|
Func<TItem, double>? categoryPropertyCache;
|
|
|
|
/// <summary>
|
|
/// Returns the parent <see cref="RadzenChart"/> instance or throws an <see cref="InvalidOperationException"/> if not present.
|
|
/// </summary>
|
|
/// <returns>The parent <see cref="RadzenChart"/>.</returns>
|
|
/// <exception cref="InvalidOperationException">Thrown when the parent chart is not set.</exception>
|
|
protected RadzenChart RequireChart()
|
|
{
|
|
return Chart ?? throw new InvalidOperationException($"{GetType().Name} requires a parent RadzenChart.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a getter function that returns a value from the specified category scale for the specified data item.
|
|
/// </summary>
|
|
/// <param name="scale">The scale.</param>
|
|
internal Func<TItem, double> Category(ScaleBase scale)
|
|
{
|
|
if (categoryPropertyCache != null)
|
|
{
|
|
return categoryPropertyCache;
|
|
}
|
|
|
|
if (IsNumeric(CategoryProperty))
|
|
{
|
|
categoryPropertyCache = PropertyAccess.Getter<TItem, double>(CategoryProperty!);
|
|
return categoryPropertyCache;
|
|
}
|
|
|
|
if (IsDate(CategoryProperty))
|
|
{
|
|
var category = PropertyAccess.Getter<TItem, DateTime>(CategoryProperty!);
|
|
categoryPropertyCache = (item) => category(item).Ticks;
|
|
return categoryPropertyCache;
|
|
}
|
|
|
|
if (scale is OrdinalScale ordinal)
|
|
{
|
|
if (String.IsNullOrEmpty(CategoryProperty))
|
|
{
|
|
return (item) => Items.IndexOf(item);
|
|
}
|
|
|
|
var ordinalCategory = PropertyAccess.Getter<TItem, object>(CategoryProperty);
|
|
|
|
return (item) => ordinal.Data?.IndexOf(ordinalCategory(item)) ?? -1;
|
|
}
|
|
|
|
return (item) => Items.IndexOf(item);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper function. Used internally.
|
|
/// </summary>
|
|
protected Func<TItem, double> ComposeCategory(ScaleBase scale)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(scale);
|
|
|
|
return scale.Compose(Category(scale));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper function. Used internally.
|
|
/// </summary>
|
|
protected Func<TItem, double> ComposeValue(ScaleBase scale)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(scale);
|
|
|
|
return scale.Compose(Value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the ValueProperty type is nullable.
|
|
/// </summary>
|
|
protected bool IsValueNullable()
|
|
{
|
|
if (string.IsNullOrEmpty(ValueProperty))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var propertyType = PropertyAccess.GetPropertyType(typeof(TItem), ValueProperty);
|
|
return propertyType != null && Nullable.GetUnderlyingType(propertyType) != null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a function that returns the raw object value of the ValueProperty (before conversion to double).
|
|
/// Used to detect null values in nullable numeric properties.
|
|
/// </summary>
|
|
protected Func<TItem, object?> GetRawValueGetter()
|
|
{
|
|
return PropertyAccess.Getter<TItem, object?>(ValueProperty!);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether the property with the specified name is <see cref="DateTime" />
|
|
/// </summary>
|
|
/// <param name="propertyName">Name of the property.</param>
|
|
/// <returns><c>true</c> if the specified property name is date; otherwise, <c>false</c>.</returns>
|
|
/// <exception cref="ArgumentException">Property {propertyName} does not exist</exception>
|
|
protected bool IsDate(string? propertyName)
|
|
{
|
|
if (String.IsNullOrEmpty(propertyName))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var property = PropertyAccess.GetPropertyType(typeof(TItem), propertyName);
|
|
|
|
if (property == null)
|
|
{
|
|
throw new ArgumentException($"Property {propertyName} does not exist");
|
|
}
|
|
|
|
if(PropertyAccess.IsDateOnly(property))
|
|
{
|
|
return false;
|
|
}
|
|
return PropertyAccess.IsDate(property);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether the property with the specified name is numeric.
|
|
/// </summary>
|
|
/// <param name="propertyName">Name of the property.</param>
|
|
/// <returns><c>true</c> if the specified property name is numeric; otherwise, <c>false</c>.</returns>
|
|
/// <exception cref="ArgumentException">Property {propertyName} does not exist</exception>
|
|
protected bool IsNumeric(string? propertyName)
|
|
{
|
|
if (String.IsNullOrEmpty(propertyName))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var property = PropertyAccess.GetPropertyType(typeof(TItem), propertyName);
|
|
|
|
if (property == null)
|
|
{
|
|
throw new ArgumentException($"Property {propertyName} does not exist");
|
|
}
|
|
|
|
return PropertyAccess.IsNumeric(property);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the name of the value axis this series is bound to.
|
|
/// When null or empty, the series uses the primary (default) axis.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string? ValueAxisName { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
[Parameter]
|
|
public string Title { get; set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the child content.
|
|
/// </summary>
|
|
/// <value>The child content.</value>
|
|
[Parameter]
|
|
public RenderFragment? ChildContent { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the tooltip template.
|
|
/// </summary>
|
|
/// <value>The tooltip template.</value>
|
|
[Parameter]
|
|
public RenderFragment<TItem>? TooltipTemplate { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets the list of overlays.
|
|
/// </summary>
|
|
/// <value>The Overlays list.</value>
|
|
public IList<IChartSeriesOverlay> Overlays { get; } = new List<IChartSeriesOverlay>();
|
|
|
|
/// <summary>
|
|
/// Gets the coordinate system of the series.
|
|
/// </summary>
|
|
/// <value>Coordinate system enum value.</value>
|
|
public virtual CoordinateSystem CoordinateSystem => CoordinateSystem.Cartesian;
|
|
|
|
/// <summary>
|
|
/// The name of the property of <typeparamref name="TItem" /> that provides the X axis (a.k.a. category axis) values.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string? CategoryProperty { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether this <see cref="CartesianSeries{TItem}"/> is visible.
|
|
/// Invisible series do not appear in the legend and cannot be shown by the user.
|
|
/// Use the <c>Visible</c> property to programatically show or hide a series.
|
|
/// </summary>
|
|
/// <value><c>true</c> if visible; otherwise, <c>false</c>.</value>
|
|
[Parameter]
|
|
public bool Visible { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether this <see cref="CartesianSeries{TItem}"/> is hidden.
|
|
/// Hidden series are initially invisible and the user can show them by clicking on their label in the legend.
|
|
/// Use the <c>Hidden</c> property to hide certain series from your users but still allow them to see them.
|
|
/// </summary>
|
|
/// <value><c>true</c> if hidden; otherwise, <c>false</c>.</value>
|
|
[Parameter]
|
|
public bool Hidden { get; set; }
|
|
|
|
bool IsVisible { get; set; } = true;
|
|
|
|
bool IChartSeries.Visible
|
|
{
|
|
get
|
|
{
|
|
return IsVisible;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool ShowInLegend { get => Visible; }
|
|
|
|
/// <summary>
|
|
/// The name of the property of <typeparamref name="TItem" /> that provides the Y axis (a.k.a. value axis) values.
|
|
/// </summary>
|
|
[Parameter]
|
|
public string? ValueProperty { get; set; }
|
|
|
|
/// <inheritdoc />
|
|
[Parameter]
|
|
public int RenderingOrder { get; set; }
|
|
|
|
/// <summary>
|
|
/// Creates a getter function that returns a value from the specified data item. Uses <see cref="ValueProperty" />.
|
|
/// </summary>
|
|
/// <value>The value.</value>
|
|
/// <exception cref="ArgumentException">ValueProperty should not be empty</exception>
|
|
internal Func<TItem, double> Value
|
|
{
|
|
get
|
|
{
|
|
if (String.IsNullOrEmpty(ValueProperty))
|
|
{
|
|
throw new ArgumentException("ValueProperty should not be empty");
|
|
}
|
|
|
|
return PropertyAccess.Getter<TItem, double>(ValueProperty);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the data of the series. The data is enumerated and its items are displayed by the series.
|
|
/// </summary>
|
|
/// <value>The data.</value>
|
|
[Parameter]
|
|
public IEnumerable<TItem>? Data { get; set; }
|
|
|
|
/// <summary>
|
|
/// Stores <see cref="Data" /> as an IList of <typeparamref name="TItem"/>.
|
|
/// </summary>
|
|
/// <value>The items.</value>
|
|
protected IList<TItem> Items { get; set; } = new List<TItem>();
|
|
|
|
/// <inheritdoc />
|
|
public RadzenMarkers Markers { get; set; } = new RadzenMarkers();
|
|
|
|
/// <inheritdoc />
|
|
public virtual MarkerType MarkerType
|
|
{
|
|
get
|
|
{
|
|
var markerType = MarkerType.None;
|
|
|
|
if (Markers != null)
|
|
{
|
|
markerType = Markers.MarkerType;
|
|
}
|
|
|
|
if (markerType == MarkerType.Auto && Chart != null)
|
|
{
|
|
var index = Chart.Series.IndexOf(this);
|
|
|
|
var types = new[] { MarkerType.Circle, MarkerType.Triangle, MarkerType.Square, MarkerType.Diamond };
|
|
|
|
markerType = types[index % types.Length];
|
|
}
|
|
|
|
return markerType;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the category values
|
|
/// </summary>
|
|
protected virtual IList<object> GetCategories()
|
|
{
|
|
if (String.IsNullOrEmpty(CategoryProperty))
|
|
{
|
|
return Items.Select((item, index) => (object)index).ToList();
|
|
}
|
|
|
|
return Items.Select(PropertyAccess.Getter<TItem, object>(CategoryProperty)).ToList();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual ScaleBase TransformCategoryScale(ScaleBase scale)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(scale);
|
|
|
|
if (Items == null)
|
|
{
|
|
return scale;
|
|
}
|
|
|
|
if (Items.Any())
|
|
{
|
|
scale.Input.MergeWidth(ScaleRange.From(Items, Category(scale)));
|
|
}
|
|
|
|
if (IsNumeric(CategoryProperty))
|
|
{
|
|
return scale;
|
|
}
|
|
|
|
if (IsDate(CategoryProperty))
|
|
{
|
|
return new DateScale
|
|
{
|
|
Input = scale.Input,
|
|
Output = scale.Output
|
|
};
|
|
}
|
|
|
|
var data = GetCategories();
|
|
|
|
if (scale is OrdinalScale ordinal && ordinal.Data != null)
|
|
{
|
|
foreach (var item in ordinal.Data)
|
|
{
|
|
if (!data.Contains(item))
|
|
{
|
|
var index = ordinal.Data.IndexOf(item);
|
|
if (index <= data.Count)
|
|
{
|
|
data.Insert(index, item);
|
|
}
|
|
else
|
|
{
|
|
data.Add(item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return new OrdinalScale
|
|
{
|
|
Data = data,
|
|
Input = scale.Input,
|
|
Output = scale.Output,
|
|
};
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual ScaleBase TransformValueScale(ScaleBase scale)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(scale);
|
|
|
|
if (Items != null)
|
|
{
|
|
IEnumerable<TItem> items = Items;
|
|
|
|
if (IsValueNullable())
|
|
{
|
|
var rawGetter = GetRawValueGetter();
|
|
items = items.Where(item => rawGetter(item) != null);
|
|
}
|
|
|
|
if (items.Any())
|
|
{
|
|
scale.Input.MergeWidth(ScaleRange.From(items, Value));
|
|
}
|
|
}
|
|
|
|
return scale;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public abstract RenderFragment Render(ScaleBase categoryScale, ScaleBase valueScale);
|
|
|
|
/// <inheritdoc />
|
|
public RenderFragment RenderOverlays(ScaleBase categoryScale, ScaleBase valueScale)
|
|
{
|
|
return new RenderFragment(builder =>
|
|
{
|
|
builder.OpenRegion(0);
|
|
foreach (var overlay in Overlays)
|
|
{
|
|
if (overlay.Visible && !overlay.RenderOnTop)
|
|
{
|
|
builder.AddContent(1, overlay.Render(categoryScale, valueScale));
|
|
}
|
|
}
|
|
builder.CloseRegion();
|
|
});
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public RenderFragment RenderTopOverlays(ScaleBase categoryScale, ScaleBase valueScale)
|
|
{
|
|
return new RenderFragment(builder =>
|
|
{
|
|
builder.OpenRegion(0);
|
|
foreach (var overlay in Overlays)
|
|
{
|
|
if (overlay.Visible && overlay.RenderOnTop)
|
|
{
|
|
builder.AddContent(1, overlay.Render(categoryScale, valueScale));
|
|
}
|
|
}
|
|
builder.CloseRegion();
|
|
});
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public abstract string Color { get; }
|
|
|
|
/// <summary>
|
|
/// Specifies whether the chart highlights the hovered data point of this series with an active point
|
|
/// dot. Range-style series override this to <c>false</c> - a single dot cannot represent their value range.
|
|
/// </summary>
|
|
public virtual bool ShowActivePoint => true;
|
|
|
|
/// <inheritdoc />
|
|
public override async Task SetParametersAsync(ParameterView parameters)
|
|
{
|
|
var shouldRefresh = parameters.DidParameterChange(nameof(Data), Data);
|
|
var visibleChanged = parameters.DidParameterChange(nameof(Visible), Visible);
|
|
var hiddenChanged = parameters.DidParameterChange(nameof(Hidden), Hidden);
|
|
var categoryChanged = parameters.DidParameterChange(nameof(CategoryProperty), CategoryProperty);
|
|
|
|
await base.SetParametersAsync(parameters);
|
|
|
|
if (hiddenChanged)
|
|
{
|
|
IsVisible = !Hidden;
|
|
shouldRefresh = true;
|
|
}
|
|
|
|
if (visibleChanged)
|
|
{
|
|
IsVisible = Visible;
|
|
shouldRefresh = true;
|
|
}
|
|
|
|
if (categoryChanged || shouldRefresh)
|
|
{
|
|
categoryPropertyCache = null;
|
|
}
|
|
|
|
if (Data != null && Data.Count() != Items.Count)
|
|
{
|
|
shouldRefresh = true;
|
|
}
|
|
|
|
if (shouldRefresh)
|
|
{
|
|
if (Data != null)
|
|
{
|
|
if (Data is IList<TItem> list)
|
|
{
|
|
Items = list;
|
|
}
|
|
else
|
|
{
|
|
Items = Data.ToList();
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(CategoryProperty) && (IsDate(CategoryProperty) || IsNumeric(CategoryProperty)))
|
|
{
|
|
Items = Items.AsQueryable().OrderBy(CategoryProperty).ToList();
|
|
}
|
|
}
|
|
|
|
if (Chart != null)
|
|
{
|
|
await Chart.Refresh(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void Initialize()
|
|
{
|
|
Chart?.AddSeries(this);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual bool Contains(double x, double y, double tolerance)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual double MeasureLegend()
|
|
{
|
|
return TextMeasurer.TextWidth(GetTitle());
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual IEnumerable<double> MeasureLegendItems()
|
|
{
|
|
return new[] { TextMeasurer.TextWidth(GetTitle()) };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if the provided point is inside the provided polygon.
|
|
/// </summary>
|
|
/// <param name="point">The point.</param>
|
|
/// <param name="polygon">The polygon.</param>
|
|
/// <returns><c>true</c> if the polygon contains the point, <c>false</c> otherwise.</returns>
|
|
protected bool InsidePolygon(Point point, Point[] polygon)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(point);
|
|
ArgumentNullException.ThrowIfNull(polygon);
|
|
|
|
var minX = polygon[0].X;
|
|
var maxX = polygon[0].X;
|
|
var minY = polygon[0].Y;
|
|
var maxY = polygon[0].Y;
|
|
|
|
for (var i = 1; i < polygon.Length; i++)
|
|
{
|
|
var current = polygon[i];
|
|
minX = Math.Min(current.X, minX);
|
|
maxX = Math.Max(current.X, maxX);
|
|
minY = Math.Min(current.Y, minY);
|
|
maxY = Math.Max(current.Y, maxY);
|
|
}
|
|
|
|
if (point.X < minX || point.X > maxX || point.Y < minY || point.Y > maxY)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool inside = false;
|
|
|
|
for (int i = 0, j = polygon.Length - 1; i < polygon.Length; j = i++)
|
|
{
|
|
if ((polygon[i].Y > point.Y) != (polygon[j].Y > point.Y) &&
|
|
point.X < (polygon[j].X - polygon[i].X) * (point.Y - polygon[i].Y) / (polygon[j].Y - polygon[i].Y) + polygon[i].X)
|
|
{
|
|
inside = !inside;
|
|
}
|
|
}
|
|
|
|
return inside;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual RenderFragment RenderTooltip(object data)
|
|
{
|
|
var chart = RequireChart();
|
|
var item = (TItem)data;
|
|
|
|
return builder =>
|
|
{
|
|
if (chart.Tooltip.Shared)
|
|
{
|
|
var category = !string.IsNullOrEmpty(CategoryProperty) ? PropertyAccess.GetValue(item, CategoryProperty) : null;
|
|
if (category != null)
|
|
{
|
|
builder.OpenComponent<ChartSharedTooltip>(0);
|
|
builder.AddAttribute(1, nameof(ChartSharedTooltip.Class), TooltipClass(item));
|
|
builder.AddAttribute(2, nameof(ChartSharedTooltip.Title), TooltipTitle(item));
|
|
builder.AddAttribute(3, nameof(ChartSharedTooltip.ChildContent), RenderSharedTooltipItems(category));
|
|
builder.CloseComponent();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
builder.OpenComponent<ChartTooltip>(0);
|
|
builder.AddAttribute(1, nameof(ChartTooltip.ChildContent), TooltipTemplate?.Invoke(item));
|
|
builder.AddAttribute(2, nameof(ChartTooltip.Title), TooltipTitle(item));
|
|
builder.AddAttribute(3, nameof(ChartTooltip.Label), TooltipLabel(item));
|
|
builder.AddAttribute(4, nameof(ChartTooltip.Value), TooltipValue(item));
|
|
builder.AddAttribute(5, nameof(ChartTooltip.Class), TooltipClass(item));
|
|
builder.AddAttribute(6, nameof(ChartTooltip.Style), TooltipStyle(item));
|
|
builder.CloseComponent();
|
|
}
|
|
};
|
|
}
|
|
|
|
private RenderFragment RenderSharedTooltipItems(object category)
|
|
{
|
|
var chart = RequireChart();
|
|
|
|
return builder =>
|
|
{
|
|
var visibleSeries = chart.Series.Where(s => s.Visible).ToList();
|
|
|
|
foreach (var series in visibleSeries)
|
|
{
|
|
builder.AddContent(1, series.RenderSharedTooltipItem(category));
|
|
}
|
|
};
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual RenderFragment RenderSharedTooltipItem(object category)
|
|
{
|
|
return builder =>
|
|
{
|
|
var item = Items.FirstOrDefault(i => !string.IsNullOrEmpty(CategoryProperty) && object.Equals(PropertyAccess.GetValue(i, CategoryProperty), category));
|
|
|
|
if (item != null)
|
|
{
|
|
builder.OpenComponent<ChartSharedTooltipItem>(0);
|
|
builder.AddAttribute(1, nameof(ChartSharedTooltipItem.Value), TooltipValue(item));
|
|
builder.AddAttribute(2, nameof(ChartSharedTooltipItem.ChildContent), TooltipTemplate?.Invoke(item));
|
|
builder.AddAttribute(3, nameof(ChartSharedTooltipItem.LegendItem), RenderLegendItem(false));
|
|
builder.CloseComponent();
|
|
}
|
|
};
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Point GetTooltipPosition(object data)
|
|
{
|
|
var item = (TItem)data;
|
|
var x = TooltipX(item);
|
|
var y = TooltipY(item);
|
|
|
|
return new Point { X = x, Y = y };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the tooltip inline style.
|
|
/// </summary>
|
|
/// <param name="item">The item.</param>
|
|
protected virtual string TooltipStyle(TItem item)
|
|
{
|
|
return Chart?.Tooltip?.Style ?? string.Empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the tooltip CSS class.
|
|
/// </summary>
|
|
/// <param name="item">The item.</param>
|
|
protected virtual string TooltipClass(TItem item)
|
|
{
|
|
var chart = Chart;
|
|
if (chart == null)
|
|
{
|
|
return "rz-series-tooltip";
|
|
}
|
|
|
|
return $"rz-series-{chart.Series.IndexOf(this)}-tooltip";
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual RenderFragment RenderLegendItem()
|
|
{
|
|
return RenderLegendItem(true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders the legend item for this series.
|
|
/// </summary>
|
|
protected virtual RenderFragment RenderLegendItem(bool clickable)
|
|
{
|
|
var chart = RequireChart();
|
|
var index = chart.Series.IndexOf(this);
|
|
var style = new List<string>();
|
|
|
|
if (IsVisible == false)
|
|
{
|
|
style.Add("text-decoration: line-through");
|
|
}
|
|
|
|
return builder =>
|
|
{
|
|
builder.OpenComponent<LegendItem>(0);
|
|
builder.AddAttribute(1, nameof(LegendItem.Index), index);
|
|
builder.AddAttribute(2, nameof(LegendItem.Color), Color);
|
|
builder.AddAttribute(3, nameof(LegendItem.MarkerType), MarkerType);
|
|
builder.AddAttribute(4, nameof(LegendItem.Style), string.Join(";", style));
|
|
builder.AddAttribute(5, nameof(LegendItem.MarkerSize), MarkerSize);
|
|
builder.AddAttribute(6, nameof(LegendItem.Text), GetTitle());
|
|
builder.AddAttribute(7, nameof(LegendItem.Click), EventCallback.Factory.Create(this, OnLegendItemClick));
|
|
builder.AddAttribute(8, nameof(LegendItem.Clickable), clickable);
|
|
builder.AddAttribute(9, nameof(LegendItem.ShowLine), ShowLineInLegend);
|
|
builder.CloseComponent();
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Specifies whether the legend swatch displays a line indicator instead of a filled marker. Overridden by line series.
|
|
/// </summary>
|
|
protected internal virtual bool ShowLineInLegend => false;
|
|
|
|
/// <inheritdoc />
|
|
public double MarkerSize
|
|
{
|
|
get
|
|
{
|
|
double markerSize = 5;
|
|
|
|
if (Markers != null)
|
|
{
|
|
markerSize = Markers.Size;
|
|
}
|
|
|
|
return markerSize;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public double GetMedian()
|
|
{
|
|
var values = Items.Select(Value).OrderBy(e => e).ToList();
|
|
if (values.Count == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return values[values.Count / 2];
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public double GetMean()
|
|
{
|
|
return Items.Any() ? Items.Select(Value).Average() : double.NaN;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public double GetMode()
|
|
{
|
|
if (!Items.Any())
|
|
{
|
|
return double.NaN;
|
|
}
|
|
|
|
return Items
|
|
.GroupBy(item => Value(item))
|
|
.Select(g => new { Value = g.Key, Count = g.Count() })
|
|
.OrderByDescending(e => e.Count)
|
|
.First()
|
|
.Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// https://en.wikipedia.org/wiki/Simple_linear_regression#Fitting_the_regression_line
|
|
/// </summary>
|
|
public (double a, double b) GetTrend()
|
|
{
|
|
double a = double.NaN, b = double.NaN;
|
|
|
|
var chart = Chart;
|
|
if (chart == null)
|
|
{
|
|
return (a, b);
|
|
}
|
|
|
|
if (Items.Any())
|
|
{
|
|
Func<TItem, double> X;
|
|
Func<TItem, double> Y;
|
|
var vs = chart.GetValueScale(ValueAxisName);
|
|
if (chart.ShouldInvertAxes())
|
|
{
|
|
var categoryAccessor = Category(vs);
|
|
X = e => chart.CategoryScale.Scale(Value(e));
|
|
Y = e => vs.Scale(categoryAccessor(e));
|
|
}
|
|
else
|
|
{
|
|
var categoryAccessor = Category(chart.CategoryScale);
|
|
X = e => chart.CategoryScale.Scale(categoryAccessor(e));
|
|
Y = e => vs.Scale(Value(e));
|
|
}
|
|
|
|
var data = Items.ToList();
|
|
var avgX = data.Select(e => X(e)).Average();
|
|
var avgY = data.Select(e => Y(e)).Average();
|
|
var sumXY = data.Sum(e => (X(e) - avgX) * (Y(e) - avgY));
|
|
if (chart.ShouldInvertAxes())
|
|
{
|
|
var sumYSq = data.Sum(e => (Y(e) - avgY) * (Y(e) - avgY));
|
|
b = sumXY / sumYSq;
|
|
a = avgX - b * avgY;
|
|
}
|
|
else
|
|
{
|
|
var sumXSq = data.Sum(e => (X(e) - avgX) * (X(e) - avgX));
|
|
b = sumXY / sumXSq;
|
|
a = avgY - b * avgX;
|
|
}
|
|
}
|
|
|
|
return (a, b);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IList<Point> GetScaledDataPoints()
|
|
{
|
|
var result = new List<Point>();
|
|
var chart = Chart;
|
|
if (chart == null || Items == null || !Items.Any())
|
|
{
|
|
return result;
|
|
}
|
|
|
|
var vs = chart.GetValueScale(ValueAxisName);
|
|
if (chart.ShouldInvertAxes())
|
|
{
|
|
var categoryAccessor = Category(vs);
|
|
foreach (var item in Items)
|
|
{
|
|
var px = chart.CategoryScale.Scale(Value(item));
|
|
var py = vs.Scale(categoryAccessor(item));
|
|
result.Add(new Point { X = px, Y = py });
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var categoryAccessor = Category(chart.CategoryScale);
|
|
foreach (var item in Items)
|
|
{
|
|
var px = chart.CategoryScale.Scale(categoryAccessor(item));
|
|
var py = vs.Scale(Value(item));
|
|
result.Add(new Point { X = px, Y = py });
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private async Task OnLegendItemClick()
|
|
{
|
|
IsVisible = !IsVisible;
|
|
|
|
var chart = Chart;
|
|
|
|
if (chart?.LegendClick.HasDelegate == true)
|
|
{
|
|
var args = new LegendClickEventArgs
|
|
{
|
|
Data = this.Data,
|
|
Title = GetTitle(),
|
|
IsVisible = IsVisible,
|
|
};
|
|
|
|
await chart.LegendClick.InvokeAsync(args);
|
|
|
|
IsVisible = args.IsVisible;
|
|
}
|
|
|
|
if (chart != null)
|
|
{
|
|
await chart.Refresh();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string GetTitle()
|
|
{
|
|
var chart = Chart;
|
|
if (string.IsNullOrEmpty(Title))
|
|
{
|
|
var index = chart?.Series.IndexOf(this) ?? 0;
|
|
return $"Series {index + 1}";
|
|
}
|
|
|
|
return Title;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the label of the tooltip displayed for this item.
|
|
/// </summary>
|
|
/// <param name="item">The item.</param>
|
|
protected virtual string TooltipLabel(TItem item)
|
|
{
|
|
return GetTitle();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the title of the tooltip displayed for this item.
|
|
/// </summary>
|
|
/// <param name="item">The item.</param>
|
|
protected virtual string TooltipTitle(TItem item)
|
|
{
|
|
var chart = RequireChart();
|
|
var category = Category(chart.CategoryScale);
|
|
return chart.CategoryAxis.Format(chart.CategoryScale, chart.CategoryScale.Value(category(item)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the value of the tooltip displayed for this item.
|
|
/// </summary>
|
|
/// <param name="item">The item.</param>
|
|
/// <returns>System.String.</returns>
|
|
protected virtual string TooltipValue(TItem item)
|
|
{
|
|
var chart = RequireChart();
|
|
var valueAxis = chart.GetValueAxis(ValueAxisName);
|
|
var valueScale = chart.GetValueScale(ValueAxisName);
|
|
return valueAxis.Format(valueScale, valueScale.Value(Value(item)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the X coordinate of the tooltip of the specified item.
|
|
/// </summary>
|
|
/// <param name="item">The item.</param>
|
|
internal virtual double TooltipX(TItem item)
|
|
{
|
|
var chart = RequireChart();
|
|
var category = Category(chart.CategoryScale);
|
|
return chart.CategoryScale.Scale(category(item), true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the Y coordinate of the tooltip of the specified item.
|
|
/// </summary>
|
|
/// <param name="item">The item.</param>
|
|
internal virtual double TooltipY(TItem item)
|
|
{
|
|
var chart = RequireChart();
|
|
return chart.GetValueScale(ValueAxisName).Scale(Value(item), true);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual (object, Point) DataAt(double x, double y)
|
|
{
|
|
if (Items.Any())
|
|
{
|
|
var retObject = Items.Select(item =>
|
|
{
|
|
var distance = Math.Abs(TooltipX(item) - x);
|
|
return new { Item = item, Distance = distance };
|
|
}).Aggregate((a, b) => a.Distance < b.Distance ? a : b).Item;
|
|
|
|
return (retObject!,
|
|
new Point() { X = TooltipX(retObject), Y = TooltipY(retObject)});
|
|
}
|
|
|
|
return (default!, new Point());
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual IEnumerable<ChartDataLabel> GetDataLabels(double offsetX, double offsetY)
|
|
{
|
|
var chart = RequireChart();
|
|
var list = new List<ChartDataLabel>();
|
|
|
|
foreach (var d in Items)
|
|
{
|
|
var value = Value(d);
|
|
list.Add(new ChartDataLabel
|
|
{
|
|
Position = new Point { X = TooltipX(d) + offsetX, Y = TooltipY(d) + offsetY },
|
|
Anchor = new Point { X = TooltipX(d), Y = TooltipY(d) },
|
|
Value = value,
|
|
TextAnchor = "middle",
|
|
Text = chart.GetValueAxis(ValueAxisName).Format(chart.GetValueScale(ValueAxisName), value)
|
|
});
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual IEnumerable<ChartDataLabel> GetDataLabels(double offsetX, double offsetY, DataLabelPosition position)
|
|
{
|
|
if (position == DataLabelPosition.Auto)
|
|
{
|
|
return GetDataLabels(offsetX, offsetY);
|
|
}
|
|
|
|
var chart = RequireChart();
|
|
var list = new List<ChartDataLabel>();
|
|
const double gap = 16;
|
|
|
|
foreach (var d in Items)
|
|
{
|
|
var anchorX = TooltipX(d);
|
|
var anchorY = TooltipY(d);
|
|
var y = position switch
|
|
{
|
|
DataLabelPosition.Top => anchorY - gap,
|
|
DataLabelPosition.Bottom => anchorY + gap,
|
|
_ => anchorY,
|
|
};
|
|
var value = Value(d);
|
|
|
|
list.Add(new ChartDataLabel
|
|
{
|
|
Position = new Point { X = anchorX + offsetX, Y = y + offsetY },
|
|
Anchor = new Point { X = anchorX, Y = anchorY },
|
|
Value = value,
|
|
TextAnchor = "middle",
|
|
Text = chart.GetValueAxis(ValueAxisName).Format(chart.GetValueScale(ValueAxisName), value)
|
|
});
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a color from the specified list of colors. Rotates colors.
|
|
/// </summary>
|
|
/// <param name="index">The index.</param>
|
|
/// <param name="colors">The colors.</param>
|
|
/// <param name="defaultValue">The default value.</param>
|
|
/// <param name="colorRange">The color range value.</param>
|
|
/// <param name="value">The value of the item.</param>
|
|
protected string? PickColor(int index, IEnumerable<string>? colors, string? defaultValue = null, IList<SeriesColorRange>? colorRange = null, double value = 0.0)
|
|
{
|
|
if (colorRange != null)
|
|
{
|
|
var result = colorRange.Where(r => r.Min <= value && r.Max >= value).FirstOrDefault<SeriesColorRange>();
|
|
return result?.Color ?? defaultValue;
|
|
}
|
|
else
|
|
{
|
|
if (colors == null || !colors.Any())
|
|
{
|
|
return defaultValue;
|
|
}
|
|
|
|
return colors.ElementAt(index % colors.Count());
|
|
}
|
|
}
|
|
/// <inheritdoc />
|
|
public void Dispose()
|
|
{
|
|
Chart?.RemoveSeries(this);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task InvokeClick(EventCallback<SeriesClickEventArgs> handler, object data)
|
|
{
|
|
var chart = RequireChart();
|
|
var category = Category(chart.CategoryScale);
|
|
var dataItem = (TItem)data;
|
|
|
|
await handler.InvokeAsync(new SeriesClickEventArgs
|
|
{
|
|
Data = data,
|
|
Title = GetTitle(),
|
|
Category = !string.IsNullOrEmpty(CategoryProperty) ? PropertyAccess.GetValue(data, CategoryProperty) : null,
|
|
Value = !string.IsNullOrEmpty(ValueProperty) ? PropertyAccess.GetValue(data, ValueProperty) : null,
|
|
Point = new SeriesPoint
|
|
{
|
|
Category = category(dataItem),
|
|
Value = Value(dataItem)
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} |