Skip to content

Commit

Permalink
Merge pull request #866 from beto-rodriguez/labels-measure-improvements
Browse files Browse the repository at this point in the history
Improves multiline labels measure
  • Loading branch information
beto-rodriguez committed Jan 2, 2023
2 parents 7898f59 + b7d16df commit 2d97448
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 11 deletions.
14 changes: 11 additions & 3 deletions samples/ViewModelsSamples/Axes/LabelsRotation/ViewModel.cs
@@ -1,4 +1,5 @@
using System.Collections.ObjectModel;
using System;
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
Expand All @@ -17,6 +18,10 @@ public partial class ViewModel
new LineSeries<double>
{
Values = new ObservableCollection<double> { 200, 558, 458, 249, 457, 339, 587 },
TooltipLabelFormatter = (point) =>
$"This is {Environment.NewLine}" +
$"A multi-line label {Environment.NewLine}" +
$"With a value of {Environment.NewLine}" + point.PrimaryValue,
}
};

Expand All @@ -26,15 +31,18 @@ public partial class ViewModel
{
// Use the Label property to indicate the format of the labels in the axis
// The Labeler takes the value of the label as parameter and must return it as string
Labeler = (value) => "Day " + value,
Labeler = (value) =>
$"This is {Environment.NewLine}" +
$"A multi-line label {Environment.NewLine}" +
$"With a value of {Environment.NewLine}" +value * 100,

// The MinStep property lets you define the minimum separation (in chart values scale)
// between every axis separator, in this case we don't want decimals,
// so lets force it to be greater or equals than 1
MinStep = 1,

// labels rotations is in degrees (0 - 360)
LabelsRotation = 0,
LabelsRotation = 45,

SeparatorsPaint = new SolidColorPaint(SKColors.LightGray, 2)
}
Expand Down
8 changes: 8 additions & 0 deletions src/LiveChartsCore/Drawing/ILabelGeometry.cs
Expand Up @@ -38,6 +38,14 @@ public interface ILabelGeometry<TDrawingContext> : IGeometry<TDrawingContext>
/// </value>
Padding Padding { get; set; }

/// <summary>
/// Gets or sets the line height [in times the text height].
/// </summary>
/// <value>
/// The padding.
/// </value>
float LineHeight { get; set; }

/// <summary>
/// Gets or sets the vertical align.
/// </summary>
Expand Down
Expand Up @@ -74,6 +74,9 @@ public LabelGeometry()
/// <inheritdoc cref="ILabelGeometry{TDrawingContext}.Padding" />
public Padding Padding { get; set; } = new();

/// <inheritdoc cref="ILabelGeometry{TDrawingContext}.LineHeight" />
public float LineHeight { get; set; } = 1.75f;

/// <inheritdoc cref="Geometry.OnDraw(SkiaSharpDrawingContext, SKPaint)" />
public override void OnDraw(SkiaSharpDrawingContext context, SKPaint paint)
{
Expand All @@ -92,9 +95,13 @@ public override void OnDraw(SkiaSharpDrawingContext context, SKPaint paint)
var lines = GetLines(Text);
double linesCount = lines.Length;
var lineNumber = 0;
var lhd = (GetActualLineHeight(paint) - GetRawLineHeight(paint)) * 0.5f;

foreach (var line in lines)
{
DrawLine(line, -size.Height + (float)(++lineNumber / linesCount * size.Height), context, paint);
var ph = (float)(++lineNumber / linesCount) * size.Height;
var yLine = ph - size.Height;
DrawLine(line, yLine - lhd, context, paint);
}
}

Expand Down Expand Up @@ -178,25 +185,35 @@ private void DrawLine(string content, float yLine, SkiaSharpDrawingContext conte
private LvcSize MeasureLines(SKPaint paint)
{
float w = 0f, h = 0f;
var lineHeight = GetActualLineHeight(paint);

foreach (var line in GetLines(Text))
{
var bounds = new SKRect();
var boundsH = new SKRect();

_ = paint.MeasureText(line, ref bounds);

// measure "|": hack to force the same max height?
// otherwise strings like "___" and "|||" will result in ---||| if the alignment is middle
_ = paint.MeasureText("|", ref boundsH);

if (bounds.Width > w) w = bounds.Width;
h += boundsH.Height;
h += lineHeight;
}

return new LvcSize(w, h);
}

private float GetActualLineHeight(SKPaint paint)
{
var boundsH = new SKRect();
_ = paint.MeasureText("", ref boundsH);
return LineHeight * boundsH.Height;
}

private float GetRawLineHeight(SKPaint paint)
{
var boundsH = new SKRect();
_ = paint.MeasureText("", ref boundsH);
return boundsH.Height;
}

private string[] GetLines(string multiLineText)
{
return string.IsNullOrEmpty(multiLineText)
Expand Down
Expand Up @@ -24,7 +24,6 @@
using LiveChartsCore.Drawing;
using LiveChartsCore.Kernel;
using LiveChartsCore.Measure;
using LiveChartsCore.Motion;
using LiveChartsCore.SkiaSharpView.Drawing;
using LiveChartsCore.SkiaSharpView.Drawing.Geometries;
using LiveChartsCore.VisualElements;
Expand All @@ -46,6 +45,7 @@ public class LabelVisual : VisualElement<SkiaSharpDrawingContext>
internal LvcColor _backgroundColor;
internal Padding _padding = new(0);
internal double _rotation;
internal float _lineHeight = 1.75f;
internal LvcPoint _translate = new();
private LvcSize _actualSize = new();
private LvcPoint _targetPosition = new();
Expand Down Expand Up @@ -99,6 +99,11 @@ public class LabelVisual : VisualElement<SkiaSharpDrawingContext>
/// </summary>
public Padding Padding { get => _padding; set { _padding = value; OnPropertyChanged(); } }

/// <summary>
/// Gets or sets the line height [times the text measured height].
/// </summary>
public float LineHeight { get => _lineHeight; set { _lineHeight = value; OnPropertyChanged(); } }

internal override IPaint<SkiaSharpDrawingContext>?[] GetPaintTasks()
{
return new[] { _paint };
Expand Down Expand Up @@ -162,6 +167,7 @@ protected internal override void OnInvalidated(Chart<SkiaSharpDrawingContext> ch
_labelGeometry.HorizontalAlign = HorizontalAlignment;
_labelGeometry.Background = BackgroundColor;
_labelGeometry.Padding = Padding;
_labelGeometry.LineHeight = LineHeight;

var drawing = chart.Canvas.Draw();
if (Paint is not null) _ = drawing.SelectPaint(Paint).Draw(_labelGeometry);
Expand Down
131 changes: 131 additions & 0 deletions tests/LiveChartsCore.UnitTesting/LabelsMeasureTest.cs
@@ -0,0 +1,131 @@
// The MIT License(MIT)
//
// Copyright(c) 2021 Alberto Rodriguez Orozco & LiveCharts Contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using System;
using System.Collections.Generic;
using LiveChartsCore.Kernel;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Drawing;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.SkiaSharpView.SKCharts;
using LiveChartsCore.SkiaSharpView.VisualElements;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SkiaSharp;

namespace LiveChartsCore.UnitTesting;

[TestClass]
public class LabelsMeasureTest
{
[TestMethod]
public void BasicCase()
{
var visuls = new List<ChartElement<SkiaSharpDrawingContext>>();

var y = 10;
var h = 20;
for (var i = 0; i < 10; i++)
{
visuls.Add(new LabelVisual
{
X = 10,
Y = y,
LocationUnit = Measure.MeasureUnit.Pixels,
VerticalAlignment = Drawing.Align.Start,
HorizontalAlignment = Drawing.Align.Start,
TextSize = h,
Text = "X-█-X",
Paint = new SolidColorPaint(SKColors.Black),
BackgroundColor = new Drawing.LvcColor(0, 0, 0, 50)
});

y += h + 10;
h += 20;
}

var chart = new SKCartesianChart
{
Width = 600,
Height = y + 10,
Series = Array.Empty<ISeries>(),
XAxes = new[] { new Axis { IsVisible = false } },
YAxes = new[] { new Axis { IsVisible = false } },
VisualElements = visuls
};

//_ = chart.GetImage();
chart.SaveImage("test.png"); // use this method to see the actual tested image
}

[TestMethod]
public void MultiLine()
{
var visuls = new List<ChartElement<SkiaSharpDrawingContext>>();

var y = 10f;
var h = 25;
var lines = 5;
var lineHeight = 0.1f;

for (var i = 0; i < 20; i++)
{
var t = "";
for (var j = 0; j < lines; j++)
{
t += $"{(j % 2 == 0 ? "-" : "")}█-{lineHeight:N2}-█{(j != lines - 1 ? Environment.NewLine : "")}";
}

var th = h * lines;

visuls.Add(new LabelVisual
{
X = 10,
Y = y,
LocationUnit = Measure.MeasureUnit.Pixels,
VerticalAlignment = Drawing.Align.Start,
HorizontalAlignment = Drawing.Align.Start,
TextSize = 25,
Text = t,
Paint = new SolidColorPaint(SKColors.Black),
BackgroundColor = new Drawing.LvcColor(0, 0, 0, 50),
LineHeight = lineHeight
});

y += th * lineHeight + 10;
lineHeight += 1f;
}

var chart = new SKCartesianChart
{
Width = 900,
Height = (int)(y + 10),
Series = Array.Empty<ISeries>(),
XAxes = new[] { new Axis { IsVisible = false } },
YAxes = new[] { new Axis { IsVisible = false } },
VisualElements = visuls
};

//_ = chart.GetImage();
chart.SaveImage("multi line labels test.png"); // use this method to see the actual tested image
}
}

0 comments on commit 2d97448

Please sign in to comment.