323 lines
14 KiB
C#
323 lines
14 KiB
C#
using System;
|
|
using System.Windows;
|
|
using System.Windows.Media;
|
|
using LiveChartsCore;
|
|
using LiveChartsCore.Defaults;
|
|
using LiveChartsCore.SkiaSharpView;
|
|
using LiveChartsCore.SkiaSharpView.WPF;
|
|
using LiveChartsCore.SkiaSharpView.Painting;
|
|
using LiveChartsCore.SkiaSharpView.Painting.Effects;
|
|
using SkiaSharp;
|
|
|
|
namespace UbisamBase.Core.Charting;
|
|
|
|
/// <summary>
|
|
/// 차트(축·시리즈)를 만들어주는 정적 팩토리 — 색을 코드에 박지 않고 현재 테마의 브러시 리소스에서
|
|
/// 매번 읽는다. 화면마다 색을 다시 지정하지 말고 이 클래스만 써야 라이트/다크 전환에도 톤이 유지된다.
|
|
/// 테마가 바뀐 뒤에는 이미 만들어진 시리즈의 색은 그대로이므로, <see cref="NotifyThemeChanged"/>를
|
|
/// 구독해 시리즈를 다시 만들어야 한다.
|
|
/// </summary>
|
|
public static class ChartTheme
|
|
{
|
|
private static readonly TimeSpan AnimationSpeed = TimeSpan.FromMilliseconds(260);
|
|
|
|
public static event Action? ThemeChanged;
|
|
|
|
public static void NotifyThemeChanged() => ThemeChanged?.Invoke();
|
|
|
|
public static SKColor Accent => ReadColor("Ubisam.Brush.Accent");
|
|
public static SKColor AccentFill => ReadColor("Ubisam.Brush.Accent.Fill");
|
|
public static SKColor GridLine => ReadColor("Ubisam.Brush.Table.GridLine");
|
|
public static SKColor Muted => ReadColor("Ubisam.Brush.Muted");
|
|
public static SKColor Surface => ReadColor("Ubisam.Brush.Sidebar.Background");
|
|
public static SKColor Text => ReadColor("Ubisam.Brush.ContentForeground");
|
|
|
|
private static SKColor ReadColor(string resourceKey)
|
|
{
|
|
var color = ((SolidColorBrush)Application.Current.Resources[resourceKey]).Color;
|
|
return new SKColor(color.R, color.G, color.B, color.A);
|
|
}
|
|
|
|
private static SKTypeface BodyTypeface => SKTypeface.FromFamilyName("Barlow") ?? SKTypeface.Default;
|
|
|
|
private static SKTypeface HeadingTypeface => SKTypeface.FromFamilyName("Barlow Condensed") ?? SKTypeface.Default;
|
|
|
|
/// <summary>측정값 선 — accent 밝은 단계, 두께 2, 보간 없음(LineSmoothness=0), 점 표시 없음, 면적 12%.</summary>
|
|
public static ISeries Line(double[] values, string name) => new LineSeries<double>
|
|
{
|
|
Values = values,
|
|
Name = name,
|
|
Stroke = new SolidColorPaint(Accent, 2),
|
|
Fill = new SolidColorPaint(Accent.WithAlpha(0x1F)),
|
|
GeometrySize = 0,
|
|
LineSmoothness = 0,
|
|
AnimationsSpeed = AnimationSpeed
|
|
};
|
|
|
|
/// <summary>기준·목표선 — 회색 파선, 면적 없음, hover 비활성. 측정선보다 먼저 시리즈에 넣어야 아래에 깔린다.</summary>
|
|
public static ISeries ReferenceLine(double[] values, string name) => new LineSeries<double>
|
|
{
|
|
Values = values,
|
|
Name = name,
|
|
Stroke = new SolidColorPaint(Muted, 1) { PathEffect = new DashEffect(new float[] { 5, 4 }) },
|
|
Fill = null,
|
|
GeometrySize = 0,
|
|
LineSmoothness = 0,
|
|
IsHoverable = false,
|
|
AnimationsSpeed = AnimationSpeed
|
|
};
|
|
|
|
/// <summary>막대 — accent 깊은 단계 채움 85%, 각진 모서리.</summary>
|
|
public static ISeries Column(double[] values, string name) => new ColumnSeries<double>
|
|
{
|
|
Values = values,
|
|
Name = name,
|
|
Fill = new SolidColorPaint(AccentFill.WithAlpha(217)),
|
|
Rx = 0,
|
|
Ry = 0,
|
|
AnimationsSpeed = AnimationSpeed
|
|
};
|
|
|
|
/// <summary>0~100 가동률 도넛 게이지 — 값 조각(accent 채움) + 잔여 조각(격자선 색).</summary>
|
|
public static ISeries[] Gauge(double value) => new ISeries[]
|
|
{
|
|
new PieSeries<double>
|
|
{
|
|
Values = new[] { value },
|
|
Fill = new SolidColorPaint(AccentFill),
|
|
InnerRadius = 40,
|
|
MaxRadialColumnWidth = 46,
|
|
AnimationsSpeed = AnimationSpeed
|
|
},
|
|
new PieSeries<double>
|
|
{
|
|
Values = new[] { 100 - value },
|
|
Fill = new SolidColorPaint(GridLine),
|
|
InnerRadius = 40,
|
|
MaxRadialColumnWidth = 46,
|
|
IsHoverable = false,
|
|
AnimationsSpeed = AnimationSpeed
|
|
}
|
|
};
|
|
|
|
/// <summary>가로축 — 세로 격자선 없음(SeparatorsPaint 미지정), 라벨 11pt Muted Barlow.</summary>
|
|
public static Axis XAxis(Func<double, string>? labeler = null) => new Axis
|
|
{
|
|
Labeler = labeler != null ? new Func<double, string>(v => labeler(v)) : null!,
|
|
TextSize = 11,
|
|
LabelsPaint = new SolidColorPaint(Muted) { SKTypeface = BodyTypeface },
|
|
SeparatorsPaint = null!
|
|
};
|
|
|
|
/// <summary>세로축 — 가로 격자선(GridLine, 헤어라인), 라벨 11pt Muted Barlow, 자릿수 고정 포맷.</summary>
|
|
public static Axis YAxis(string format = "0", double? min = null, double? max = null) => new Axis
|
|
{
|
|
Labeler = v => v.ToString(format),
|
|
MinLimit = min,
|
|
MaxLimit = max,
|
|
TextSize = 11,
|
|
LabelsPaint = new SolidColorPaint(Muted) { SKTypeface = BodyTypeface },
|
|
SeparatorsPaint = new SolidColorPaint(GridLine, 1)
|
|
};
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// 시리즈 색
|
|
// 단일 계열 차트는 지금까지처럼 accent 하나만 쓴다. 여러 계열을 한 그래프에 그릴 때만
|
|
// 아래 팔레트를 순서대로 쓴다 — 색을 화면마다 고르면 프로그램마다 톤이 달라진다.
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>계열 순번(0부터)에 해당하는 색. 6개를 넘어가면 처음으로 돌아간다.</summary>
|
|
public static SKColor SeriesColor(int index)
|
|
=> ReadColor("Ubisam.Brush.Chart.Series" + ((Math.Abs(index) % 6) + 1));
|
|
|
|
/// <summary>꺾은선 — 여러 계열용. 색만 팔레트에서 가져오고 나머지는 단일 계열과 같다(두께 2, 보간 없음).</summary>
|
|
public static ISeries Line(double[] values, string name, int seriesIndex)
|
|
{
|
|
var color = SeriesColor(seriesIndex);
|
|
return new LineSeries<double>
|
|
{
|
|
Values = values,
|
|
Name = name,
|
|
Stroke = new SolidColorPaint(color, 2),
|
|
Fill = null,
|
|
GeometrySize = 0,
|
|
LineSmoothness = 0,
|
|
AnimationsSpeed = AnimationSpeed
|
|
};
|
|
}
|
|
|
|
/// <summary>부드러운 꺾은선 — 추세용. 공정 측정값처럼 "튄 지점"이 중요한 데이터에는 쓰지 말 것
|
|
/// (보간이 실제로 없던 중간값을 지어낸다).</summary>
|
|
public static ISeries SmoothLine(double[] values, string name, int seriesIndex = 0)
|
|
{
|
|
var color = SeriesColor(seriesIndex);
|
|
return new LineSeries<double>
|
|
{
|
|
Values = values,
|
|
Name = name,
|
|
Stroke = new SolidColorPaint(color, 2),
|
|
Fill = null,
|
|
GeometrySize = 0,
|
|
LineSmoothness = 0.65,
|
|
AnimationsSpeed = AnimationSpeed
|
|
};
|
|
}
|
|
|
|
/// <summary>면적 — 누적량·점유량처럼 "쌓인 양"을 보여줄 때. 겹쳐 그릴 수 있도록 채움을 24%로 둔다.</summary>
|
|
public static ISeries Area(double[] values, string name, int seriesIndex = 0)
|
|
{
|
|
var color = SeriesColor(seriesIndex);
|
|
return new LineSeries<double>
|
|
{
|
|
Values = values,
|
|
Name = name,
|
|
Stroke = new SolidColorPaint(color, 2),
|
|
Fill = new SolidColorPaint(color.WithAlpha(0x3D)),
|
|
GeometrySize = 0,
|
|
LineSmoothness = 0,
|
|
AnimationsSpeed = AnimationSpeed
|
|
};
|
|
}
|
|
|
|
/// <summary>계단선 — ON/OFF나 단계값처럼 "다음 값까지 그 상태가 유지되는" 신호용.
|
|
/// 이런 데이터를 꺾은선으로 그리면 없는 중간값이 있는 것처럼 보인다.</summary>
|
|
public static ISeries StepLine(double[] values, string name, int seriesIndex = 0)
|
|
{
|
|
var color = SeriesColor(seriesIndex);
|
|
return new StepLineSeries<double>
|
|
{
|
|
Values = values,
|
|
Name = name,
|
|
Stroke = new SolidColorPaint(color, 2),
|
|
Fill = null,
|
|
GeometrySize = 0,
|
|
AnimationsSpeed = AnimationSpeed
|
|
};
|
|
}
|
|
|
|
/// <summary>세로 막대 — 여러 계열용. 각진 모서리는 단일 계열과 같다.</summary>
|
|
public static ISeries Column(double[] values, string name, int seriesIndex)
|
|
=> new ColumnSeries<double>
|
|
{
|
|
Values = values,
|
|
Name = name,
|
|
Fill = new SolidColorPaint(SeriesColor(seriesIndex).WithAlpha(217)),
|
|
Rx = 0,
|
|
Ry = 0,
|
|
AnimationsSpeed = AnimationSpeed
|
|
};
|
|
|
|
/// <summary>누적 막대 — 전체 대비 구성비를 볼 때. 같은 차트에 넣은 누적 막대끼리 쌓인다.</summary>
|
|
public static ISeries StackedColumn(double[] values, string name, int seriesIndex)
|
|
=> new StackedColumnSeries<double>
|
|
{
|
|
Values = values,
|
|
Name = name,
|
|
Fill = new SolidColorPaint(SeriesColor(seriesIndex).WithAlpha(217)),
|
|
Rx = 0,
|
|
Ry = 0,
|
|
AnimationsSpeed = AnimationSpeed
|
|
};
|
|
|
|
/// <summary>가로 막대 — 항목 이름이 길어 세로 막대에서 라벨이 겹칠 때(설비명·불량 유형 등).</summary>
|
|
public static ISeries Bar(double[] values, string name, int seriesIndex = 0)
|
|
=> new RowSeries<double>
|
|
{
|
|
Values = values,
|
|
Name = name,
|
|
Fill = new SolidColorPaint(SeriesColor(seriesIndex).WithAlpha(217)),
|
|
Rx = 0,
|
|
Ry = 0,
|
|
AnimationsSpeed = AnimationSpeed
|
|
};
|
|
|
|
/// <summary>원형 — 구성비. 조각은 팔레트 순서대로, 경계는 판 색으로 얇게 갈라 서로 붙어 보이지 않게 한다.
|
|
/// 조각이 6개를 넘으면 색이 돌아오므로 그때는 가로 막대(Bar)가 낫다.</summary>
|
|
public static ISeries[] Pie(double[] values, string[] names = null)
|
|
=> BuildPie(values, names, innerRadius: 0);
|
|
|
|
/// <summary>도넛 — 원형과 같되 가운데를 비워 총계·제목을 얹을 수 있다.</summary>
|
|
public static ISeries[] Donut(double[] values, string[] names = null)
|
|
=> BuildPie(values, names, innerRadius: 52);
|
|
|
|
private static ISeries[] BuildPie(double[] values, string[] names, double innerRadius)
|
|
{
|
|
var series = new ISeries[values.Length];
|
|
|
|
for (var i = 0; i < values.Length; i++)
|
|
{
|
|
series[i] = new PieSeries<double>
|
|
{
|
|
Values = new[] { values[i] },
|
|
Name = names != null && i < names.Length ? names[i] : string.Empty,
|
|
Fill = new SolidColorPaint(SeriesColor(i)),
|
|
Stroke = new SolidColorPaint(Surface, 2),
|
|
InnerRadius = innerRadius,
|
|
AnimationsSpeed = AnimationSpeed
|
|
};
|
|
}
|
|
|
|
return series;
|
|
}
|
|
|
|
/// <summary>산점 — 두 값의 상관을 볼 때. 점이 겹쳐도 개수를 셀 수 있도록 판 색 테두리를 두른다.</summary>
|
|
public static ISeries Scatter(ObservablePoint[] points, string name, int seriesIndex = 0)
|
|
{
|
|
var color = SeriesColor(seriesIndex);
|
|
return new ScatterSeries<ObservablePoint>
|
|
{
|
|
Values = points,
|
|
Name = name,
|
|
Fill = new SolidColorPaint(color.WithAlpha(0xC8)),
|
|
Stroke = new SolidColorPaint(Surface, 1),
|
|
GeometrySize = 9,
|
|
AnimationsSpeed = AnimationSpeed
|
|
};
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// 툴팁 · 범례 — 차트 컨트롤에 직접 넣어야 하는 값들.
|
|
// 지정하지 않으면 LiveCharts 기본(밝은 바탕 + 검은 글자)이 나와서 다크 테마에서 튄다.
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
public static SolidColorPaint TooltipTextPaint => new SolidColorPaint(Text) { SKTypeface = BodyTypeface };
|
|
|
|
public static SolidColorPaint TooltipBackgroundPaint => new SolidColorPaint(Surface);
|
|
|
|
public static SolidColorPaint LegendTextPaint => new SolidColorPaint(Muted) { SKTypeface = BodyTypeface };
|
|
|
|
public static SolidColorPaint LegendBackgroundPaint => new SolidColorPaint(Surface);
|
|
|
|
/// <summary>툴팁·범례 색을 이 차트에 입힌다. 차트를 만든 뒤와 테마가 바뀐 뒤에 한 번씩 불러주면 된다.</summary>
|
|
public static void Apply(CartesianChart chart)
|
|
{
|
|
if (chart == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
chart.TooltipTextPaint = TooltipTextPaint;
|
|
chart.TooltipBackgroundPaint = TooltipBackgroundPaint;
|
|
chart.TooltipTextSize = 12;
|
|
chart.LegendTextPaint = LegendTextPaint;
|
|
chart.LegendBackgroundPaint = LegendBackgroundPaint;
|
|
chart.LegendTextSize = 12;
|
|
}
|
|
|
|
/// <summary>원형·도넛 차트에도 같은 처리.</summary>
|
|
public static void Apply(PieChart chart)
|
|
{
|
|
if (chart == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
chart.TooltipTextPaint = TooltipTextPaint;
|
|
chart.TooltipBackgroundPaint = TooltipBackgroundPaint;
|
|
chart.TooltipTextSize = 12;
|
|
chart.LegendTextPaint = LegendTextPaint;
|
|
chart.LegendBackgroundPaint = LegendBackgroundPaint;
|
|
chart.LegendTextSize = 12;
|
|
}
|
|
}
|