mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-07-08 18:16:08 +00:00
RadzenContourSeries and RadzenHeatmapSeries added
This commit is contained in:
142
Radzen.Blazor.Tests/ContourSeriesTests.cs
Normal file
142
Radzen.Blazor.Tests/ContourSeriesTests.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Bunit;
|
||||
using Xunit;
|
||||
using static Radzen.Blazor.Tests.ChartTestHelper;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
public class ContourSeriesTests
|
||||
{
|
||||
private class ContourItem
|
||||
{
|
||||
public double X { get; set; }
|
||||
public double Y { get; set; }
|
||||
public double Value { get; set; }
|
||||
}
|
||||
|
||||
// A 3x3 grid with a radial gradient centred at (1,1)
|
||||
private static ContourItem[] Grid3x3
|
||||
{
|
||||
get
|
||||
{
|
||||
var items = new List<ContourItem>();
|
||||
for (var ix = 0; ix < 3; ix++)
|
||||
{
|
||||
for (var iy = 0; iy < 3; iy++)
|
||||
{
|
||||
var dx = ix - 1;
|
||||
var dy = iy - 1;
|
||||
items.Add(new ContourItem { X = ix, Y = iy, Value = 10 - (dx * dx + dy * dy) });
|
||||
}
|
||||
}
|
||||
return items.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static IList<SeriesColorRange> Ranges => new[]
|
||||
{
|
||||
new SeriesColorRange { Min = 0, Max = 7, Color = "#0000ff" },
|
||||
new SeriesColorRange { Min = 7, Max = 9, Color = "#00ff00" },
|
||||
new SeriesColorRange { Min = 9, Max = 100, Color = "#ff0000" },
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async System.Threading.Tasks.Task ContourSeries_Renders_FilledPolygons()
|
||||
{
|
||||
using var ctx = CreateChartContext();
|
||||
|
||||
var chart = ctx.RenderComponent<RadzenChart>(p => p
|
||||
.AddChildContent<RadzenContourSeries<ContourItem>>(s => s
|
||||
.Add(x => x.CategoryProperty, nameof(ContourItem.X))
|
||||
.Add(x => x.ValueProperty, nameof(ContourItem.Y))
|
||||
.Add(x => x.IntensityProperty, nameof(ContourItem.Value))
|
||||
.Add(x => x.ColorRange, Ranges)
|
||||
.Add(x => x.Data, Grid3x3)));
|
||||
|
||||
await chart.InvokeAsync(() => chart.Instance.Resize(400, 300));
|
||||
|
||||
Assert.Contains("rz-contour-series rz-series-0", chart.Markup);
|
||||
var polyCount = Regex.Matches(chart.Markup, "<polygon ").Count;
|
||||
Assert.True(polyCount > 0, $"Expected filled polygons, got {polyCount}");
|
||||
// All three band colors should appear somewhere
|
||||
Assert.Contains("#0000ff", chart.Markup);
|
||||
Assert.Contains("#00ff00", chart.Markup);
|
||||
Assert.Contains("#ff0000", chart.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async System.Threading.Tasks.Task ContourSeries_ShowLines_EmitsLineElements()
|
||||
{
|
||||
using var ctx = CreateChartContext();
|
||||
|
||||
var chart = ctx.RenderComponent<RadzenChart>(p => p
|
||||
.AddChildContent<RadzenContourSeries<ContourItem>>(s => s
|
||||
.Add(x => x.CategoryProperty, nameof(ContourItem.X))
|
||||
.Add(x => x.ValueProperty, nameof(ContourItem.Y))
|
||||
.Add(x => x.IntensityProperty, nameof(ContourItem.Value))
|
||||
.Add(x => x.ColorRange, Ranges)
|
||||
.Add(x => x.ShowLines, true)
|
||||
.Add(x => x.LineColor, "#000")
|
||||
.Add(x => x.Data, Grid3x3)));
|
||||
|
||||
await chart.InvokeAsync(() => chart.Instance.Resize(400, 300));
|
||||
|
||||
var lineCount = Regex.Matches(chart.Markup, "<line ").Count;
|
||||
Assert.True(lineCount > 0, $"Expected iso-lines when ShowLines=true, got {lineCount}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContourSeries_ClipLow_RemovesVerticesBelowThreshold()
|
||||
{
|
||||
var triangle = new List<RadzenContourSeries<ContourItem>.Vertex>
|
||||
{
|
||||
new(0, 0, 0),
|
||||
new(1, 0, 10),
|
||||
new(0, 1, 5),
|
||||
};
|
||||
|
||||
var clipped = RadzenContourSeries<ContourItem>.ClipLow(triangle, 5);
|
||||
|
||||
// The vertex at (0,0,0) is below 5 and gets removed; two crossings are added.
|
||||
Assert.True(clipped.Count >= 3);
|
||||
Assert.All(clipped, v => Assert.True(v.V >= 5 - 1e-9));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContourSeries_ClipHigh_RemovesVerticesAboveThreshold()
|
||||
{
|
||||
var triangle = new List<RadzenContourSeries<ContourItem>.Vertex>
|
||||
{
|
||||
new(0, 0, 0),
|
||||
new(1, 0, 10),
|
||||
new(0, 1, 5),
|
||||
};
|
||||
|
||||
var clipped = RadzenContourSeries<ContourItem>.ClipHigh(triangle, 5);
|
||||
|
||||
Assert.True(clipped.Count >= 3);
|
||||
Assert.All(clipped, v => Assert.True(v.V <= 5 + 1e-9));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async System.Threading.Tasks.Task ContourSeries_Legend_ShowsEntryPerColorRange()
|
||||
{
|
||||
using var ctx = CreateChartContext();
|
||||
|
||||
var chart = ctx.RenderComponent<RadzenChart>(p => p
|
||||
.AddChildContent<RadzenContourSeries<ContourItem>>(s => s
|
||||
.Add(x => x.CategoryProperty, nameof(ContourItem.X))
|
||||
.Add(x => x.ValueProperty, nameof(ContourItem.Y))
|
||||
.Add(x => x.IntensityProperty, nameof(ContourItem.Value))
|
||||
.Add(x => x.ColorRange, Ranges)
|
||||
.Add(x => x.Data, Grid3x3)));
|
||||
|
||||
await chart.InvokeAsync(() => chart.Instance.Resize(400, 300));
|
||||
|
||||
var legendItems = Regex.Matches(chart.Markup, "rz-legend-item\"").Count;
|
||||
Assert.True(legendItems >= Ranges.Count, $"Expected >= {Ranges.Count} legend items, got {legendItems}");
|
||||
}
|
||||
}
|
||||
}
|
||||
110
Radzen.Blazor.Tests/HeatmapSeriesTests.cs
Normal file
110
Radzen.Blazor.Tests/HeatmapSeriesTests.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using Bunit;
|
||||
using Xunit;
|
||||
using static Radzen.Blazor.Tests.ChartTestHelper;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
public class HeatmapSeriesTests
|
||||
{
|
||||
private class HeatItem
|
||||
{
|
||||
public double X { get; set; }
|
||||
public double Y { get; set; }
|
||||
public double Lux { get; set; }
|
||||
}
|
||||
|
||||
private static HeatItem[] Grid2x2 => new[]
|
||||
{
|
||||
new HeatItem { X = 0, Y = 0, Lux = 1 },
|
||||
new HeatItem { X = 1, Y = 0, Lux = 3 },
|
||||
new HeatItem { X = 0, Y = 1, Lux = 5 },
|
||||
new HeatItem { X = 1, Y = 1, Lux = 7 },
|
||||
};
|
||||
|
||||
private static IList<SeriesColorRange> Ranges => new[]
|
||||
{
|
||||
new SeriesColorRange { Min = 0, Max = 2.5, Color = "#0000ff" },
|
||||
new SeriesColorRange { Min = 2.5, Max = 5, Color = "#00ff00" },
|
||||
new SeriesColorRange { Min = 5, Max = 10, Color = "#ff0000" },
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async System.Threading.Tasks.Task HeatmapSeries_Renders_RectPerCell()
|
||||
{
|
||||
using var ctx = CreateChartContext();
|
||||
|
||||
var chart = ctx.RenderComponent<RadzenChart>(p => p
|
||||
.AddChildContent<RadzenHeatmapSeries<HeatItem>>(s => s
|
||||
.Add(x => x.CategoryProperty, nameof(HeatItem.X))
|
||||
.Add(x => x.ValueProperty, nameof(HeatItem.Y))
|
||||
.Add(x => x.IntensityProperty, nameof(HeatItem.Lux))
|
||||
.Add(x => x.ColorRange, Ranges)
|
||||
.Add(x => x.Data, Grid2x2)));
|
||||
|
||||
await chart.InvokeAsync(() => chart.Instance.Resize(400, 300));
|
||||
|
||||
Assert.Contains("rz-heatmap-series rz-series-0", chart.Markup);
|
||||
var rectCount = Regex.Matches(chart.Markup, "<rect ").Count;
|
||||
Assert.True(rectCount >= 4, $"Expected at least 4 cell rects, got {rectCount}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async System.Threading.Tasks.Task HeatmapSeries_Uses_ColorRange_ForFill()
|
||||
{
|
||||
using var ctx = CreateChartContext();
|
||||
|
||||
var chart = ctx.RenderComponent<RadzenChart>(p => p
|
||||
.AddChildContent<RadzenHeatmapSeries<HeatItem>>(s => s
|
||||
.Add(x => x.CategoryProperty, nameof(HeatItem.X))
|
||||
.Add(x => x.ValueProperty, nameof(HeatItem.Y))
|
||||
.Add(x => x.IntensityProperty, nameof(HeatItem.Lux))
|
||||
.Add(x => x.ColorRange, Ranges)
|
||||
.Add(x => x.Data, Grid2x2)));
|
||||
|
||||
await chart.InvokeAsync(() => chart.Instance.Resize(400, 300));
|
||||
|
||||
// Lux=1 → blue, Lux=3 → green, Lux=5 or 7 → red
|
||||
Assert.Contains("#0000ff", chart.Markup);
|
||||
Assert.Contains("#00ff00", chart.Markup);
|
||||
Assert.Contains("#ff0000", chart.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async System.Threading.Tasks.Task HeatmapSeries_Legend_ShowsOneItemPerColorRange()
|
||||
{
|
||||
using var ctx = CreateChartContext();
|
||||
|
||||
var chart = ctx.RenderComponent<RadzenChart>(p => p
|
||||
.AddChildContent<RadzenHeatmapSeries<HeatItem>>(s => s
|
||||
.Add(x => x.CategoryProperty, nameof(HeatItem.X))
|
||||
.Add(x => x.ValueProperty, nameof(HeatItem.Y))
|
||||
.Add(x => x.IntensityProperty, nameof(HeatItem.Lux))
|
||||
.Add(x => x.ColorRange, Ranges)
|
||||
.Add(x => x.Data, Grid2x2)));
|
||||
|
||||
await chart.InvokeAsync(() => chart.Instance.Resize(400, 300));
|
||||
|
||||
var legendItems = Regex.Matches(chart.Markup, "rz-legend-item\"").Count;
|
||||
Assert.True(legendItems >= Ranges.Count, $"Expected >= {Ranges.Count} legend items, got {legendItems}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HeatmapSeries_Hidden_NoRectsRendered()
|
||||
{
|
||||
using var ctx = CreateChartContext();
|
||||
|
||||
var chart = ctx.RenderComponent<RadzenChart>(p => p
|
||||
.AddChildContent<RadzenHeatmapSeries<HeatItem>>(s => s
|
||||
.Add(x => x.CategoryProperty, nameof(HeatItem.X))
|
||||
.Add(x => x.ValueProperty, nameof(HeatItem.Y))
|
||||
.Add(x => x.IntensityProperty, nameof(HeatItem.Lux))
|
||||
.Add(x => x.ColorRange, Ranges)
|
||||
.Add(x => x.Visible, false)
|
||||
.Add(x => x.Data, Grid2x2)));
|
||||
|
||||
Assert.DoesNotContain("rz-heatmap-series", chart.Markup);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Radzen.Blazor/RadzenContourSeries.razor
Normal file
28
Radzen.Blazor/RadzenContourSeries.razor
Normal file
@@ -0,0 +1,28 @@
|
||||
@using Radzen.Blazor.Rendering
|
||||
@typeparam TItem
|
||||
@inherits Radzen.Blazor.CartesianSeries<TItem>
|
||||
<CascadingValue Value="@this">
|
||||
@ChildContent
|
||||
</CascadingValue>
|
||||
@code {
|
||||
public override RenderFragment Render(ScaleBase categoryScale, ScaleBase valueScale)
|
||||
{
|
||||
var chart = RequireChart();
|
||||
var index = chart.Series.IndexOf(this);
|
||||
var className = $"rz-contour-series rz-series-{index}";
|
||||
var style = $"clip-path: url(#{chart.ClipPath}); -webkit-clip-path: url(#{chart.ClipPath});";
|
||||
|
||||
return builder =>
|
||||
{
|
||||
var seq = 0;
|
||||
builder.OpenElement(seq++, "g");
|
||||
builder.AddAttribute(seq++, "class", className);
|
||||
builder.AddAttribute(seq++, "style", style);
|
||||
|
||||
RenderBands(builder, ref seq, categoryScale, valueScale);
|
||||
RenderIsoLines(builder, ref seq, categoryScale, valueScale);
|
||||
|
||||
builder.CloseElement();
|
||||
};
|
||||
}
|
||||
}
|
||||
527
Radzen.Blazor/RadzenContourSeries.razor.cs
Normal file
527
Radzen.Blazor/RadzenContourSeries.razor.cs
Normal file
@@ -0,0 +1,527 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Radzen.Blazor.Rendering;
|
||||
|
||||
namespace Radzen.Blazor
|
||||
{
|
||||
/// <summary>
|
||||
/// A chart series that renders a filled contour (isoband) plot from scalar field data sampled on a
|
||||
/// regular grid. Each data item provides an X coordinate
|
||||
/// (<see cref="CartesianSeries{TItem}.CategoryProperty"/>), a Y coordinate
|
||||
/// (<see cref="CartesianSeries{TItem}.ValueProperty"/>) and an intensity value
|
||||
/// (<see cref="IntensityProperty"/>). The series draws one filled polygon per
|
||||
/// (cell, band) so that areas with intensity within a given <see cref="SeriesColorRange"/> are
|
||||
/// shaded with that range's color.
|
||||
/// <para>
|
||||
/// Uses linear interpolation inside each grid triangle (marching-triangles), which avoids the saddle
|
||||
/// ambiguity of marching-squares and produces smooth iso-bands suitable for isoilluminance plots,
|
||||
/// temperature maps and similar visualisations.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of data items in the series.</typeparam>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// <RadzenChart>
|
||||
/// <RadzenContourSeries Data=@grid CategoryProperty="X" ValueProperty="Y" IntensityProperty="Lux"
|
||||
/// ColorRange="@ranges" ShowLines="true" Title="Illuminance" />
|
||||
/// <RadzenCategoryAxis Min="0" Max="5" />
|
||||
/// <RadzenValueAxis Min="0" Max="5" />
|
||||
/// </RadzenChart>
|
||||
/// </code>
|
||||
/// </example>
|
||||
[UnconditionalSuppressMessage(TrimMessages.Trimming, TrimMessages.IL2026, Justification = TrimMessages.DataTypePreserved)]
|
||||
public partial class RadzenContourSeries<TItem> : CartesianSeries<TItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the numeric property of <typeparamref name="TItem"/> that provides the scalar field
|
||||
/// (the value mapped to color).
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string? IntensityProperty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default fill color applied to regions whose intensity does not match any entry in
|
||||
/// <see cref="ColorRange"/>.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string? Fill { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The color used for iso-lines when <see cref="ShowLines"/> is <c>true</c>. When not set the
|
||||
/// band color is used.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string? LineColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The iso-line width in pixels.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public double LineWidth { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// When <c>true</c> iso-lines are drawn between bands. Defaults to <c>false</c>.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool ShowLines { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Explicit iso-line thresholds. When <c>null</c> the <c>Min</c> of each entry in
|
||||
/// <see cref="ColorRange"/> is used.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public IList<double>? LineThresholds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The value-to-color mapping for isoband fills. Each range specifies a <c>Min</c>, <c>Max</c>
|
||||
/// and <c>Color</c>.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public IList<SeriesColorRange>? ColorRange { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The label used for the intensity dimension in the tooltip. Defaults to <c>"Value"</c>.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string IntensityLabel { get; set; } = "Value";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Color => Fill ?? ColorRange?.FirstOrDefault()?.Color ?? string.Empty;
|
||||
|
||||
internal Func<TItem, double> Intensity
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(IntensityProperty))
|
||||
{
|
||||
return _ => 0;
|
||||
}
|
||||
|
||||
return PropertyAccess.Getter<TItem, double>(IntensityProperty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the regular grid lookup from <see cref="CartesianSeries{TItem}.Items"/>. Returns the
|
||||
/// sorted unique X and Y values and a 2D array of intensities indexed as <c>[xIndex, yIndex]</c>.
|
||||
/// Cells with missing corner values are represented as <see cref="double.NaN"/>.
|
||||
/// </summary>
|
||||
internal (double[] xs, double[] ys, double[,] grid) BuildGrid(ScaleBase categoryScale)
|
||||
{
|
||||
var categoryGetter = Category(categoryScale);
|
||||
var pairs = Items.Select(item => (X: categoryGetter(item), Y: Value(item), V: Intensity(item))).ToList();
|
||||
|
||||
var xs = pairs.Select(p => p.X).Distinct().OrderBy(v => v).ToArray();
|
||||
var ys = pairs.Select(p => p.Y).Distinct().OrderBy(v => v).ToArray();
|
||||
|
||||
var xIndex = xs.Select((x, i) => (x, i)).ToDictionary(p => p.x, p => p.i);
|
||||
var yIndex = ys.Select((y, i) => (y, i)).ToDictionary(p => p.y, p => p.i);
|
||||
|
||||
var grid = new double[xs.Length, ys.Length];
|
||||
for (var i = 0; i < xs.Length; i++)
|
||||
{
|
||||
for (var j = 0; j < ys.Length; j++)
|
||||
{
|
||||
grid[i, j] = double.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var p in pairs)
|
||||
{
|
||||
grid[xIndex[p.X], yIndex[p.Y]] = p.V;
|
||||
}
|
||||
|
||||
return (xs, ys, grid);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ScaleBase TransformValueScale(ScaleBase scale)
|
||||
{
|
||||
// Extend the value axis so that the full grid is included even when axis Min/Max are not set.
|
||||
return base.TransformValueScale(scale);
|
||||
}
|
||||
|
||||
internal readonly struct Vertex
|
||||
{
|
||||
public Vertex(double x, double y, double v) { X = x; Y = y; V = v; }
|
||||
public double X { get; }
|
||||
public double Y { get; }
|
||||
public double V { get; }
|
||||
}
|
||||
|
||||
internal static List<Vertex> ClipLow(List<Vertex> polygon, double threshold)
|
||||
{
|
||||
var result = new List<Vertex>();
|
||||
if (polygon.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
for (var i = 0; i < polygon.Count; i++)
|
||||
{
|
||||
var curr = polygon[i];
|
||||
var prev = polygon[(i - 1 + polygon.Count) % polygon.Count];
|
||||
var currIn = curr.V >= threshold;
|
||||
var prevIn = prev.V >= threshold;
|
||||
|
||||
if (currIn != prevIn)
|
||||
{
|
||||
var denom = curr.V - prev.V;
|
||||
var t = denom == 0 ? 0 : (threshold - prev.V) / denom;
|
||||
result.Add(new Vertex(
|
||||
prev.X + t * (curr.X - prev.X),
|
||||
prev.Y + t * (curr.Y - prev.Y),
|
||||
threshold));
|
||||
}
|
||||
|
||||
if (currIn)
|
||||
{
|
||||
result.Add(curr);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static List<Vertex> ClipHigh(List<Vertex> polygon, double threshold)
|
||||
{
|
||||
var result = new List<Vertex>();
|
||||
if (polygon.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
for (var i = 0; i < polygon.Count; i++)
|
||||
{
|
||||
var curr = polygon[i];
|
||||
var prev = polygon[(i - 1 + polygon.Count) % polygon.Count];
|
||||
var currIn = curr.V <= threshold;
|
||||
var prevIn = prev.V <= threshold;
|
||||
|
||||
if (currIn != prevIn)
|
||||
{
|
||||
var denom = curr.V - prev.V;
|
||||
var t = denom == 0 ? 0 : (threshold - prev.V) / denom;
|
||||
result.Add(new Vertex(
|
||||
prev.X + t * (curr.X - prev.X),
|
||||
prev.Y + t * (curr.Y - prev.Y),
|
||||
threshold));
|
||||
}
|
||||
|
||||
if (currIn)
|
||||
{
|
||||
result.Add(curr);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static (Vertex A, Vertex B)? TriangleIsoSegment(Vertex a, Vertex b, Vertex c, double level)
|
||||
{
|
||||
Vertex? first = null;
|
||||
Vertex? second = null;
|
||||
|
||||
void CheckEdge(Vertex p, Vertex q)
|
||||
{
|
||||
var pAbove = p.V > level;
|
||||
var qAbove = q.V > level;
|
||||
if (pAbove == qAbove)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var denom = q.V - p.V;
|
||||
if (denom == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var t = (level - p.V) / denom;
|
||||
var v = new Vertex(p.X + t * (q.X - p.X), p.Y + t * (q.Y - p.Y), level);
|
||||
if (first == null)
|
||||
{
|
||||
first = v;
|
||||
}
|
||||
else if (second == null)
|
||||
{
|
||||
second = v;
|
||||
}
|
||||
}
|
||||
|
||||
CheckEdge(a, b);
|
||||
CheckEdge(b, c);
|
||||
CheckEdge(c, a);
|
||||
|
||||
if (first.HasValue && second.HasValue)
|
||||
{
|
||||
return (first.Value, second.Value);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static string BuildPolygonPoints(List<Vertex> polygon)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
for (var i = 0; i < polygon.Count; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
sb.Append(' ');
|
||||
}
|
||||
sb.Append(polygon[i].X.ToString("R", CultureInfo.InvariantCulture));
|
||||
sb.Append(',');
|
||||
sb.Append(polygon[i].Y.ToString("R", CultureInfo.InvariantCulture));
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
internal void RenderBands(RenderTreeBuilder builder, ref int sequence, ScaleBase categoryScale, ScaleBase valueScale)
|
||||
{
|
||||
if (ColorRange == null || ColorRange.Count == 0 || !Items.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var (xs, ys, grid) = BuildGrid(categoryScale);
|
||||
if (xs.Length < 2 || ys.Length < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var xPx = xs.Select(x => categoryScale.Scale(x, true)).ToArray();
|
||||
var yPx = ys.Select(y => valueScale.Scale(y, true)).ToArray();
|
||||
|
||||
foreach (var range in ColorRange)
|
||||
{
|
||||
for (var i = 0; i < xs.Length - 1; i++)
|
||||
{
|
||||
for (var j = 0; j < ys.Length - 1; j++)
|
||||
{
|
||||
var v00 = grid[i, j];
|
||||
var v10 = grid[i + 1, j];
|
||||
var v01 = grid[i, j + 1];
|
||||
var v11 = grid[i + 1, j + 1];
|
||||
if (double.IsNaN(v00) || double.IsNaN(v10) || double.IsNaN(v01) || double.IsNaN(v11))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var p00 = new Vertex(xPx[i], yPx[j], v00);
|
||||
var p10 = new Vertex(xPx[i + 1], yPx[j], v10);
|
||||
var p01 = new Vertex(xPx[i], yPx[j + 1], v01);
|
||||
var p11 = new Vertex(xPx[i + 1], yPx[j + 1], v11);
|
||||
|
||||
EmitClippedTriangle(builder, ref sequence, p00, p10, p11, range);
|
||||
EmitClippedTriangle(builder, ref sequence, p00, p11, p01, range);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EmitClippedTriangle(RenderTreeBuilder builder, ref int sequence, Vertex a, Vertex b, Vertex c, SeriesColorRange range)
|
||||
{
|
||||
var poly = new List<Vertex> { a, b, c };
|
||||
poly = ClipLow(poly, range.Min);
|
||||
if (poly.Count < 3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
poly = ClipHigh(poly, range.Max);
|
||||
if (poly.Count < 3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var points = BuildPolygonPoints(poly);
|
||||
builder.OpenElement(sequence++, "polygon");
|
||||
builder.AddAttribute(sequence++, "points", points);
|
||||
builder.AddAttribute(sequence++, "fill", range.Color);
|
||||
builder.AddAttribute(sequence++, "stroke", "none");
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
internal void RenderIsoLines(RenderTreeBuilder builder, ref int sequence, ScaleBase categoryScale, ScaleBase valueScale)
|
||||
{
|
||||
if (!ShowLines || !Items.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var thresholds = LineThresholds;
|
||||
if (thresholds == null || thresholds.Count == 0)
|
||||
{
|
||||
thresholds = ColorRange?.Select(r => r.Min).Where(m => !double.IsInfinity(m)).Distinct().OrderBy(v => v).ToList();
|
||||
}
|
||||
|
||||
if (thresholds == null || thresholds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var (xs, ys, grid) = BuildGrid(categoryScale);
|
||||
if (xs.Length < 2 || ys.Length < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var xPx = xs.Select(x => categoryScale.Scale(x, true)).ToArray();
|
||||
var yPx = ys.Select(y => valueScale.Scale(y, true)).ToArray();
|
||||
|
||||
foreach (var level in thresholds)
|
||||
{
|
||||
for (var i = 0; i < xs.Length - 1; i++)
|
||||
{
|
||||
for (var j = 0; j < ys.Length - 1; j++)
|
||||
{
|
||||
var v00 = grid[i, j];
|
||||
var v10 = grid[i + 1, j];
|
||||
var v01 = grid[i, j + 1];
|
||||
var v11 = grid[i + 1, j + 1];
|
||||
if (double.IsNaN(v00) || double.IsNaN(v10) || double.IsNaN(v01) || double.IsNaN(v11))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var p00 = new Vertex(xPx[i], yPx[j], v00);
|
||||
var p10 = new Vertex(xPx[i + 1], yPx[j], v10);
|
||||
var p01 = new Vertex(xPx[i], yPx[j + 1], v01);
|
||||
var p11 = new Vertex(xPx[i + 1], yPx[j + 1], v11);
|
||||
|
||||
EmitIsoLineForTriangle(builder, ref sequence, p00, p10, p11, level);
|
||||
EmitIsoLineForTriangle(builder, ref sequence, p00, p11, p01, level);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EmitIsoLineForTriangle(RenderTreeBuilder builder, ref int sequence, Vertex a, Vertex b, Vertex c, double level)
|
||||
{
|
||||
var segment = TriangleIsoSegment(a, b, c, level);
|
||||
if (!segment.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var stroke = LineColor ?? PickColor(0, null, Fill, ColorRange, level) ?? "#000";
|
||||
builder.OpenElement(sequence++, "line");
|
||||
builder.AddAttribute(sequence++, "x1", segment.Value.A.X.ToString("R", CultureInfo.InvariantCulture));
|
||||
builder.AddAttribute(sequence++, "y1", segment.Value.A.Y.ToString("R", CultureInfo.InvariantCulture));
|
||||
builder.AddAttribute(sequence++, "x2", segment.Value.B.X.ToString("R", CultureInfo.InvariantCulture));
|
||||
builder.AddAttribute(sequence++, "y2", segment.Value.B.Y.ToString("R", CultureInfo.InvariantCulture));
|
||||
builder.AddAttribute(sequence++, "stroke", stroke);
|
||||
builder.AddAttribute(sequence++, "stroke-width", LineWidth.ToInvariantString());
|
||||
builder.AddAttribute(sequence++, "fill", "none");
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override RenderFragment RenderLegendItem(bool clickable)
|
||||
{
|
||||
if (ColorRange == null || ColorRange.Count == 0)
|
||||
{
|
||||
return base.RenderLegendItem(clickable);
|
||||
}
|
||||
|
||||
var chart = RequireChart();
|
||||
var index = chart.Series.IndexOf(this);
|
||||
|
||||
return builder =>
|
||||
{
|
||||
var seq = 0;
|
||||
for (var r = 0; r < ColorRange.Count; r++)
|
||||
{
|
||||
var range = ColorRange[r];
|
||||
builder.OpenComponent<Rendering.LegendItem>(seq++);
|
||||
builder.AddAttribute(seq++, nameof(Rendering.LegendItem.Index), index);
|
||||
builder.AddAttribute(seq++, nameof(Rendering.LegendItem.Color), range.Color);
|
||||
builder.AddAttribute(seq++, nameof(Rendering.LegendItem.MarkerType), MarkerType.Square);
|
||||
builder.AddAttribute(seq++, nameof(Rendering.LegendItem.MarkerSize), MarkerSize);
|
||||
builder.AddAttribute(seq++, nameof(Rendering.LegendItem.Text), FormatRangeLabel(range));
|
||||
builder.AddAttribute(seq++, nameof(Rendering.LegendItem.Clickable), false);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatRangeLabel(SeriesColorRange range)
|
||||
{
|
||||
var min = double.IsNegativeInfinity(range.Min) ? "-∞" : range.Min.ToString("R", CultureInfo.InvariantCulture);
|
||||
var max = double.IsPositiveInfinity(range.Max) ? "+∞" : range.Max.ToString("R", CultureInfo.InvariantCulture);
|
||||
return $"{min} – {max}";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Contains(double x, double y, double tolerance)
|
||||
{
|
||||
var chart = Chart;
|
||||
if (chart == null || !Items.Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var category = ComposeCategory(chart.CategoryScale);
|
||||
var value = ComposeValue(chart.GetValueScale(ValueAxisName));
|
||||
|
||||
return Items.Any(item =>
|
||||
{
|
||||
var dx = category(item) - x;
|
||||
var dy = value(item) - y;
|
||||
return Math.Sqrt(dx * dx + dy * dy) <= tolerance;
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (object, Point) DataAt(double x, double y)
|
||||
{
|
||||
var chart = Chart;
|
||||
if (chart == null || !Items.Any())
|
||||
{
|
||||
return (default!, new Point());
|
||||
}
|
||||
|
||||
var category = ComposeCategory(chart.CategoryScale);
|
||||
var value = ComposeValue(chart.GetValueScale(ValueAxisName));
|
||||
|
||||
var result = Items.Select(item =>
|
||||
{
|
||||
var px = category(item);
|
||||
var py = value(item);
|
||||
var dx = px - x;
|
||||
var dy = py - y;
|
||||
return new { Item = item, Distance = Math.Sqrt(dx * dx + dy * dy), Point = new Point { X = px, Y = py } };
|
||||
}).Aggregate((a, b) => a.Distance < b.Distance ? a : b);
|
||||
|
||||
return (result.Item!, result.Point);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string TooltipValue(TItem item)
|
||||
{
|
||||
if (string.IsNullOrEmpty(IntensityProperty))
|
||||
{
|
||||
return base.TooltipValue(item);
|
||||
}
|
||||
|
||||
var intensity = Intensity(item);
|
||||
return $"{IntensityLabel}: {intensity.ToInvariantString()}";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string TooltipTitle(TItem item)
|
||||
{
|
||||
var chart = RequireChart();
|
||||
var category = Category(chart.CategoryScale);
|
||||
var xText = chart.CategoryAxis.Format(chart.CategoryScale, chart.CategoryScale.Value(category(item)));
|
||||
var vs = chart.GetValueScale(ValueAxisName);
|
||||
var yText = chart.GetValueAxis(ValueAxisName).Format(vs, vs.Value(Value(item)));
|
||||
return $"{xText}, {yText}";
|
||||
}
|
||||
}
|
||||
}
|
||||
45
Radzen.Blazor/RadzenHeatmapSeries.razor
Normal file
45
Radzen.Blazor/RadzenHeatmapSeries.razor
Normal file
@@ -0,0 +1,45 @@
|
||||
@using Radzen.Blazor.Rendering
|
||||
@typeparam TItem
|
||||
@inherits Radzen.Blazor.CartesianSeries<TItem>
|
||||
<CascadingValue Value="@this">
|
||||
@ChildContent
|
||||
</CascadingValue>
|
||||
@code {
|
||||
public override RenderFragment Render(ScaleBase categoryScale, ScaleBase valueScale)
|
||||
{
|
||||
var chart = RequireChart();
|
||||
var category = ComposeCategory(categoryScale);
|
||||
var value = ComposeValue(valueScale);
|
||||
var index = chart.Series.IndexOf(this);
|
||||
var className = $"rz-heatmap-series rz-series-{index}";
|
||||
var style = $"clip-path: url(#{chart.ClipPath}); -webkit-clip-path: url(#{chart.ClipPath});";
|
||||
|
||||
var cellWidth = EffectiveCellWidth(categoryScale);
|
||||
var cellHeight = EffectiveCellHeight();
|
||||
|
||||
var halfWidthPx = Math.Abs(categoryScale.Scale(cellWidth, true) - categoryScale.Scale(0, true)) / 2;
|
||||
var halfHeightPx = Math.Abs(valueScale.Scale(0, true) - valueScale.Scale(cellHeight, true)) / 2;
|
||||
var widthPx = halfWidthPx * 2;
|
||||
var heightPx = halfHeightPx * 2;
|
||||
|
||||
return
|
||||
@<g class="@className" style="@style">
|
||||
@foreach (var item in Items)
|
||||
{
|
||||
var cx = category(item);
|
||||
var cy = value(item);
|
||||
var intensity = Intensity(item);
|
||||
var fill = PickColor(Items.IndexOf(item), null, Fill, ColorRange, intensity) ?? "transparent";
|
||||
var x = cx - halfWidthPx;
|
||||
var y = cy - halfHeightPx;
|
||||
var cellItem = item;
|
||||
<rect x="@x.ToInvariantString()" y="@y.ToInvariantString()"
|
||||
width="@widthPx.ToInvariantString()" height="@heightPx.ToInvariantString()"
|
||||
fill="@fill"
|
||||
stroke="@(Stroke ?? "none")"
|
||||
stroke-width="@StrokeWidth.ToInvariantString()"
|
||||
@onclick="@(async () => { if (chart.SeriesClick.HasDelegate) { await InvokeClick(chart.SeriesClick, cellItem!); } })" />
|
||||
}
|
||||
</g>;
|
||||
}
|
||||
}
|
||||
284
Radzen.Blazor/RadzenHeatmapSeries.razor.cs
Normal file
284
Radzen.Blazor/RadzenHeatmapSeries.razor.cs
Normal file
@@ -0,0 +1,284 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Radzen.Blazor.Rendering;
|
||||
|
||||
namespace Radzen.Blazor
|
||||
{
|
||||
/// <summary>
|
||||
/// A chart series that renders a grid of colored cells (a heatmap). Each data item provides an
|
||||
/// X coordinate (<see cref="CartesianSeries{TItem}.CategoryProperty"/>), a Y coordinate
|
||||
/// (<see cref="CartesianSeries{TItem}.ValueProperty"/>) and an intensity value
|
||||
/// (<see cref="IntensityProperty"/>) that drives the cell color.
|
||||
/// <para>
|
||||
/// Cell color is picked from <see cref="ColorRange"/>. When no range matches, <see cref="Fill"/> is used.
|
||||
/// Both <see cref="CartesianSeries{TItem}.CategoryProperty"/> and <see cref="CartesianSeries{TItem}.ValueProperty"/>
|
||||
/// must be numeric.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of data items in the series.</typeparam>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// <RadzenChart>
|
||||
/// <RadzenHeatmapSeries Data=@grid CategoryProperty="X" ValueProperty="Y" IntensityProperty="Lux"
|
||||
/// ColorRange="@ranges" Title="Illuminance" />
|
||||
/// <RadzenCategoryAxis Min="0" Max="5" />
|
||||
/// <RadzenValueAxis Min="0" Max="5" />
|
||||
/// </RadzenChart>
|
||||
/// </code>
|
||||
/// </example>
|
||||
[UnconditionalSuppressMessage(TrimMessages.Trimming, TrimMessages.IL2026, Justification = TrimMessages.DataTypePreserved)]
|
||||
public partial class RadzenHeatmapSeries<TItem> : CartesianSeries<TItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the numeric property of <typeparamref name="TItem"/> that provides the cell intensity
|
||||
/// (the value mapped to color).
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string? IntensityProperty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The width of each cell in axis units. When <c>0</c> (default) the width is inferred from the
|
||||
/// smallest gap between unique X values in the data.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public double CellWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The height of each cell in axis units. When <c>0</c> (default) the height is inferred from the
|
||||
/// smallest gap between unique Y values in the data.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public double CellHeight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default fill color applied to cells whose intensity does not match any entry in
|
||||
/// <see cref="ColorRange"/>.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string? Fill { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The cell border color.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string? Stroke { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The cell border width in pixels.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public double StrokeWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The value-to-color mapping for cells. Each range specifies a <c>Min</c>, <c>Max</c> and <c>Color</c>.
|
||||
/// Cells with an intensity falling inside a range are filled with its color.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public IList<SeriesColorRange>? ColorRange { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The label used for the intensity dimension in the tooltip. Defaults to <c>"Value"</c>.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string IntensityLabel { get; set; } = "Value";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string Color => Fill ?? ColorRange?.FirstOrDefault()?.Color ?? Stroke ?? string.Empty;
|
||||
|
||||
internal Func<TItem, double> Intensity
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(IntensityProperty))
|
||||
{
|
||||
return _ => 0;
|
||||
}
|
||||
|
||||
return PropertyAccess.Getter<TItem, double>(IntensityProperty);
|
||||
}
|
||||
}
|
||||
|
||||
internal double EffectiveCellWidth(ScaleBase categoryScale)
|
||||
{
|
||||
if (CellWidth > 0)
|
||||
{
|
||||
return CellWidth;
|
||||
}
|
||||
|
||||
var getter = Category(categoryScale);
|
||||
return InferStep(Items.Select(getter));
|
||||
}
|
||||
|
||||
internal double EffectiveCellHeight()
|
||||
{
|
||||
if (CellHeight > 0)
|
||||
{
|
||||
return CellHeight;
|
||||
}
|
||||
|
||||
return InferStep(Items.Select(Value));
|
||||
}
|
||||
|
||||
private static double InferStep(IEnumerable<double> values)
|
||||
{
|
||||
var distinct = values.Distinct().OrderBy(v => v).ToList();
|
||||
if (distinct.Count < 2)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
double step = double.MaxValue;
|
||||
for (var i = 1; i < distinct.Count; i++)
|
||||
{
|
||||
var gap = distinct[i] - distinct[i - 1];
|
||||
if (gap > 0 && gap < step)
|
||||
{
|
||||
step = gap;
|
||||
}
|
||||
}
|
||||
|
||||
return step == double.MaxValue ? 1 : step;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ScaleBase TransformCategoryScale(ScaleBase scale)
|
||||
{
|
||||
var result = base.TransformCategoryScale(scale);
|
||||
ExpandScaleByHalfCell(result, EffectiveCellWidth(result) / 2);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ScaleBase TransformValueScale(ScaleBase scale)
|
||||
{
|
||||
var result = base.TransformValueScale(scale);
|
||||
ExpandScaleByHalfCell(result, EffectiveCellHeight() / 2);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void ExpandScaleByHalfCell(ScaleBase scale, double halfCell)
|
||||
{
|
||||
if (halfCell <= 0 || scale.Input == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
scale.Input.MergeWidth(new ScaleRange
|
||||
{
|
||||
Start = scale.Input.Start - halfCell,
|
||||
End = scale.Input.End + halfCell,
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Contains(double x, double y, double tolerance)
|
||||
{
|
||||
var chart = Chart;
|
||||
if (chart == null || !Items.Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var category = ComposeCategory(chart.CategoryScale);
|
||||
var value = ComposeValue(chart.GetValueScale(ValueAxisName));
|
||||
var halfWidth = chart.CategoryScale.Scale(EffectiveCellWidth(chart.CategoryScale), true) - chart.CategoryScale.Scale(0, true);
|
||||
var halfHeight = chart.GetValueScale(ValueAxisName).Scale(0, true) - chart.GetValueScale(ValueAxisName).Scale(EffectiveCellHeight(), true);
|
||||
halfWidth = Math.Abs(halfWidth) / 2;
|
||||
halfHeight = Math.Abs(halfHeight) / 2;
|
||||
|
||||
return Items.Any(item =>
|
||||
{
|
||||
var cx = category(item);
|
||||
var cy = value(item);
|
||||
return x >= cx - halfWidth && x <= cx + halfWidth &&
|
||||
y >= cy - halfHeight && y <= cy + halfHeight;
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override (object, Point) DataAt(double x, double y)
|
||||
{
|
||||
var chart = Chart;
|
||||
if (chart == null || !Items.Any())
|
||||
{
|
||||
return (default!, new Point());
|
||||
}
|
||||
|
||||
var category = ComposeCategory(chart.CategoryScale);
|
||||
var value = ComposeValue(chart.GetValueScale(ValueAxisName));
|
||||
|
||||
var result = Items.Select(item =>
|
||||
{
|
||||
var cx = category(item);
|
||||
var cy = value(item);
|
||||
var dx = cx - x;
|
||||
var dy = cy - y;
|
||||
return new { Item = item, Distance = Math.Sqrt(dx * dx + dy * dy), Point = new Point { X = cx, Y = cy } };
|
||||
}).Aggregate((a, b) => a.Distance < b.Distance ? a : b);
|
||||
|
||||
return (result.Item!, result.Point);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override RenderFragment RenderLegendItem(bool clickable)
|
||||
{
|
||||
if (ColorRange == null || ColorRange.Count == 0)
|
||||
{
|
||||
return base.RenderLegendItem(clickable);
|
||||
}
|
||||
|
||||
var chart = RequireChart();
|
||||
var index = chart.Series.IndexOf(this);
|
||||
|
||||
return builder =>
|
||||
{
|
||||
var seq = 0;
|
||||
for (var r = 0; r < ColorRange.Count; r++)
|
||||
{
|
||||
var range = ColorRange[r];
|
||||
builder.OpenComponent<Rendering.LegendItem>(seq++);
|
||||
builder.AddAttribute(seq++, nameof(Rendering.LegendItem.Index), index);
|
||||
builder.AddAttribute(seq++, nameof(Rendering.LegendItem.Color), range.Color);
|
||||
builder.AddAttribute(seq++, nameof(Rendering.LegendItem.MarkerType), MarkerType.Square);
|
||||
builder.AddAttribute(seq++, nameof(Rendering.LegendItem.MarkerSize), MarkerSize);
|
||||
builder.AddAttribute(seq++, nameof(Rendering.LegendItem.Text), FormatRangeLabel(range));
|
||||
builder.AddAttribute(seq++, nameof(Rendering.LegendItem.Clickable), false);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatRangeLabel(SeriesColorRange range)
|
||||
{
|
||||
var min = double.IsNegativeInfinity(range.Min) ? "-∞" : range.Min.ToString("R", System.Globalization.CultureInfo.InvariantCulture);
|
||||
var max = double.IsPositiveInfinity(range.Max) ? "+∞" : range.Max.ToString("R", System.Globalization.CultureInfo.InvariantCulture);
|
||||
return $"{min} – {max}";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string TooltipTitle(TItem item)
|
||||
{
|
||||
var chart = RequireChart();
|
||||
var category = Category(chart.CategoryScale);
|
||||
var xText = chart.CategoryAxis.Format(chart.CategoryScale, chart.CategoryScale.Value(category(item)));
|
||||
var vs = chart.GetValueScale(ValueAxisName);
|
||||
var yText = chart.GetValueAxis(ValueAxisName).Format(vs, vs.Value(Value(item)));
|
||||
return $"{xText}, {yText}";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string TooltipValue(TItem item)
|
||||
{
|
||||
if (string.IsNullOrEmpty(IntensityProperty))
|
||||
{
|
||||
return base.TooltipValue(item);
|
||||
}
|
||||
|
||||
var intensity = Intensity(item);
|
||||
return $"{IntensityLabel}: {intensity.ToInvariantString()}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,8 @@
|
||||
</RadzenText>
|
||||
<ul>
|
||||
<li><RadzenBadge BadgeStyle="BadgeStyle.Info" Shade="Shade.Lighter" Text="New" /> New chart series types: Candlestick, OHLC, Waterfall, Horizontal Waterfall, Range Column, Range Bar, Range Area, Range Step Area, Box Plot, Bullet, Pyramid, Heatmap, Treemap, Funnel, Radar Column, Step Line, Step Area, Stacked Line, Full Stacked Column, Full Stacked Bar, Full Stacked Area, Full Stacked Line, and High-Low.</li>
|
||||
<li><RadzenBadge BadgeStyle="BadgeStyle.Info" Shade="Shade.Lighter" Text="New" /> <RadzenLink Text="RadzenHeatmapSeries" Path="/heatmap-series-chart" /> - chart series that renders a grid of colour-coded cells on numeric X/Y axes, driven by an intensity property and a <code>SeriesColorRange</code> palette.</li>
|
||||
<li><RadzenBadge BadgeStyle="BadgeStyle.Info" Shade="Shade.Lighter" Text="New" /> <RadzenLink Text="RadzenContourSeries" Path="/contour-chart" /> - chart series that renders a filled iso-band contour plot (e.g. isoilluminance) from a regular grid of scalar samples, with optional iso-lines.</li>
|
||||
<li><RadzenBadge BadgeStyle="BadgeStyle.Info" Shade="Shade.Lighter" Text="New" /> <RadzenLink Text="Range Navigator" Path="/range-navigator" /> component for interactive range selection on chart data.</li>
|
||||
<li><RadzenBadge BadgeStyle="BadgeStyle.Info" Shade="Shade.Lighter" Text="New" /> <RadzenLink Text="Chart Gallery" Path="/chart-gallery" /> page showcasing all available chart types.</li>
|
||||
<li><RadzenBadge BadgeStyle="BadgeStyle.Success" Shade="Shade.Lighter" Text="Feature" /> Pan and zoom support for interactive data exploration.</li>
|
||||
|
||||
@@ -999,6 +999,61 @@
|
||||
</RadzenLink>
|
||||
</RadzenColumn>
|
||||
|
||||
@* Heatmap Series (numeric X/Y) *@
|
||||
<RadzenColumn Size="6" SizeMD="4" SizeLG="3">
|
||||
<RadzenLink Path="/heatmap-series-chart">
|
||||
<RadzenCard Variant="Variant.Text" class="rz-p-0">
|
||||
<div class="chart-gallery-thumb">
|
||||
<svg viewBox="0 0 200 132" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Heatmap series chart thumbnail">
|
||||
<rect x="22" y="22" width="32" height="22" rx="2" fill="var(--rz-series-1)" opacity="0.15"/>
|
||||
<rect x="56" y="22" width="32" height="22" rx="2" fill="var(--rz-series-1)" opacity="0.3"/>
|
||||
<rect x="90" y="22" width="32" height="22" rx="2" fill="var(--rz-series-2)" opacity="0.45"/>
|
||||
<rect x="124" y="22" width="32" height="22" rx="2" fill="var(--rz-series-2)" opacity="0.55"/>
|
||||
<rect x="158" y="22" width="20" height="22" rx="2" fill="var(--rz-series-3)" opacity="0.65"/>
|
||||
<rect x="22" y="46" width="32" height="22" rx="2" fill="var(--rz-series-1)" opacity="0.3"/>
|
||||
<rect x="56" y="46" width="32" height="22" rx="2" fill="var(--rz-series-2)" opacity="0.55"/>
|
||||
<rect x="90" y="46" width="32" height="22" rx="2" fill="var(--rz-series-3)" opacity="0.75"/>
|
||||
<rect x="124" y="46" width="32" height="22" rx="2" fill="var(--rz-series-3)" opacity="0.85"/>
|
||||
<rect x="158" y="46" width="20" height="22" rx="2" fill="var(--rz-series-4)" opacity="0.65"/>
|
||||
<rect x="22" y="70" width="32" height="22" rx="2" fill="var(--rz-series-1)" opacity="0.45"/>
|
||||
<rect x="56" y="70" width="32" height="22" rx="2" fill="var(--rz-series-2)" opacity="0.7"/>
|
||||
<rect x="90" y="70" width="32" height="22" rx="2" fill="var(--rz-series-4)" opacity="0.95"/>
|
||||
<rect x="124" y="70" width="32" height="22" rx="2" fill="var(--rz-series-3)" opacity="0.75"/>
|
||||
<rect x="158" y="70" width="20" height="22" rx="2" fill="var(--rz-series-2)" opacity="0.5"/>
|
||||
<rect x="22" y="94" width="32" height="14" rx="2" fill="var(--rz-series-1)" opacity="0.2"/>
|
||||
<rect x="56" y="94" width="32" height="14" rx="2" fill="var(--rz-series-1)" opacity="0.35"/>
|
||||
<rect x="90" y="94" width="32" height="14" rx="2" fill="var(--rz-series-2)" opacity="0.5"/>
|
||||
<rect x="124" y="94" width="32" height="14" rx="2" fill="var(--rz-series-2)" opacity="0.4"/>
|
||||
<rect x="158" y="94" width="20" height="14" rx="2" fill="var(--rz-series-1)" opacity="0.25"/>
|
||||
</svg>
|
||||
</div>
|
||||
<RadzenText TextStyle="TextStyle.Body1" class="rz-pb-4 rz-text-align-center">Heatmap Series <RadzenBadge BadgeStyle="BadgeStyle.Success" Text="New" Style="vertical-align: text-top;" /></RadzenText>
|
||||
</RadzenCard>
|
||||
</RadzenLink>
|
||||
</RadzenColumn>
|
||||
|
||||
@* Contour (Isoilluminance) *@
|
||||
<RadzenColumn Size="6" SizeMD="4" SizeLG="3">
|
||||
<RadzenLink Path="/contour-chart">
|
||||
<RadzenCard Variant="Variant.Text" class="rz-p-0">
|
||||
<div class="chart-gallery-thumb">
|
||||
<svg viewBox="0 0 200 132" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Contour chart thumbnail">
|
||||
<rect x="22" y="18" width="156" height="96" rx="2" fill="var(--rz-series-1)" opacity="0.25"/>
|
||||
<ellipse cx="100" cy="66" rx="68" ry="40" fill="var(--rz-series-2)" opacity="0.45"/>
|
||||
<ellipse cx="100" cy="66" rx="52" ry="30" fill="var(--rz-series-3)" opacity="0.6"/>
|
||||
<ellipse cx="100" cy="66" rx="36" ry="20" fill="var(--rz-series-4)" opacity="0.75"/>
|
||||
<ellipse cx="100" cy="66" rx="20" ry="11" fill="var(--rz-series-5)" opacity="0.9"/>
|
||||
<ellipse cx="100" cy="66" rx="68" ry="40" fill="none" stroke="rgba(0,0,0,0.35)" stroke-width="0.75"/>
|
||||
<ellipse cx="100" cy="66" rx="52" ry="30" fill="none" stroke="rgba(0,0,0,0.35)" stroke-width="0.75"/>
|
||||
<ellipse cx="100" cy="66" rx="36" ry="20" fill="none" stroke="rgba(0,0,0,0.35)" stroke-width="0.75"/>
|
||||
<ellipse cx="100" cy="66" rx="20" ry="11" fill="none" stroke="rgba(0,0,0,0.35)" stroke-width="0.75"/>
|
||||
</svg>
|
||||
</div>
|
||||
<RadzenText TextStyle="TextStyle.Body1" class="rz-pb-4 rz-text-align-center">Contour <RadzenBadge BadgeStyle="BadgeStyle.Success" Text="New" Style="vertical-align: text-top;" /></RadzenText>
|
||||
</RadzenCard>
|
||||
</RadzenLink>
|
||||
</RadzenColumn>
|
||||
|
||||
@* Treemap *@
|
||||
<RadzenColumn Size="6" SizeMD="4" SizeLG="3">
|
||||
<RadzenLink Path="/treemap-chart">
|
||||
|
||||
69
RadzenBlazorDemos/Pages/ContourChart.razor
Normal file
69
RadzenBlazorDemos/Pages/ContourChart.razor
Normal file
@@ -0,0 +1,69 @@
|
||||
@using System.Collections.Generic
|
||||
|
||||
<RadzenStack class="rz-p-0 rz-p-md-6 rz-p-lg-12">
|
||||
<RadzenCard Variant="Variant.Outlined">
|
||||
<RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" Gap="1rem">
|
||||
<RadzenCheckBox @bind-Value="@showLines" Name="showLines" />
|
||||
<RadzenLabel Text="Show iso-lines" Component="showLines" />
|
||||
</RadzenStack>
|
||||
</RadzenCard>
|
||||
|
||||
<RadzenChart>
|
||||
<RadzenContourSeries Data="@samples" CategoryProperty="X" ValueProperty="Y" IntensityProperty="Lux"
|
||||
ColorRange="@ranges"
|
||||
ShowLines="@showLines" LineColor="rgba(0,0,0,0.5)" LineWidth="1"
|
||||
IntensityLabel="Lux" Title="Illuminance" />
|
||||
<RadzenCategoryAxis Min="0" Max="5" Step="1">
|
||||
<RadzenGridLines Visible="true" />
|
||||
<RadzenAxisTitle Text="X (m)" />
|
||||
</RadzenCategoryAxis>
|
||||
<RadzenValueAxis Min="0" Max="5" Step="1">
|
||||
<RadzenGridLines Visible="true" />
|
||||
<RadzenAxisTitle Text="Y (m)" />
|
||||
</RadzenValueAxis>
|
||||
<RadzenLegend Position="LegendPosition.Right" />
|
||||
</RadzenChart>
|
||||
</RadzenStack>
|
||||
|
||||
@code {
|
||||
class Sample
|
||||
{
|
||||
public double X { get; set; }
|
||||
public double Y { get; set; }
|
||||
public double Lux { get; set; }
|
||||
}
|
||||
|
||||
bool showLines = true;
|
||||
List<Sample> samples = new();
|
||||
|
||||
IList<SeriesColorRange> ranges = new[]
|
||||
{
|
||||
new SeriesColorRange { Min = 0, Max = 5, Color = "#0d47a1" },
|
||||
new SeriesColorRange { Min = 5, Max = 10, Color = "#1976d2" },
|
||||
new SeriesColorRange { Min = 10, Max = 20, Color = "#42a5f5" },
|
||||
new SeriesColorRange { Min = 20, Max = 40, Color = "#66bb6a" },
|
||||
new SeriesColorRange { Min = 40, Max = 70, Color = "#fdd835" },
|
||||
new SeriesColorRange { Min = 70, Max = 120, Color = "#fb8c00" },
|
||||
new SeriesColorRange { Min = 120, Max = 500, Color = "#e53935" },
|
||||
};
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
// Radial illuminance pattern centred at (2.5, 2.5). A dense 21x21 grid gives smooth iso-bands.
|
||||
const int steps = 20;
|
||||
const double size = 5;
|
||||
for (var ix = 0; ix <= steps; ix++)
|
||||
{
|
||||
for (var iy = 0; iy <= steps; iy++)
|
||||
{
|
||||
var x = (double)ix * size / steps;
|
||||
var y = (double)iy * size / steps;
|
||||
var dx = x - 2.5;
|
||||
var dy = y - 2.5;
|
||||
var d2 = dx * dx + dy * dy;
|
||||
var lux = 150 * System.Math.Exp(-d2 / 1.5);
|
||||
samples.Add(new Sample { X = x, Y = y, Lux = lux });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
RadzenBlazorDemos/Pages/ContourChartPage.razor
Normal file
21
RadzenBlazorDemos/Pages/ContourChartPage.razor
Normal file
@@ -0,0 +1,21 @@
|
||||
@page "/contour-chart"
|
||||
|
||||
<RadzenText TextStyle="TextStyle.H2" TagName="TagName.H1" class="rz-pt-8">
|
||||
Radzen Blazor Chart with contour series (isoilluminance)
|
||||
</RadzenText>
|
||||
<RadzenText TextStyle="TextStyle.Subtitle1" TagName="TagName.P" class="rz-pb-4">
|
||||
<strong>RadzenContourSeries</strong> renders a filled iso-band contour plot from a regular grid of scalar samples. Values between the grid points are interpolated linearly (marching-triangles), producing smooth bands suitable for <strong>isoilluminance plots, temperature maps</strong> and similar scientific visualisations. Enable <code>ShowLines</code> to draw iso-lines between bands.
|
||||
</RadzenText>
|
||||
|
||||
<RadzenAlert AlertStyle="AlertStyle.Info" Shade="Shade.Lighter" AllowClose="false" class="rz-mb-4">
|
||||
<strong>When to use which?</strong>
|
||||
<ul class="rz-mt-2 rz-mb-0">
|
||||
<li>Use <strong>RadzenContourSeries</strong> (this demo) when samples lie on a <strong>numeric grid</strong> and you want smooth interpolation between them — the result looks continuous and is resolution-independent.</li>
|
||||
<li>Use <RadzenLink Path="/heatmap-series-chart" Text="RadzenHeatmapSeries" /> when you want to show the <strong>raw sample cells</strong> without interpolation (each data point becomes one coloured rectangle).</li>
|
||||
<li>Use <RadzenLink Path="/heatmap-chart" Text="RadzenHeatmap" /> for <strong>categorical</strong> grids (days × hours) where the axes are strings, not numbers.</li>
|
||||
</ul>
|
||||
</RadzenAlert>
|
||||
|
||||
<RadzenExample ComponentName="Chart" Example="ContourChart" DocumentationLink="https://blazor.radzen.com/docs/guides/components/chart.html">
|
||||
<ContourChart />
|
||||
</RadzenExample>
|
||||
@@ -4,9 +4,18 @@
|
||||
Radzen Blazor Heatmap Chart
|
||||
</RadzenText>
|
||||
<RadzenText TextStyle="TextStyle.Subtitle1" TagName="TagName.P" class="rz-pb-4">
|
||||
Visualize data intensity as a color-coded grid using RadzenHeatmap.
|
||||
<strong>RadzenHeatmap</strong> is a standalone component (not a chart series) that visualises data intensity as a color-coded grid with <strong>categorical string</strong> axes and a two-colour gradient. Best for calendar heatmaps, activity matrices and day × hour style dashboards.
|
||||
</RadzenText>
|
||||
|
||||
<RadzenAlert AlertStyle="AlertStyle.Info" Shade="Shade.Lighter" AllowClose="false" class="rz-mb-4">
|
||||
<strong>When to use which?</strong>
|
||||
<ul class="rz-mt-2 rz-mb-0">
|
||||
<li>Use <strong>RadzenHeatmap</strong> (this demo) when X and Y are <strong>categorical strings</strong> and a continuous two-colour gradient is enough.</li>
|
||||
<li>Use <RadzenLink Path="/heatmap-series-chart" Text="RadzenHeatmapSeries" /> inside a <code>RadzenChart</code> when X and Y are <strong>numeric</strong> measurements and you need shared axes, discrete colour bands or overlay with other series.</li>
|
||||
<li>Use <RadzenLink Path="/contour-chart" Text="RadzenContourSeries" /> when samples are on a numeric grid and you want smooth interpolated iso-bands — e.g. isoilluminance, temperature or scalar-field plots.</li>
|
||||
</ul>
|
||||
</RadzenAlert>
|
||||
|
||||
<RadzenExample ComponentName="Heatmap" Example="HeatmapChart">
|
||||
<HeatmapChart />
|
||||
</RadzenExample>
|
||||
|
||||
53
RadzenBlazorDemos/Pages/HeatmapSeriesChart.razor
Normal file
53
RadzenBlazorDemos/Pages/HeatmapSeriesChart.razor
Normal file
@@ -0,0 +1,53 @@
|
||||
@using System.Collections.Generic
|
||||
|
||||
<RadzenChart>
|
||||
<RadzenHeatmapSeries Data="@cells" CategoryProperty="X" ValueProperty="Y" IntensityProperty="Lux"
|
||||
ColorRange="@ranges" IntensityLabel="Lux" Title="Illuminance" />
|
||||
<RadzenCategoryAxis Min="-0.5" Max="5.5" Step="1">
|
||||
<RadzenGridLines Visible="true" />
|
||||
<RadzenAxisTitle Text="X (m)" />
|
||||
</RadzenCategoryAxis>
|
||||
<RadzenValueAxis Min="-0.5" Max="5.5" Step="1">
|
||||
<RadzenGridLines Visible="true" />
|
||||
<RadzenAxisTitle Text="Y (m)" />
|
||||
</RadzenValueAxis>
|
||||
<RadzenLegend Position="LegendPosition.Right" />
|
||||
</RadzenChart>
|
||||
|
||||
@code {
|
||||
class Cell
|
||||
{
|
||||
public double X { get; set; }
|
||||
public double Y { get; set; }
|
||||
public double Lux { get; set; }
|
||||
}
|
||||
|
||||
List<Cell> cells = new();
|
||||
|
||||
IList<SeriesColorRange> ranges = new[]
|
||||
{
|
||||
new SeriesColorRange { Min = 0, Max = 5, Color = "#0d47a1" },
|
||||
new SeriesColorRange { Min = 5, Max = 10, Color = "#1976d2" },
|
||||
new SeriesColorRange { Min = 10, Max = 20, Color = "#42a5f5" },
|
||||
new SeriesColorRange { Min = 20, Max = 40, Color = "#66bb6a" },
|
||||
new SeriesColorRange { Min = 40, Max = 70, Color = "#fdd835" },
|
||||
new SeriesColorRange { Min = 70, Max = 120, Color = "#fb8c00" },
|
||||
new SeriesColorRange { Min = 120, Max = 1000, Color = "#e53935" },
|
||||
};
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
// Radial illuminance pattern centred at (2.5, 2.5) on a 6x6 m grid at 1m spacing.
|
||||
for (var ix = 0; ix <= 5; ix++)
|
||||
{
|
||||
for (var iy = 0; iy <= 5; iy++)
|
||||
{
|
||||
var dx = ix - 2.5;
|
||||
var dy = iy - 2.5;
|
||||
var d2 = dx * dx + dy * dy;
|
||||
var lux = 150 * System.Math.Exp(-d2 / 4);
|
||||
cells.Add(new Cell { X = ix, Y = iy, Lux = System.Math.Round(lux, 1) });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
RadzenBlazorDemos/Pages/HeatmapSeriesChartPage.razor
Normal file
21
RadzenBlazorDemos/Pages/HeatmapSeriesChartPage.razor
Normal file
@@ -0,0 +1,21 @@
|
||||
@page "/heatmap-series-chart"
|
||||
|
||||
<RadzenText TextStyle="TextStyle.H2" TagName="TagName.H1" class="rz-pt-8">
|
||||
Radzen Blazor Chart with heatmap series
|
||||
</RadzenText>
|
||||
<RadzenText TextStyle="TextStyle.Subtitle1" TagName="TagName.P" class="rz-pb-4">
|
||||
<strong>RadzenHeatmapSeries</strong> renders a grid of coloured cells inside a <code>RadzenChart</code>, on <strong>numeric</strong> X/Y axes. Cell colour is driven by an intensity property mapped through discrete <code>SeriesColorRange</code> bands.
|
||||
</RadzenText>
|
||||
|
||||
<RadzenAlert AlertStyle="AlertStyle.Info" Shade="Shade.Lighter" AllowClose="false" class="rz-mb-4">
|
||||
<strong>When to use which?</strong>
|
||||
<ul class="rz-mt-2 rz-mb-0">
|
||||
<li>Use <RadzenLink Path="/heatmap-chart" Text="RadzenHeatmap" /> when X and Y are <strong>categorical strings</strong> (days × hours, rows × columns) and a simple two-colour gradient is enough — it is a standalone component, not a chart series.</li>
|
||||
<li>Use <strong>RadzenHeatmapSeries</strong> (this demo) when X and Y are <strong>numeric measurements</strong> (metres, seconds, temperature). It shares axes, tooltips, legend and click events with other series, so you can overlay a scatter/line on top of it.</li>
|
||||
<li>Use <RadzenLink Path="/contour-chart" Text="RadzenContourSeries" /> when you want smooth <strong>interpolated iso-bands</strong> between sample points instead of discrete cells — ideal for isoilluminance, temperature and other scalar-field plots.</li>
|
||||
</ul>
|
||||
</RadzenAlert>
|
||||
|
||||
<RadzenExample ComponentName="Chart" Example="HeatmapSeriesChart" DocumentationLink="https://blazor.radzen.com/docs/guides/components/chart.html">
|
||||
<HeatmapSeriesChart />
|
||||
</RadzenExample>
|
||||
@@ -2642,6 +2642,22 @@ namespace RadzenBlazorDemos
|
||||
Description = "Radzen Blazor Chart with bubble series for visualizing three dimensions of data.",
|
||||
Tags = new [] { "chart", "graph", "bubble", "scatter", "size" }
|
||||
},
|
||||
new Example
|
||||
{
|
||||
Name = "Heatmap Series Chart",
|
||||
Path = "heatmap-series-chart",
|
||||
Description = "Radzen Blazor Chart with heatmap series rendering a coloured grid on numeric X/Y axes.",
|
||||
Tags = new [] { "chart", "graph", "heatmap", "grid", "intensity", "density" },
|
||||
New = true
|
||||
},
|
||||
new Example
|
||||
{
|
||||
Name = "Contour Chart",
|
||||
Path = "contour-chart",
|
||||
Description = "Radzen Blazor Chart with contour series for isoilluminance, temperature and scalar-field plots.",
|
||||
Tags = new [] { "chart", "graph", "contour", "isoilluminance", "isoline", "isoband", "heatmap", "scalar field" },
|
||||
New = true
|
||||
},
|
||||
}
|
||||
},
|
||||
new Example
|
||||
|
||||
Reference in New Issue
Block a user