From 76bd07bb2627a66dc9a9b68527fd76e045f20948 Mon Sep 17 00:00:00 2001 From: Eltee Date: Tue, 14 Nov 2023 04:25:39 -0600 Subject: [PATCH 1/2] Scriptable value based option demo. 4am edition --- .../Pages/Tests/ChartsDataLabelsPage.razor.cs | 4 +- .../ChartDataLabelsOptions.cs | 4 + .../wwwroot/chart.datalabels.js | 7 +- .../ScriptableValueBasedOptions.cs | 195 ++++++++++++++++++ .../ScriptableValueBasedOptionsConverter.cs | 63 ++++++ 5 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 Source/Extensions/Blazorise.Charts/ScriptableValueBasedOptions.cs create mode 100644 Source/Extensions/Blazorise.Charts/ScriptableValueBasedOptionsConverter.cs diff --git a/Demos/Blazorise.Demo/Pages/Tests/ChartsDataLabelsPage.razor.cs b/Demos/Blazorise.Demo/Pages/Tests/ChartsDataLabelsPage.razor.cs index e7abd2ae9b..11c63234b8 100644 --- a/Demos/Blazorise.Demo/Pages/Tests/ChartsDataLabelsPage.razor.cs +++ b/Demos/Blazorise.Demo/Pages/Tests/ChartsDataLabelsPage.razor.cs @@ -182,10 +182,12 @@ public partial class ChartsDataLabelsPage BackgroundColor = BackgroundColors[2], BorderColor = BorderColors[2], Align = "center", - Anchor = "center" + Anchor = "center", + ScriptableFormatter = ScriptableFormatter } }, }; + static Expression> ScriptableFormatter = ( value, context ) => "$ " + value; ChartDataLabelsOptions barDataLabelsOptions = new() { diff --git a/Source/Extensions/Blazorise.Charts.DataLabels/ChartDataLabelsOptions.cs b/Source/Extensions/Blazorise.Charts.DataLabels/ChartDataLabelsOptions.cs index a7605ce2bc..c3355584e1 100644 --- a/Source/Extensions/Blazorise.Charts.DataLabels/ChartDataLabelsOptions.cs +++ b/Source/Extensions/Blazorise.Charts.DataLabels/ChartDataLabelsOptions.cs @@ -55,6 +55,10 @@ public class ChartDataLabelsOptions [JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )] public ChartMathFormatter? Formatter { get; set; } + [JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )] + [JsonConverter( typeof( ScriptableValueBasedOptionsConverter ) )] + public ScriptableValueBasedOptions ScriptableFormatter { get; set; } + [JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )] public object Labels { get; set; } diff --git a/Source/Extensions/Blazorise.Charts.DataLabels/wwwroot/chart.datalabels.js b/Source/Extensions/Blazorise.Charts.DataLabels/wwwroot/chart.datalabels.js index e95648dcbe..b3405cb05b 100644 --- a/Source/Extensions/Blazorise.Charts.DataLabels/wwwroot/chart.datalabels.js +++ b/Source/Extensions/Blazorise.Charts.DataLabels/wwwroot/chart.datalabels.js @@ -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]); + } else { + options[key] = parseFunction(options[key]); + } + } }); diff --git a/Source/Extensions/Blazorise.Charts/ScriptableValueBasedOptions.cs b/Source/Extensions/Blazorise.Charts/ScriptableValueBasedOptions.cs new file mode 100644 index 0000000000..9d2af8cc18 --- /dev/null +++ b/Source/Extensions/Blazorise.Charts/ScriptableValueBasedOptions.cs @@ -0,0 +1,195 @@ +#region Using directives +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +#endregion + +namespace Blazorise.Charts; + +/// +/// 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 ). +/// +/// A value that is returned from a function. +/// The current label value of the dataset +/// A context representing contextual information. +public class ScriptableValueBasedOptions + : IEquatable> + where TContext : ScriptableOptionsContext +{ + #region Members + + private readonly TOptions value; + + private readonly Expression> scriptableValue; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of which represents a single value. + /// + /// The single value this should represent. + public ScriptableValueBasedOptions( TOptions value ) + { + this.value = value; + + IsScriptable = false; + } + + /// + /// Creates a new instance of which represents a scriptable value. + /// + /// The scriptable value this should represent. + public ScriptableValueBasedOptions( Expression> scriptableValue ) + { + this.scriptableValue = scriptableValue; + + IsScriptable = true; + } + + #endregion + + #region Operators + + /// + /// Implicitly wraps a single value of to a new instance of . + /// + /// The single value to wrap. + //public static implicit operator ScriptableValueBasedOptions( TOptions value ) + //{ + // return new ScriptableValueBasedOptions( value ); + //} + + /// + /// Implicitly wraps an expression of to a new instance of . + /// + /// The expression values to wrap. + public static implicit operator ScriptableValueBasedOptions( Expression> scriptableValue ) + { + return new ScriptableValueBasedOptions( scriptableValue ); + } + + /// + /// Determines whether two specified instances contain the same value. + /// + /// The first to compare + /// The second to compare + /// true if the value of a is the same as the value of b; otherwise, false. + public static bool operator ==( ScriptableValueBasedOptions a, ScriptableValueBasedOptions b ) => a.Equals( b ); + + /// + /// Determines whether two specified instances contain different values. + /// + /// The first to compare + /// The second to compare + /// true if the value of a is different from the value of b; otherwise, false. + public static bool operator !=( ScriptableValueBasedOptions a, ScriptableValueBasedOptions b ) => !( a == b ); + + #endregion + + #region Methods + + /// + /// Determines whether the specified instance is considered equal to the current instance. + /// + /// The to compare with. + /// true if the objects are considered equal; otherwise, false. + public bool Equals( ScriptableValueBasedOptions other ) + { + if ( IsScriptable != other.IsScriptable ) + return false; + + if ( IsScriptable ) + { + return ScriptableValue == other.ScriptableValue; + } + else + { + return EqualityComparer.Default.Equals( Value, other.Value ); + } + } + + /// + /// Determines whether the specified object instance is considered equal to the current instance. + /// + /// The object to compare with. + /// true if the objects are considered equal; otherwise, false. + public override bool Equals( object obj ) + { + if ( obj == null ) + return false; + + if ( obj is ScriptableValueBasedOptions option ) + { + return Equals( option ); + } + else + { + if ( IsScriptable ) + { + return ScriptableValue.Equals( obj ); + } + else + { + return Value.Equals( obj ); + } + } + } + + /// + /// Returns the hash of the underlying object. + /// + /// The hash of the underlying object. + public override int GetHashCode() + { + var hashCode = -506568782; + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode( Value ); + hashCode = hashCode * -1521134295 + EqualityComparer>>.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; + + /// + /// Gets the value indicating whether the option wrapped in this is scriptable. + /// + public bool IsScriptable { get; } + + /// + /// The single value represented by this instance. + /// + public TOptions Value + { + get + { + if ( IsScriptable ) + throw new InvalidOperationException( "This instance represents an scriptable values. The scriptable values is not available." ); + + return value; + } + } + + /// + /// The scriptable value represented by this instance. + /// + public Expression> ScriptableValue + { + get + { + if ( !IsScriptable ) + throw new InvalidOperationException( "This instance represents a single value. The scriptable values is not available." ); + + return scriptableValue; + } + } + + #endregion +} \ No newline at end of file diff --git a/Source/Extensions/Blazorise.Charts/ScriptableValueBasedOptionsConverter.cs b/Source/Extensions/Blazorise.Charts/ScriptableValueBasedOptionsConverter.cs new file mode 100644 index 0000000000..50d520db0c --- /dev/null +++ b/Source/Extensions/Blazorise.Charts/ScriptableValueBasedOptionsConverter.cs @@ -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 : JsonConverter> + where TContext : ScriptableOptionsContext +{ + public override ScriptableValueBasedOptions Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options ) + { + // I only need serialization currently + throw new NotImplementedException(); + } + + public override void Write( Utf8JsonWriter writer, ScriptableValueBasedOptions 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( Expression 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( StringBuilder sb, Expression 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 ); + } + } +} \ No newline at end of file From 81854820e4a0dc65bb31cc69a7eceee18e4ea150 Mon Sep 17 00:00:00 2001 From: Eltee Date: Tue, 14 Nov 2023 18:28:52 -0600 Subject: [PATCH 2/2] Soft code formatter logic instead of using Lambda2Js --- .../Pages/Tests/ChartsDataLabelsPage.razor.cs | 3 +- .../ChartDataLabelsOptions.cs | 3 +- .../wwwroot/chart.datalabels.js | 2 +- .../ScriptableValueBasedOptions.cs | 195 ------------------ .../ScriptableValueBasedOptionsConverter.cs | 63 ------ 5 files changed, 3 insertions(+), 263 deletions(-) delete mode 100644 Source/Extensions/Blazorise.Charts/ScriptableValueBasedOptions.cs delete mode 100644 Source/Extensions/Blazorise.Charts/ScriptableValueBasedOptionsConverter.cs diff --git a/Demos/Blazorise.Demo/Pages/Tests/ChartsDataLabelsPage.razor.cs b/Demos/Blazorise.Demo/Pages/Tests/ChartsDataLabelsPage.razor.cs index 11c63234b8..f495aa2183 100644 --- a/Demos/Blazorise.Demo/Pages/Tests/ChartsDataLabelsPage.razor.cs +++ b/Demos/Blazorise.Demo/Pages/Tests/ChartsDataLabelsPage.razor.cs @@ -183,11 +183,10 @@ public partial class ChartsDataLabelsPage BorderColor = BorderColors[2], Align = "center", Anchor = "center", - ScriptableFormatter = ScriptableFormatter + SoftCodeFormatter = "function(value, context) { return \"$\" + value; }" } }, }; - static Expression> ScriptableFormatter = ( value, context ) => "$ " + value; ChartDataLabelsOptions barDataLabelsOptions = new() { diff --git a/Source/Extensions/Blazorise.Charts.DataLabels/ChartDataLabelsOptions.cs b/Source/Extensions/Blazorise.Charts.DataLabels/ChartDataLabelsOptions.cs index c3355584e1..dbe54a8c6d 100644 --- a/Source/Extensions/Blazorise.Charts.DataLabels/ChartDataLabelsOptions.cs +++ b/Source/Extensions/Blazorise.Charts.DataLabels/ChartDataLabelsOptions.cs @@ -56,8 +56,7 @@ public class ChartDataLabelsOptions public ChartMathFormatter? Formatter { get; set; } [JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )] - [JsonConverter( typeof( ScriptableValueBasedOptionsConverter ) )] - public ScriptableValueBasedOptions ScriptableFormatter { get; set; } + public string SoftCodeFormatter { get; set; } [JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )] public object Labels { get; set; } diff --git a/Source/Extensions/Blazorise.Charts.DataLabels/wwwroot/chart.datalabels.js b/Source/Extensions/Blazorise.Charts.DataLabels/wwwroot/chart.datalabels.js index b3405cb05b..9ff3f4a2cc 100644 --- a/Source/Extensions/Blazorise.Charts.DataLabels/wwwroot/chart.datalabels.js +++ b/Source/Extensions/Blazorise.Charts.DataLabels/wwwroot/chart.datalabels.js @@ -52,7 +52,7 @@ function compileDatasetsOptionsCallbacks(options) { Object.keys(options).forEach(function (key) { if (options[key] && options[key].startsWith("function")) { - if (key === 'scriptableFormatter') { + if (key === 'softCodeFormatter') { options['formatter'] = parseFunction(options[key]); } else { options[key] = parseFunction(options[key]); diff --git a/Source/Extensions/Blazorise.Charts/ScriptableValueBasedOptions.cs b/Source/Extensions/Blazorise.Charts/ScriptableValueBasedOptions.cs deleted file mode 100644 index 9d2af8cc18..0000000000 --- a/Source/Extensions/Blazorise.Charts/ScriptableValueBasedOptions.cs +++ /dev/null @@ -1,195 +0,0 @@ -#region Using directives -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -#endregion - -namespace Blazorise.Charts; - -/// -/// 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 ). -/// -/// A value that is returned from a function. -/// The current label value of the dataset -/// A context representing contextual information. -public class ScriptableValueBasedOptions - : IEquatable> - where TContext : ScriptableOptionsContext -{ - #region Members - - private readonly TOptions value; - - private readonly Expression> scriptableValue; - - #endregion - - #region Constructors - - /// - /// Creates a new instance of which represents a single value. - /// - /// The single value this should represent. - public ScriptableValueBasedOptions( TOptions value ) - { - this.value = value; - - IsScriptable = false; - } - - /// - /// Creates a new instance of which represents a scriptable value. - /// - /// The scriptable value this should represent. - public ScriptableValueBasedOptions( Expression> scriptableValue ) - { - this.scriptableValue = scriptableValue; - - IsScriptable = true; - } - - #endregion - - #region Operators - - /// - /// Implicitly wraps a single value of to a new instance of . - /// - /// The single value to wrap. - //public static implicit operator ScriptableValueBasedOptions( TOptions value ) - //{ - // return new ScriptableValueBasedOptions( value ); - //} - - /// - /// Implicitly wraps an expression of to a new instance of . - /// - /// The expression values to wrap. - public static implicit operator ScriptableValueBasedOptions( Expression> scriptableValue ) - { - return new ScriptableValueBasedOptions( scriptableValue ); - } - - /// - /// Determines whether two specified instances contain the same value. - /// - /// The first to compare - /// The second to compare - /// true if the value of a is the same as the value of b; otherwise, false. - public static bool operator ==( ScriptableValueBasedOptions a, ScriptableValueBasedOptions b ) => a.Equals( b ); - - /// - /// Determines whether two specified instances contain different values. - /// - /// The first to compare - /// The second to compare - /// true if the value of a is different from the value of b; otherwise, false. - public static bool operator !=( ScriptableValueBasedOptions a, ScriptableValueBasedOptions b ) => !( a == b ); - - #endregion - - #region Methods - - /// - /// Determines whether the specified instance is considered equal to the current instance. - /// - /// The to compare with. - /// true if the objects are considered equal; otherwise, false. - public bool Equals( ScriptableValueBasedOptions other ) - { - if ( IsScriptable != other.IsScriptable ) - return false; - - if ( IsScriptable ) - { - return ScriptableValue == other.ScriptableValue; - } - else - { - return EqualityComparer.Default.Equals( Value, other.Value ); - } - } - - /// - /// Determines whether the specified object instance is considered equal to the current instance. - /// - /// The object to compare with. - /// true if the objects are considered equal; otherwise, false. - public override bool Equals( object obj ) - { - if ( obj == null ) - return false; - - if ( obj is ScriptableValueBasedOptions option ) - { - return Equals( option ); - } - else - { - if ( IsScriptable ) - { - return ScriptableValue.Equals( obj ); - } - else - { - return Value.Equals( obj ); - } - } - } - - /// - /// Returns the hash of the underlying object. - /// - /// The hash of the underlying object. - public override int GetHashCode() - { - var hashCode = -506568782; - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode( Value ); - hashCode = hashCode * -1521134295 + EqualityComparer>>.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; - - /// - /// Gets the value indicating whether the option wrapped in this is scriptable. - /// - public bool IsScriptable { get; } - - /// - /// The single value represented by this instance. - /// - public TOptions Value - { - get - { - if ( IsScriptable ) - throw new InvalidOperationException( "This instance represents an scriptable values. The scriptable values is not available." ); - - return value; - } - } - - /// - /// The scriptable value represented by this instance. - /// - public Expression> ScriptableValue - { - get - { - if ( !IsScriptable ) - throw new InvalidOperationException( "This instance represents a single value. The scriptable values is not available." ); - - return scriptableValue; - } - } - - #endregion -} \ No newline at end of file diff --git a/Source/Extensions/Blazorise.Charts/ScriptableValueBasedOptionsConverter.cs b/Source/Extensions/Blazorise.Charts/ScriptableValueBasedOptionsConverter.cs deleted file mode 100644 index 50d520db0c..0000000000 --- a/Source/Extensions/Blazorise.Charts/ScriptableValueBasedOptionsConverter.cs +++ /dev/null @@ -1,63 +0,0 @@ -#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 : JsonConverter> - where TContext : ScriptableOptionsContext -{ - public override ScriptableValueBasedOptions Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options ) - { - // I only need serialization currently - throw new NotImplementedException(); - } - - public override void Write( Utf8JsonWriter writer, ScriptableValueBasedOptions 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( Expression 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( StringBuilder sb, Expression 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 ); - } - } -} \ No newline at end of file