diff --git a/docs/BlazorApexCharts.Docs/Components/ChartTypes/HeatmapCharts/DataLabelColors.razor b/docs/BlazorApexCharts.Docs/Components/ChartTypes/HeatmapCharts/DataLabelColors.razor new file mode 100644 index 00000000..6d42a984 --- /dev/null +++ b/docs/BlazorApexCharts.Docs/Components/ChartTypes/HeatmapCharts/DataLabelColors.razor @@ -0,0 +1,48 @@ + + + + @foreach (var source in incidents.GroupBy(e => e.Source).OrderBy(e => e.Key)) + { + + } + + + + +@code { + private List incidents { get; set; } = SampleData.GetSupportIncidents(); + private ApexChartOptions options = new ApexChartOptions + { + DataLabels = new DataLabels + { + Style = new DataLabelsStyle + { + Colors = ["function(opts) { return (opts.series[opts.seriesIndex][opts.dataPointIndex] < 30) ? '#00f' : '#f00'; }"], + }, + }, + }; + + private string GetColor(IncidentSource source) + { + switch (source) + { + case IncidentSource.Internal: + return "#FF0000"; + case IncidentSource.ThirdParty: + return "#0000FF"; + case IncidentSource.Integration: + return "#FF00FF"; + default: + return "#008FFB"; + } + } +} \ No newline at end of file diff --git a/docs/BlazorApexCharts.Docs/Components/ChartTypes/HeatmapCharts/HeatmapCharts.razor b/docs/BlazorApexCharts.Docs/Components/ChartTypes/HeatmapCharts/HeatmapCharts.razor index 5c21fcf1..d9b036a9 100644 --- a/docs/BlazorApexCharts.Docs/Components/ChartTypes/HeatmapCharts/HeatmapCharts.razor +++ b/docs/BlazorApexCharts.Docs/Components/ChartTypes/HeatmapCharts/HeatmapCharts.razor @@ -20,6 +20,12 @@ + + + + + + diff --git a/src/Blazor-ApexCharts/Internal/ChartUtilities.cs b/src/Blazor-ApexCharts/Internal/ChartUtilities.cs new file mode 100644 index 00000000..f0840588 --- /dev/null +++ b/src/Blazor-ApexCharts/Internal/ChartUtilities.cs @@ -0,0 +1,65 @@ +using System; +using System.Text.RegularExpressions; + +namespace ApexCharts.Internal; + +/// +/// Provides JavaScript related helper utilities for ApexCharts internal processing. +/// +internal static class ChartUtilities +{ + /// + /// Regex that matches the beginning of a JavaScript function or arrow function (including optional leading comments). + /// + private static readonly Regex JsFunctionStartRegex = new( + @"^\s*(?:/\*[\s\S]*?\*/\s*|//[^\n]*\n\s*)*(?:async\s+)?(?: + function\b + | \([^)]*\)\s*=> # (args) => + | [A-Za-z_$][\w$]*\s*=> # identifier => + )", + RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnorePatternWhitespace); + + /// + /// Regex that matches immediately-invoked function expressions (IIFE) using the traditional function keyword. + /// + private static readonly Regex IifeFunctionRegex = new( + @"^\s*\(\s*(?:async\s+)?function\b", + RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); + + /// + /// Regex that matches immediately-invoked arrow function expressions. + /// + private static readonly Regex IifeArrowRegex = new( + @"^\s*\(\s*(?:async\s+)?\([^)]*\)\s*=>", + RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant); + + /// + /// Determines whether the provided string appears to represent a JavaScript function + /// (standard function declaration, arrow function, or IIFE). + /// + /// The string to inspect. + /// + /// if the string structurally resembles a JavaScript function; + /// otherwise, . + /// + internal static bool IsJavaScriptFunction(string candidate) + { + if (string.IsNullOrWhiteSpace(candidate)) + return false; + + // Quick cheap pre-filter: avoid regex if no plausible tokens + if (!(candidate.Contains("function", StringComparison.OrdinalIgnoreCase) || + candidate.Contains("=>", StringComparison.Ordinal))) + return false; + + // Direct structural check at start + if (JsFunctionStartRegex.IsMatch(candidate)) + return true; + + // IIFE patterns: (function(...) {...})(), (() => {...})(), etc. + if (IifeFunctionRegex.IsMatch(candidate) || IifeArrowRegex.IsMatch(candidate)) + return true; + + return false; + } +} \ No newline at end of file diff --git a/src/Blazor-ApexCharts/Internal/Converters/ListStringOrFunctionConverter.cs b/src/Blazor-ApexCharts/Internal/Converters/ListStringOrFunctionConverter.cs new file mode 100644 index 00000000..781ceb14 --- /dev/null +++ b/src/Blazor-ApexCharts/Internal/Converters/ListStringOrFunctionConverter.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ApexCharts.Internal; + +/// +/// Converter for lists of strings that detects entries containing a JavaScript function definition +/// (case-insensitive search for the word "function") and serializes those as an object with the "@eval" key +/// so that ApexCharts interprets them as executable functions instead of plain strings. +/// +/// Output behavior: +/// - Normal strings are written as JSON string elements inside the array. +/// - Strings representing functions are written as: +/// { "@eval": "function(x) { return x; }" } +/// +/// Example: +/// C# input: +/// +/// new List<string> +/// { +/// "Label 1", +/// "function(w) { return w; }", +/// "Another label" +/// }; +/// +/// +/// JSON output: +/// +/// [ +/// "Label 1", +/// { "@eval": "function(w) { return w; }" }, +/// "Another label" +/// ] +/// +/// +/// Note: Deserialization (Read) is not implemented because this converter is intended only for outgoing +/// serialization to the client. +/// +internal class ListStringOrFunctionConverter : JsonConverter> +{ + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(List); + + public override List Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options) + { + writer.WriteStartArray(); + + foreach (var item in value) + { + if (ChartUtilities.IsJavaScriptFunction(item)) + { + writer.WriteStartObject(); + writer.WritePropertyName("@eval"); + JsonSerializer.Serialize(writer, item, options); + writer.WriteEndObject(); + } + else + { + writer.WriteStringValue(item); + } + } + + writer.WriteEndArray(); + } +} \ No newline at end of file diff --git a/src/Blazor-ApexCharts/Models/ApexChartOptions.cs b/src/Blazor-ApexCharts/Models/ApexChartOptions.cs index 836ec23f..7237485f 100644 --- a/src/Blazor-ApexCharts/Models/ApexChartOptions.cs +++ b/src/Blazor-ApexCharts/Models/ApexChartOptions.cs @@ -1856,8 +1856,36 @@ public class DataLabelsBackground public class DataLabelsStyle { /// - /// Fore colors for the dataLabels. Accepts an array of string colors (['#333', '#999']) or an array of functions ([function(opts) { return '#333' }]) (Each index in the array corresponds to the series). + /// Fore colors for the dataLabels (each index in the array corresponds to the series). /// + /// + /// Accepts: + /// + /// An array of string colors (e.g. { "#333", "#999" }) + /// An array of JavaScript functions serialized as strings (e.g. { "function(opts) { return '#333' }" }) + /// + /// Each index in the array corresponds to the series index. + /// Examples: + /// + /// // Using static colors + /// var style = new DataLabelsStyle + /// { + /// Colors = new List<string> { "#333", "#999" } + /// }; + /// + /// // Using JavaScript functions (strings will be passed through the converter) + /// var styleWithFunctions = new DataLabelsStyle + /// { + /// Colors = new List<string> + /// { + /// "function(opts) { return '#333'; }", + /// "function(opts) { return opts.seriesIndex % 2 ? '#999' : '#555'; }" + /// } + /// }; + /// + /// For short inline snippets use the inline code tag, e.g. "#333". + /// + [JsonConverter(typeof(ListStringOrFunctionConverter))] public List Colors { get; set; } ///