Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scriptable value based option demo. 4am edition #5146

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,12 @@ public partial class ChartsDataLabelsPage
BackgroundColor = BackgroundColors[2],
BorderColor = BorderColors[2],
Align = "center",
Anchor = "center"
Anchor = "center",
ScriptableFormatter = ScriptableFormatter
}
},
};
static Expression<Func<object, ScriptableOptionsContext, string>> ScriptableFormatter = ( value, context ) => "$ " + value;
Eltee-Taiwo marked this conversation as resolved.
Show resolved Hide resolved

ChartDataLabelsOptions barDataLabelsOptions = new()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ public class ChartDataLabelsOptions
[JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )]
public ChartMathFormatter? Formatter { get; set; }

[JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )]
[JsonConverter( typeof( ScriptableValueBasedOptionsConverter<string, object, ScriptableOptionsContext> ) )]
public ScriptableValueBasedOptions<string, object, ScriptableOptionsContext> ScriptableFormatter { get; set; }

[JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )]
public object Labels { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ function compileDatasetsOptionsCallbacks(options) {

Object.keys(options).forEach(function (key) {
if (options[key] && options[key].startsWith("function")) {
options[key] = parseFunction(options[key]);
if (key === 'scriptableFormatter') {
options['formatter'] = parseFunction(options[key]);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hacky way to make it backwards compatible and be able to keep the existing int rounding rules.

} else {
options[key] = parseFunction(options[key]);
}

}
});

Expand Down
195 changes: 195 additions & 0 deletions Source/Extensions/Blazorise.Charts/ScriptableValueBasedOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#region Using directives
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
#endregion

namespace Blazorise.Charts;

/// <summary>
/// Scriptable options also accept a function which is called for each of the underlying data values and that takes the unique
/// argument context representing contextual information (see <see cref="ScriptableOptionsContext"/>).
/// </summary>
/// <typeparam name="TOptions">A value that is returned from a function.</typeparam>
/// <typeparam name="TValue">The current label value of the dataset</typeparam>
/// <typeparam name="TContext">A context representing contextual information.</typeparam>
public class ScriptableValueBasedOptions<TOptions, TValue, TContext>
Eltee-Taiwo marked this conversation as resolved.
Show resolved Hide resolved
: IEquatable<ScriptableValueBasedOptions<TOptions, TValue, TContext>>
where TContext : ScriptableOptionsContext
{
#region Members

private readonly TOptions value;

private readonly Expression<Func<TValue, TContext, TOptions>> scriptableValue;

#endregion

#region Constructors

/// <summary>
/// Creates a new instance of <see cref="ScriptableValueBasedOptions{TOptions, TValue, TContext}"/> which represents a single value.
/// </summary>
/// <param name="value">The single value this <see cref="ScriptableValueBasedOptions{TOptions, TValue, TContext}"/> should represent.</param>
public ScriptableValueBasedOptions( TOptions value )
{
this.value = value;

IsScriptable = false;
}

/// <summary>
/// Creates a new instance of <see cref="ScriptableValueBasedOptions{TOptions, TValue, TContext}"/> which represents a scriptable value.
/// </summary>
/// <param name="scriptableValue">The scriptable value this <see cref="ScriptableValueBasedOptions{TOptions, TValue, TContext}"/> should represent.</param>
public ScriptableValueBasedOptions( Expression<Func<TValue, TContext, TOptions>> scriptableValue )
{
this.scriptableValue = scriptableValue;

IsScriptable = true;
}

#endregion

#region Operators

/// <summary>
/// Implicitly wraps a single value of <typeparamref name="TOptions"/> to a new instance of <see cref="ScriptableValueBasedOptions{TOptions, TValue, TContext}"/>.
/// </summary>
/// <param name="value">The single value to wrap.</param>
//public static implicit operator ScriptableValueBasedOptions<TOptions, TValue, TContext>( TOptions value )
//{
// return new ScriptableValueBasedOptions<TOptions, TValue, TContext>( value );
//}

/// <summary>
/// Implicitly wraps an expression of <typeparamref name="TOptions"/> to a new instance of <see cref="ScriptableValueBasedOptions{TOptions, TValue, TContext}"/>.
/// </summary>
/// <param name="scriptableValue">The expression values to wrap.</param>
public static implicit operator ScriptableValueBasedOptions<TOptions, TValue, TContext>( Expression<Func<TValue, TContext, TOptions>> scriptableValue )
{
return new ScriptableValueBasedOptions<TOptions, TValue, TContext>( scriptableValue );
}

/// <summary>
/// Determines whether two specified <see cref="ScriptableValueBasedOptions{TOptions, TValue, TContext}"/> instances contain the same value.
/// </summary>
/// <param name="a">The first <see cref="ScriptableValueBasedOptions{TOptions, TValue, TContext}"/> to compare</param>
/// <param name="b">The second <see cref="ScriptableValueBasedOptions{TOptions, TValue, TContext}"/> to compare</param>
/// <returns>true if the value of a is the same as the value of b; otherwise, false.</returns>
public static bool operator ==( ScriptableValueBasedOptions<TOptions, TValue, TContext> a, ScriptableValueBasedOptions<TOptions, TValue, TContext> b ) => a.Equals( b );

/// <summary>
/// Determines whether two specified <see cref="ScriptableValueBasedOptions{TOptions, TValue, TContext}"/> instances contain different values.
/// </summary>
/// <param name="a">The first <see cref="ScriptableValueBasedOptions{TOptions, TValue, TContext}"/> to compare</param>
/// <param name="b">The second <see cref="ScriptableValueBasedOptions{TOptions, TValue, TContext}"/> to compare</param>
/// <returns>true if the value of a is different from the value of b; otherwise, false.</returns>
public static bool operator !=( ScriptableValueBasedOptions<TOptions, TValue, TContext> a, ScriptableValueBasedOptions<TOptions, TValue, TContext> b ) => !( a == b );

#endregion

#region Methods

/// <summary>
/// Determines whether the specified <see cref="ScriptableValueBasedOptions{TOptions, TValue, TContext}"/> instance is considered equal to the current instance.
/// </summary>
/// <param name="other">The <see cref="ScriptableValueBasedOptions{TOptions, TValue, TContext}"/> to compare with.</param>
/// <returns>true if the objects are considered equal; otherwise, false.</returns>
public bool Equals( ScriptableValueBasedOptions<TOptions, TValue, TContext> other )
{
if ( IsScriptable != other.IsScriptable )
return false;

if ( IsScriptable )
{
return ScriptableValue == other.ScriptableValue;
}
else
{
return EqualityComparer<TOptions>.Default.Equals( Value, other.Value );
}
}

/// <summary>
/// Determines whether the specified object instance is considered equal to the current instance.
/// </summary>
/// <param name="obj">The object to compare with.</param>
/// <returns>true if the objects are considered equal; otherwise, false.</returns>
public override bool Equals( object obj )
{
if ( obj == null )
return false;

if ( obj is ScriptableValueBasedOptions<TOptions, TValue, TContext> option )
{
return Equals( option );
}
else
{
if ( IsScriptable )
{
return ScriptableValue.Equals( obj );
}
else
{
return Value.Equals( obj );
}
}
}

/// <summary>
/// Returns the hash of the underlying object.
/// </summary>
/// <returns>The hash of the underlying object.</returns>
public override int GetHashCode()
{
var hashCode = -506568782;
hashCode = hashCode * -1521134295 + EqualityComparer<TOptions>.Default.GetHashCode( Value );
hashCode = hashCode * -1521134295 + EqualityComparer<Expression<Func<TValue, TContext, TOptions>>>.Default.GetHashCode( ScriptableValue );
hashCode = hashCode * -1521134295 + IsScriptable.GetHashCode();
return hashCode;
}

#endregion

#region Properties

// for serialization, there has to be a cast to object anyway
internal object BoxedValue => IsScriptable ? ScriptableValue : Value;

/// <summary>
/// Gets the value indicating whether the option wrapped in this <see cref="ScriptableValueBasedOptions{TOptions, TValue, TContext}"/> is scriptable.
/// </summary>
public bool IsScriptable { get; }

/// <summary>
/// The single value represented by this instance.
/// </summary>
public TOptions Value
{
get
{
if ( IsScriptable )
throw new InvalidOperationException( "This instance represents an scriptable values. The scriptable values is not available." );

return value;
}
}

/// <summary>
/// The scriptable value represented by this instance.
/// </summary>
public Expression<Func<TValue, TContext, TOptions>> ScriptableValue
{
get
{
if ( !IsScriptable )
throw new InvalidOperationException( "This instance represents a single value. The scriptable values is not available." );

return scriptableValue;
}
}

#endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#region Using directives
using System;
using System.Linq.Expressions;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Lambda2Js;
#endregion

namespace Blazorise.Charts;

public class ScriptableValueBasedOptionsConverter<TOptions, TValue, TContext> : JsonConverter<ScriptableValueBasedOptions<TOptions, TValue, TContext>>
Eltee-Taiwo marked this conversation as resolved.
Show resolved Hide resolved
where TContext : ScriptableOptionsContext
{
public override ScriptableValueBasedOptions<TOptions, TValue, TContext> Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options )
{
// I only need serialization currently
throw new NotImplementedException();
}

public override void Write( Utf8JsonWriter writer, ScriptableValueBasedOptions<TOptions, TValue, TContext> value, JsonSerializerOptions options )
{
if ( value.IsScriptable && value.ScriptableValue != null )
{
var jsBody = value.ScriptableValue.CompileToJavascript( new JavascriptCompilationOptions( JsCompilationFlags.BodyOnly ) );

var result = BuildFunction( value.ScriptableValue, jsBody );

writer.WriteStringValue( result );
}
else if ( value.Value != null )
JsonSerializer.Serialize( writer, value.Value, value.Value.GetType(), options );
}

private static string BuildFunction<T>( Expression<T> expression, string jsBody )
{
var sb = new StringBuilder();

sb.Append( "function(" );
BuildFunctionParameters( sb, expression );
sb.Append( ") {" );

sb.Append( "return " );
sb.Append( jsBody );
sb.Append( "; }" );

return sb.ToString();
}

private static void BuildFunctionParameters<T>( StringBuilder sb, Expression<T> value )
{
if ( value.Parameters == null || value.Parameters.Count == 0 )
return;

for ( int i = 0; i < value.Parameters.Count; i++ )
{
if ( i > 0 )
sb.Append( ", " );

sb.Append( value.Parameters[i].Name );
}
}
}