Files
radzen-blazor/Radzen.Blazor/ScaleBase.cs

247 lines
7.6 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
namespace Radzen.Blazor
{
/// <summary>
/// Base class for RadzenChart scales.
/// </summary>
public abstract class ScaleBase
{
/// <summary>
/// Gets or sets the input.
/// </summary>
/// <value>The input.</value>
public ScaleRange Input { get; set; } = new ScaleRange();
/// <summary>
/// Gets or sets the output.
/// </summary>
/// <value>The output.</value>
public ScaleRange Output { get; set; } = new ScaleRange();
/// <summary>
/// Gets the size of the output.
/// </summary>
/// <value>The size of the output.</value>
public double OutputSize
{
get
{
return Math.Abs(Output.End - Output.Start);
}
}
/// <summary>
/// Calculates the number of ticks with the specified distance.
/// </summary>
/// <param name="distance">The distance.</param>
abstract public (double Start, double End, double Step) Ticks(int distance);
/// <summary>
/// The values at which axis ticks are rendered. The default implementation enumerates
/// <see cref="Ticks(int)" /> uniformly; scales with non-uniform intervals (e.g. calendar months) override it.
/// </summary>
/// <param name="distance">The desired distance between ticks in pixels.</param>
public virtual IEnumerable<double> TickValues(int distance)
{
var (start, end, step) = Ticks(distance);
if (step <= 0 || !double.IsFinite(step) || !double.IsFinite(start) || !double.IsFinite(end))
{
yield return start;
yield break;
}
if (IsLogarithmic)
{
if (step <= 1 || start <= 0)
{
yield return start;
yield break;
}
for (var value = start; value <= end; value *= step)
{
yield return value;
}
yield break;
}
for (var value = (decimal)start; value <= (decimal)end; value += (decimal)step)
{
yield return (double)value;
}
}
/// <summary>
/// Converts the specified value to a value from this scale with optional padding.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="padding">Whether to apply padding.</param>
abstract public double Scale(double value, bool padding = false);
/// <summary>
/// Gets a value indicating whether this scale uses logarithmic tick stepping.
/// When true, the tick loop uses multiplicative stepping (idx *= step) instead of additive (idx += step).
/// </summary>
public virtual bool IsLogarithmic => false;
/// <summary>
/// Composes the specified selector.
/// </summary>
/// <typeparam name="TItem">The type of the t item.</typeparam>
/// <param name="selector">The selector.</param>
public virtual Func<TItem, double> Compose<TItem>(Func<TItem, double> selector)
{
return (value) => Scale(selector(value), true);
}
/// <summary>
/// Gets or sets the step.
/// </summary>
/// <value>The step.</value>
public virtual object? Step { get; set; }
/// <summary>
/// Resizes the scale to the specified values.
/// </summary>
/// <param name="min">The minimum.</param>
/// <param name="max">The maximum.</param>
public virtual void Resize(object min, object max)
{
if (min != null)
{
Input.Start = Convert.ToDouble(min, CultureInfo.InvariantCulture);
Round = false;
}
if (max != null)
{
Input.End = Convert.ToDouble(max, CultureInfo.InvariantCulture);
Round = false;
}
}
/// <summary>
/// Returns a "nice" number (closest power of 10).
/// </summary>
/// <param name="range">The range.</param>
/// <param name="round">Wether to round.</param>
public double NiceNumber(double range, bool round)
{
if (range == 0)
{
return 1;
}
var sign = Math.Sign(range);
range = Math.Abs(range);
var exponent = Math.Floor(Math.Log10(range));
var fraction = range / Math.Pow(10, exponent);
double niceFraction;
if (round)
{
if (fraction < 1.5)
{
niceFraction = 1;
}
else if (fraction < 3)
{
niceFraction = 2;
}
else if (fraction < 7)
{
niceFraction = 5;
}
else
{
niceFraction = 10;
}
}
else
{
if (fraction <= 1)
{
niceFraction = 1;
}
else if (fraction <= 2)
{
niceFraction = 2;
}
else if (fraction <= 5)
{
niceFraction = 5;
}
else
{
niceFraction = 10;
}
}
return sign * niceFraction * Math.Pow(10, exponent);
}
/// <summary>
/// Gets or sets the padding.
/// </summary>
/// <value>The padding.</value>
public double Padding { get; set; }
/// <summary>
/// Fits the scale within the distance.
/// </summary>
/// <param name="distance">The distance.</param>
public virtual void Fit(int distance)
{
var ticks = Ticks(distance);
Input.MergeWidth(new ScaleRange { Start = ticks.Start, End = ticks.End });
Round = false;
Step = ticks.Step;
}
/// <summary>
/// Returns a value from the scale.
/// </summary>
/// <param name="value">The value.</param>
public abstract object Value(double value);
/// <summary>
/// Formats the tick value.
/// </summary>
/// <param name="format">The format.</param>
/// <param name="value">The value.</param>
/// <returns>System.String.</returns>
public abstract string FormatTick(string format, object value);
/// <summary>
/// Gets or sets a value indicating whether this <see cref="ScaleBase"/> is round.
/// </summary>
/// <value><c>true</c> if round; otherwise, <c>false</c>.</value>
public bool Round { get; set; } = true;
/// <summary>
/// Determines whether the specified scale is equal to the current one.
/// </summary>
/// <param name="scale">The scale.</param>
/// <returns><c>true</c> if the scales are equal; otherwise, <c>false</c>.</returns>
public bool IsEqualTo(ScaleBase scale)
{
ArgumentNullException.ThrowIfNull(scale);
if (GetType() != scale.GetType())
{
return false;
}
return Input.IsEqualTo(scale.Input) && Output.IsEqualTo(scale.Output);
}
}
}