pascalabcnet/bin/Lib/PlotML.pas

1377 lines
33 KiB
ObjectPascal
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// Модуль визуализации на базе InteractiveDataDisplay (WPF).
/// ВАЖНО:
/// • Требует Windows и WPF.
/// • Использует GAC-сборку InteractiveDataDisplay.WPF.
/// • Не поддерживается на Linux/macOS и в .NET Core без дополнительной настройки.
/// Рекомендуется использовать только в desktop-сценариях.
unit PlotML;
{$reference %GAC%\InteractiveDataDisplay.WPF.dll}
{$reference 'PresentationFramework.dll'}
{$reference 'WindowsBase.dll'}
{$reference 'PresentationCore.dll'}
interface
uses System,
System.Windows,
System.Windows.Controls,
System.Windows.Media,
System.Windows.Shapes,
System.Threading,
System.Windows.Threading,
InteractiveDataDisplay.WPF,
LinearAlgebraML;
type
ApplicationWPF = System.Windows.Application;
WindowWPF = System.Windows.Window;
GridWPF = System.Windows.Controls.Grid;
ChartWPF = InteractiveDataDisplay.WPF.Chart;
LineGraphWPF = InteractiveDataDisplay.WPF.LineGraph;
MarkerGraphWPF = InteractiveDataDisplay.WPF.CircleMarkerGraph;
PlotWPF = InteractiveDataDisplay.WPF.Plot;
BrushWPF = System.Windows.Media.SolidColorBrush;
Colors = System.Windows.Media.Colors;
ColorWPF = System.Windows.Media.Color;
Matrix = LinearAlgebraML.Matrix; // кто-то еще определяет Matrix и Vector поэтому переопределяю здесь
Vector = LinearAlgebraML.Vector;
const DefaultColor = default(ColorWPF);
type
MarkerType = (Circle, Box, Triangle, Diamond, Cross);
Palette = class
public
Colors: array of Color;
constructor Create(params c: array of Color);
end;
PaletteWPF = Palette;
Palettes = static class
private
static paletteDict: Dictionary<string, Palette>;
static constructor;
static procedure InitPalettes;
public
static function &Default: Palette;
static function Pastel: Palette;
static function Dark: Palette;
static function Bright: Palette;
static function Muted: Palette;
static procedure Register(name: string; p: Palette);
static function Get(name: string): Palette;
end;
Figure = class;
Cell = class
private
parentGrid: GridWPF;
row,col: integer;
chart: ChartWPF;
palette: Palette;
paletteIndex: integer := 0;
function NextColor: Color;
procedure EnsureChart;
procedure SetTitle(s: string);
public
constructor Create(g: GridWPF; r,c: integer);
procedure LineGraph(x, y: array of real; color: ColorWPF := DefaultColor;
thickness: real := 2; legend: string := nil);
procedure Points(x, y: array of real; color: ColorWPF := DefaultColor;
size: real := 6; marker: MarkerType := MarkerType.Circle; legend: string := nil);
procedure Points(x, y: array of real; labels: array of integer;
size: real := 6; marker: MarkerType := MarkerType.Circle);
procedure Hist(x: array of real; bins: integer := 0;
color: ColorWPF := DefaultColor; alpha: real := 0.7; legend: string := nil);
procedure Heatmap(m: array[,] of real);
// --- Vector overloads
procedure LineGraph(x, y: Vector; color: ColorWPF := DefaultColor;
thickness: real := 2; legend: string := nil);
procedure Points(x, y: Vector; color: ColorWPF := DefaultColor;
size: real := 6; marker: MarkerType := MarkerType.Circle; legend: string := nil);
procedure Points(x, y: Vector; labels: array of integer;
size: real := 6; marker: MarkerType := MarkerType.Circle);
procedure Hist(x: Vector; bins: integer := 0;
color: ColorWPF := DefaultColor; alpha: real := 0.7; legend: string := nil);
procedure Surface(x1, x2: array of real; nx, ny: integer; f: Matrix -> array of integer; pal: PlotML.Palette := nil);
procedure Heatmap(m: Matrix);
procedure Text(s: string; x: real := 0.5; y: real := 0.5);
procedure SetPalette(p: PaletteWPF);
procedure XLabel(s: string);
procedure YLabel(s: string);
procedure Limits(xmin, xmax, ymin, ymax: real);
procedure XLim(xmin,xmax: real);
procedure YLim(ymin,ymax: real);
procedure Clear;
property Title: string write SetTitle;
end;
Figure = class
private
grid: GridWPF;
cells: array[,] of Cell;
public
constructor Create(rows,cols: integer);
property Item[r,c: integer]: Cell read cells[r,c]; default;
end;
Plot = static class
private
static procedure RunUI(a: procedure);
static function CreateLineSeries(x,y: array of real; c: Color): LineGraphWPF;
static function CreatePointSeries(x, y: array of real;
color: ColorWPF; size: real; marker: MarkerType): MarkerGraphWPF;
static procedure DrawLine(chart: ChartWPF; x, y: array of real;
color: ColorWPF; thickness: real; legend: string);
static procedure DrawText(chart: ChartWPF; s: string; x, y: real);
static procedure DrawPoints(chart: ChartWPF; x, y: array of real;
color: ColorWPF; size: real; marker: MarkerType; legend: string);
static procedure DrawHeatmap(chart: ChartWPF; m: array[,] of real);
static procedure DrawHist(chart: ChartWPF; x: array of real;
bins: integer; color: ColorWPF; alpha: real; legend: string);
static procedure DrawSurface(chart: ChartWPF; labels: array of integer;
nx, ny: integer; xmin, xmax, ymin, ymax: real; pal: Palette);
static function MakeGrid(xmin, xmax, ymin, ymax: real; nx, ny: integer): Matrix;
static procedure SetTitle(s: string);
public
static procedure AddSeries(chart: ChartWPF; series: UIElement);
static procedure LineGraph(x, y: array of real;
color: ColorWPF := DefaultColor; thickness: real := 2; legend: string := nil);
static procedure Points(x, y: array of real;
color: ColorWPF := DefaultColor; size: real := 6; marker: MarkerType := MarkerType.Circle; legend: string := nil);
static procedure Points(x, y: array of real;
labels: array of integer; color: ColorWPF := DefaultColor; size: real := 6; marker: MarkerType := MarkerType.Circle);
static procedure Hist(x: array of real; bins: integer := 0;
color: ColorWPF := DefaultColor; alpha: real := 0.7; legend: string := nil);
static procedure PairPlot(X: array[,] of real; labels: array of integer; names: array of string);
static procedure Heatmap(m: array[,] of real);
static procedure Surface(labels: array of integer; nx, ny: integer; xmin, xmax, ymin, ymax: real; pal: PlotML.Palette := nil);
static procedure Surface(x1, x2: array of real; nx, ny: integer; f: Matrix -> array of integer; pal: PlotML.Palette := nil);
// с матрицами - векторами
static procedure LineGraph(x, y: Vector;
color: ColorWPF := DefaultColor; thickness: real := 2; legend: string := nil)
:= LineGraph(x.Data, y.Data, color, thickness, legend);
static procedure Points(x, y: Vector; color: ColorWPF := DefaultColor; size: real := 6;
marker: MarkerType := MarkerType.Circle; legend: string := nil)
:= Points(x.Data, y.Data, color, size, marker, legend);
static procedure Points(x, y: Vector;
labels: array of integer; color: ColorWPF := DefaultColor; size: real := 6;
marker: MarkerType := MarkerType.Circle)
:= Points(x.Data, y.Data, labels, color, size, marker);
static procedure Hist(x: Vector; bins: integer := 0;
color: ColorWPF := DefaultColor; alpha: real := 0.7; legend: string := nil)
:= Hist(x.Data, bins, color, alpha, legend);
static procedure PairPlot(X: Matrix; labels: array of integer; names: array of string)
:= PairPlot(X.Data, labels, names);
static procedure Heatmap(m: Matrix) := Heatmap(m.Data);
static function Grid(rows,cols: integer): Figure;
static procedure SetPalette(p: PaletteWPF);
static procedure EnsureAxes(chart: ChartWPF);
static procedure Limits(xmin,xmax,ymin,ymax: real);
static procedure XLim(xmin,xmax: real);
static procedure YLim(ymin,ymax: real);
static procedure XLabel(s: string);
static procedure YLabel(s: string);
static procedure SetLabels(title: string := ''; xlabel: string := ''; ylabel: string := '');
static procedure Clear;
static procedure Save(filename: string);
static property Title: string write SetTitle;
end;
HistogramPlot = class(PlotWPF)
private
fBins: List<Polygon>;
fColor: ColorWPF;
fAlpha: real;
fBinsCount: integer;
fDescription: string;
fMaxCount: integer;
public
constructor Create;
procedure SetData(x: array of real);
property Color: ColorWPF read fColor write fColor;
property Alpha: real read fAlpha write fAlpha;
property BinsCount: integer read fBinsCount write fBinsCount;
property Description: string read fDescription write fDescription;
property MaxCount: integer read fMaxCount;
end;
SurfacePlot = class(PlotWPF)
private
fRects: List<Polygon> := new List<Polygon>;
public
procedure SetData(labels: array of integer;
nx, ny: integer; xmin, xmax, ymin, ymax: real; pal: Palette);
end;
implementation
var
uiThread: Thread;
uiDispatcher: Dispatcher;
app: ApplicationWPF;
win: WindowWPF;
rootChart: ChartWPF;
paletteDict: Dictionary<string,Palette>;
currentPalette: Palette;
rootPaletteIndex: integer := 0;
static function Palettes.&Default: Palette := paletteDict['default'];
static function Palettes.Pastel: Palette := paletteDict['pastel'];
static function Palettes.Dark: Palette := paletteDict['dark'];
static function Palettes.Bright: Palette := paletteDict['bright'];
static function Palettes.Muted: Palette := paletteDict['muted'];
static constructor Palettes.Create;
begin
InitPalettes
end;
static procedure Palettes.Register(name: string; p: Palette);
begin
paletteDict[name] := p;
end;
static function Palettes.Get(name: string): Palette;
begin
Result := paletteDict[name];
end;
static procedure Palettes.InitPalettes;
begin
if paletteDict <> nil then exit;
paletteDict := new Dictionary<string,Palette>;
paletteDict['default'] := new Palette(
Color.FromRgb($1f,$77,$b4),
Color.FromRgb($ff,$7f,$0e),
Color.FromRgb($2c,$a0,$2c),
Color.FromRgb($d6,$27,$28),
Color.FromRgb($94,$67,$bd),
Color.FromRgb($8c,$56,$4b),
Color.FromRgb($e3,$77,$c2),
Color.FromRgb($7f,$7f,$7f),
Color.FromRgb($bc,$bd,$22),
Color.FromRgb($17,$be,$cf)
);
paletteDict['pastel'] := new Palette(
Color.FromRgb(141,211,199),
Color.FromRgb(255,255,179),
Color.FromRgb(190,186,218),
Color.FromRgb(251,128,114),
Color.FromRgb(128,177,211),
Color.FromRgb(253,180,98),
Color.FromRgb(179,222,105),
Color.FromRgb(252,205,229)
);
paletteDict['dark'] := new Palette(
Color.FromRgb(27,158,119),
Color.FromRgb(217,95,2),
Color.FromRgb(117,112,179),
Color.FromRgb(231,41,138),
Color.FromRgb(102,166,30),
Color.FromRgb(230,171,2),
Color.FromRgb(166,118,29),
Color.FromRgb(102,102,102)
);
paletteDict['bright'] := new Palette(
Color.FromRgb(0,114,178),
Color.FromRgb(230,159,0),
Color.FromRgb(0,158,115),
Color.FromRgb(213,94,0),
Color.FromRgb(204,121,167),
Color.FromRgb(86,180,233)
);
paletteDict['muted'] := new Palette(
Color.FromRgb(76,114,176),
Color.FromRgb(221,132,82),
Color.FromRgb(85,168,104),
Color.FromRgb(196,78,82),
Color.FromRgb(129,114,179),
Color.FromRgb(147,120,96)
);
currentPalette := paletteDict['default'];
end;
function CreateMarker(t: MarkerType): InteractiveDataDisplay.WPF.ColorSizeMarker;
begin
case t of
MarkerType.Circle: Result := new CircleMarker;
MarkerType.Box: Result := new BoxMarker;
MarkerType.Triangle: Result := new TriangleMarker;
MarkerType.Diamond: Result := new DiamondMarker;
MarkerType.Cross: Result := new CrossMarker;
end;
end;
function NextRootColor: ColorWPF;
begin
var c := currentPalette.Colors[
rootPaletteIndex mod currentPalette.Colors.Length
];
rootPaletteIndex += 1;
Result := c;
end;
function MakeHistogram(data: array of real; bins: integer): (array of real, array of real);
begin
var xmin := data.Min;
var xmax := data.Max;
var h := (xmax-xmin)/bins;
var counts := new real[bins];
foreach var v in data do
begin
var k := integer((v-xmin)/h);
if k=bins then k := bins-1;
counts[k] += 1;
end;
var xs := ArrGen(bins, i -> xmin + (i+0.5)*h);
Result := (xs,counts);
end;
procedure InitUI;
begin
uiThread := new Thread(() ->
begin
app := new ApplicationWPF;
app.Dispatcher.UnhandledException += (o,e) ->
begin
Println(e.Exception.Message);
if e.Exception.InnerException <> nil then
Println(e.Exception.InnerException.Message);
halt;
end;
rootChart := new ChartWPF;
rootChart.Margin := new Thickness(2);
win := new WindowWPF;
win.Title := 'PlotML';
win.Width := 800;
win.Height := 600;
win.Content := rootChart;
win.Closed += (o,e) ->
Dispatcher.CurrentDispatcher.BeginInvokeShutdown(
DispatcherPriority.Normal);
uiDispatcher := Dispatcher.CurrentDispatcher;
Palettes.InitPalettes;
win.Show;
Dispatcher.Run;
end);
uiThread.SetApartmentState(ApartmentState.STA);
uiThread.Start;
while uiDispatcher = nil do
Sleep(10);
end;
constructor Palette.Create(params c: array of Color);
begin
Colors := c;
end;
constructor Cell.Create(g: GridWPF; r,c: integer);
begin
parentGrid := g;
row := r;
col := c;
palette := currentPalette;
end;
function Cell.NextColor: ColorWPF;
begin
var c := palette.Colors[
paletteIndex mod palette.Colors.Length
];
paletteIndex += 1;
Result := c;
end;
procedure Cell.SetPalette(p: PaletteWPF);
begin
if p = nil then exit;
palette := p;
paletteIndex := 0;
end;
procedure Cell.SetTitle(s: string);
begin
Plot.RunUI(() ->
begin
EnsureChart;
chart.Title := s;
end);
end;
procedure Cell.XLabel(s: string);
begin
Plot.RunUI(() ->
begin
EnsureChart;
chart.BottomTitle := s;
end);
end;
procedure Cell.YLabel(s: string);
begin
Plot.RunUI(() ->
begin
EnsureChart;
chart.LeftTitle := s;
end);
end;
procedure Cell.Limits(xmin,xmax,ymin,ymax: real);
begin
Plot.RunUI(() ->
begin
EnsureChart;
chart.PlotOriginX := xmin;
chart.PlotOriginY := ymin;
chart.PlotWidth := xmax - xmin;
chart.PlotHeight := ymax - ymin;
end);
end;
procedure Cell.XLim(xmin,xmax: real);
begin
Plot.RunUI(() ->
begin
EnsureChart;
chart.PlotOriginX := xmin;
chart.PlotWidth := xmax - xmin;
end);
end;
procedure Cell.YLim(ymin,ymax: real);
begin
Plot.RunUI(() ->
begin
EnsureChart;
chart.PlotOriginY := ymin;
chart.PlotHeight := ymax - ymin;
end);
end;
procedure Cell.Clear;
begin
Plot.RunUI(() ->
begin
if chart = nil then exit;
var container := chart.Content as GridWPF;
if container <> nil then
container.Children.Clear;
paletteIndex := 0;
end);
end;
procedure Cell.EnsureChart;
begin
if chart <> nil then exit;
chart := new ChartWPF;
chart.Margin := new Thickness(2);
chart.LegendVisibility := Visibility.Hidden;
var container := new GridWPF;
chart.Content := container;
GridWPF.SetRow(chart,row);
GridWPF.SetColumn(chart,col);
parentGrid.Children.Add(chart);
end;
procedure Cell.LineGraph(x, y: array of real; color: ColorWPF;
thickness: real; legend: string);
begin
Plot.RunUI(() ->
begin
EnsureChart;
var clr := if color<>DefaultColor then color else NextColor;
Plot.DrawLine(chart, x, y, clr, thickness, legend);
end);
end;
procedure Cell.Points(x, y: array of real; color: ColorWPF;
size: real; marker: MarkerType; legend: string);
begin
Plot.RunUI(() ->
begin
EnsureChart;
var clr := if color<>DefaultColor then color else NextColor;
Plot.DrawPoints(chart, x, y, clr, size, marker, legend);
end);
end;
procedure Cell.Points(x, y: array of real; labels: array of integer;
size: real; marker: MarkerType);
begin
if (x = nil) or (y = nil) or (labels = nil) then
raise new System.ArgumentNullException;
if (x.Length <> y.Length) or (x.Length <> labels.Length) then
raise new System.ArgumentException('Points: array sizes mismatch');
var classes := labels.Distinct.ToArray;
&Array.Sort(classes);
var pal := palette;
foreach var c in classes do
begin
var ind := labels.Indices(v -> v = c).ToArray;
var xs := ind.ConvertAll(i -> x[i]);
var ys := ind.ConvertAll(i -> y[i]);
var clr := pal.Colors[c mod pal.Colors.Length];
self.Points(xs, ys, clr, size, marker, nil);
end;
end;
procedure Cell.Heatmap(m: array[,] of real);
begin
Plot.RunUI(() ->
begin
EnsureChart;
Plot.DrawHeatmap(chart, m);
end);
end;
procedure Cell.Hist(x: array of real; bins: integer; color: ColorWPF; alpha: real; legend: string);
begin
Plot.RunUI(() ->
begin
EnsureChart;
var clr := if color<>DefaultColor then color else NextColor;
Plot.DrawHist(chart, x, bins, clr, alpha, legend);
end);
end;
procedure Cell.LineGraph(x, y: Vector; color: ColorWPF; thickness: real; legend: string);
begin
LineGraph(x.Data, y.Data, color, thickness, legend);
end;
procedure Cell.Points(x, y: Vector; color: ColorWPF; size: real; marker: MarkerType; legend: string);
begin
Points(x.Data, y.Data, color, size, marker, legend);
end;
procedure Cell.Points(x, y: Vector; labels: array of integer; size: real; marker: MarkerType);
begin
Points(x.Data, y.Data, labels, size, marker);
end;
procedure Cell.Heatmap(m: Matrix);
begin
Heatmap(m.Data);
end;
procedure Cell.Hist(x: Vector; bins: integer;
color: ColorWPF; alpha: real; legend: string);
begin
Hist(x.Data, bins, color, alpha, legend);
end;
procedure Cell.Text(s: string; x: real; y: real);
begin
Plot.RunUI(() ->
begin
EnsureChart;
Plot.DrawText(chart, s, x, y);
end);
end;
constructor Figure.Create(rows,cols: integer);
begin
grid := new GridWPF;
for var i:=0 to rows-1 do
grid.RowDefinitions.Add(new RowDefinition);
for var j:=0 to cols-1 do
grid.ColumnDefinitions.Add(new ColumnDefinition);
cells := new Cell[rows,cols];
for var i:=0 to rows-1 do
for var j:=0 to cols-1 do
cells[i,j] := new Cell(grid,i,j);
end;
static procedure Plot.RunUI(a: procedure);
begin
if Dispatcher.CurrentDispatcher = uiDispatcher then
a()
else
uiDispatcher.Invoke(a);
end;
static procedure Plot.Limits(xmin,xmax,ymin,ymax: real);
begin
RunUI(() ->
begin
rootChart.PlotOriginX := xmin;
rootChart.PlotOriginY := ymin;
rootChart.PlotWidth := xmax - xmin;
rootChart.PlotHeight := ymax - ymin;
end);
end;
static procedure Plot.XLim(xmin,xmax: real);
begin
RunUI(() ->
begin
rootChart.PlotOriginX := xmin;
rootChart.PlotWidth := xmax - xmin;
end);
end;
static procedure Plot.YLim(ymin,ymax: real);
begin
RunUI(() ->
begin
rootChart.PlotOriginY := ymin;
rootChart.PlotHeight := ymax - ymin;
end);
end;
var gridMode := false;
static procedure Plot.SetTitle(s: string);
begin
RunUI(() ->
begin
if gridMode then
win.Title := s
else
rootChart.Title := s;
end);
end;
static procedure Plot.XLabel(s: string);
begin
RunUI(() ->
begin
rootChart.BottomTitle := s;
end);
end;
static procedure Plot.YLabel(s: string);
begin
RunUI(() ->
begin
rootChart.LeftTitle := s;
end);
end;
static procedure Plot.SetLabels(title: string; xlabel: string; ylabel: string);
begin
RunUI(() ->
begin
if title <> '' then
rootChart.Title := title;
if xlabel <> '' then
rootChart.BottomTitle := xlabel;
if ylabel <> '' then
rootChart.LeftTitle := ylabel;
end);
end;
static procedure Plot.Clear;
begin
RunUI(() ->
begin
var container := rootChart.Content as GridWPF;
if container <> nil then
container.Children.Clear;
rootPaletteIndex := 0;
end);
end;
static procedure Plot.Save(filename: string);
begin
RunUI(() ->
begin
var rtb := new System.Windows.Media.Imaging.RenderTargetBitmap(
integer(win.Width),
integer(win.Height),
96, 96,
PixelFormats.Pbgra32
);
rtb.Render(win);
var encoder := new System.Windows.Media.Imaging.PngBitmapEncoder;
encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(rtb));
var fs := new System.IO.FileStream(filename, System.IO.FileMode.Create);
encoder.Save(fs);
fs.Close;
end);
end;
static function Plot.CreateLineSeries(x,y: array of real; c: Color): LineGraphWPF;
begin
var g := new LineGraphWPF;
g.Stroke := new BrushWPF(c);
g.StrokeThickness := 2;
g.Plot(x,y);
Result := g;
end;
static function Plot.CreatePointSeries(x, y: array of real;
color: ColorWPF; size: real; marker: MarkerType): MarkerGraphWPF;
begin
var g := new MarkerGraphWPF;
{var alpha := 0.5;
var a := round(alpha * 255);
var c := ColorWPF.FromArgb(a, color.R, color.G, color.B);
g.Color := c;}
g.Color := color;
g.Size := size;
g.MarkerType := CreateMarker(marker);
g.StrokeThickness := 0;
g.PlotColor(x,y,color);
Result := g;
end;
static procedure Plot.DrawLine(chart: ChartWPF; x, y: array of real;
color: ColorWPF; thickness: real; legend: string);
begin
var g := CreateLineSeries(x, y, color);
g.StrokeThickness := thickness;
if legend <> nil then
begin
g.Description := legend;
chart.LegendVisibility := Visibility.Visible;
end;
AddSeries(chart, g);
end;
static procedure Plot.DrawText(chart: ChartWPF; s: string; x, y: real);
begin
var tb := new System.Windows.Controls.TextBlock;
tb.Text := s;
tb.FontSize := 14;
tb.FontWeight := System.Windows.FontWeights.Bold;
tb.HorizontalAlignment := System.Windows.HorizontalAlignment.Center;
tb.VerticalAlignment := System.Windows.VerticalAlignment.Center;
var grid := chart.Parent as System.Windows.Controls.Grid;
if grid <> nil then
begin
tb.HorizontalAlignment := System.Windows.HorizontalAlignment.Center;
tb.VerticalAlignment := System.Windows.VerticalAlignment.Center;
grid.Children.Add(tb);
end;
end;
static procedure Plot.DrawPoints(chart: ChartWPF; x, y: array of real;
color: ColorWPF; size: real; marker: MarkerType; legend: string);
begin
var g := CreatePointSeries(x, y, color, size, marker);
if legend <> nil then
begin
g.Description := legend;
chart.LegendVisibility := Visibility.Visible;
end;
AddSeries(chart, g);
end;
static procedure Plot.DrawHeatmap(chart: ChartWPF; m: array[,] of real);
begin
var rows := m.GetLength(0);
var cols := m.GetLength(1);
var x := ArrGen(cols, i -> i);
var y := ArrGen(rows, i -> i);
var g := new HeatmapGraph;
g.Plot(m, x, y);
AddSeries(chart, g);
end;
class procedure Plot.AddSeries(chart: ChartWPF; series: UIElement);
begin
var container := chart.Content as GridWPF;
if container = nil then
begin
container := new GridWPF;
chart.Content := container;
end;
container.Children.Add(series);
end;
class procedure Plot.LineGraph(x, y: array of real;
color: ColorWPF; thickness: real; legend: string);
begin
RunUI(() ->
begin
var clr := if color<>DefaultColor then color else NextRootColor;
DrawLine(rootChart, x, y, clr, thickness, legend);
end);
end;
static procedure Plot.Points(x, y: array of real;
color: ColorWPF; size: real; marker: MarkerType; legend: string);
begin
RunUI(() ->
begin
var clr := if color<>DefaultColor then color else NextRootColor;
DrawPoints(rootChart, x, y, clr, size, marker, legend);
end);
end;
static procedure Plot.Points(x, y: array of real; labels: array of integer;
color: ColorWPF; size: real; marker: MarkerType);
begin
if (x = nil) or (y = nil) or (labels = nil) then
raise new System.ArgumentNullException;
if (x.Length <> y.Length) or (x.Length <> labels.Length) then
raise new System.ArgumentException('Points: array sizes mismatch');
var classes := labels.Distinct.ToArray;
&Array.Sort(classes);
var pal := CurrentPalette;
foreach var c in classes do
begin
var ind := labels.Indices(v -> v = c).ToArray;
var xs := ind.ConvertAll(i -> x[i]);
var ys := ind.ConvertAll(i -> y[i]);
var clr :=
if color<>DefaultColor
then color
else pal.Colors[c mod pal.Colors.Length];
Points(xs, ys, clr, size, marker, nil);
end;
end;
static procedure Plot.Heatmap(m: array[,] of real);
begin
RunUI(() ->
begin
DrawHeatmap(rootChart, m);
end);
end;
// --- Vector overloads
{static procedure Plot.LineGraph(x, y: Vector;
color: ColorWPF; thickness: real; legend: string);
begin
LineGraph(x.Data, y.Data, color, thickness, legend);
end;
static procedure Plot.Points(x, y: Vector;
color: ColorWPF; size: real; marker: MarkerType; legend: string);
begin
Points(x.Data, y.Data, color, size, marker, legend);
end;
static procedure Plot.Points(x, y: Vector;
labels: array of integer;
color: ColorWPF; size: real; marker: MarkerType);
begin
Points(x.Data, y.Data, labels, color, size, marker);
end;
static procedure Plot.Hist(x: Vector; bins: integer;
color: ColorWPF; alpha: real; legend: string);
begin
Hist(x.Data, bins, color, alpha, legend);
end;
// --- Matrix overloads
static procedure Plot.Heatmap(m: Matrix);
begin
Heatmap(m.Data);
end;
static procedure Plot.PairPlot(X: Matrix; labels: array of integer; names: array of string);
begin
PairPlot(X.Data, labels, names);
end;}
static function Plot.Grid(rows,cols: integer): Figure;
begin
var fig: Figure;
RunUI(() ->
begin
fig := new Figure(rows,cols);
win.Content := fig.grid;
gridMode := true;
end);
Result := fig;
end;
static procedure Plot.SetPalette(p: PaletteWPF);
begin
RunUI(() ->
begin
if p = nil then exit;
currentPalette := p;
end);
end;
function HistogramCounts(x: array of real; bins: integer; xmin, xmax: real): array of integer;
begin
var counts := new integer[bins];
var w := (xmax-xmin)/bins;
foreach var v in x do
begin
var k := trunc((v-xmin)/w);
if k>=bins then k := bins-1;
if k<0 then k := 0;
counts[k] += 1;
end;
Result := counts;
end;
class procedure Plot.PairPlot(X: array[,] of real; labels: array of integer; names: array of string);
begin
var n := names.Length;
var bins := 20;
var fig := Plot.Grid(n,n);
var xmin := new real[n];
var xmax := new real[n];
var ymax := new real[n];
// диапазоны
for var j := 0 to n-1 do
begin
var col := X.Col(j);
xmin[j] := Floor(col.Min);
xmax[j] := Ceil(col.Max);
var counts := HistogramCounts(col,bins,xmin[j],xmax[j]);
ymax[j] := counts.Max*1.1;
end;
for var i := 0 to n-1 do
for var j := 0 to n-1 do
begin
var ax := fig[i,j];
if i=j then
begin
ax.Hist(X.Col(i),bins:=bins);
ax.XLim(xmin[j],xmax[j]);
ax.YLim(0,ymax[j]);
end
else
begin
var xs := X.Col(j);
var ys := X.Col(i);
if labels = nil then
ax.Points(xs, ys, size := 3)
else
ax.Points(xs, ys, labels, size := 3);
ax.XLim(xmin[j],xmax[j]);
ax.YLim(xmin[i],xmax[i]);
end;
// подписи только по краям
if i=n-1 then
ax.XLabel(names[j]);
if j=0 then
ax.YLabel(names[i]);
end;
end;
class procedure Plot.EnsureAxes(chart: ChartWPF);
begin
var container := chart.Content as GridWPF;
if container = nil then
begin
container := new GridWPF;
chart.Content := container;
end;
foreach var el in container.Children do
if el is LineGraphWPF then
exit;
var g := new LineGraphWPF;
g.StrokeThickness := 0;
g.Plot(Arr(0.0, 1.0), Arr(0.0, 1.0));
container.Children.Add(g);
end;
static procedure Plot.Hist(x: array of real; bins: integer;
color: ColorWPF; alpha: real; legend: string);
begin
RunUI(() ->
begin
Plot.DrawHist(rootChart, x, bins, color, alpha, legend);
end);
end;
static procedure Plot.DrawHist(chart: ChartWPF; x: array of real;
bins: integer; color: ColorWPF; alpha: real; legend: string);
begin
EnsureAxes(chart);
var hist := new HistogramPlot;
if bins = 0 then
bins := Round(Sqrt(x.Length));
hist.BinsCount := bins;
hist.Color := if color<>DefaultColor then color else NextRootColor;
hist.Alpha := alpha;
hist.SetData(x);
// после этого известен hist.MaxCount
if legend<>nil then
begin
hist.Description := legend;
chart.LegendVisibility := Visibility.Visible;
end;
AddSeries(chart, hist);
var xmin := Floor(x.Min);
var xmax := Ceil(x.Max);
chart.PlotOriginX := xmin;
chart.PlotWidth := xmax - xmin;
chart.PlotOriginY := 0;
chart.PlotHeight := hist.MaxCount * 1.1;
end;
constructor HistogramPlot.Create;
begin
fBins := new List<Polygon>;
fColor := Colors.SteelBlue;
fAlpha := 0.7;
fBinsCount := 20;
IsAutoFitEnabled := false;
end;
procedure HistogramPlot.SetData(x: array of real);
begin
Children.Clear;
fBins.Clear;
if x=nil then exit;
if x.Length=0 then exit;
var xmin := Floor(x.Min);
var xmax := Ceil(x.Max);
if xmax=xmin then exit;
var w := (xmax-xmin)/fBinsCount;
var counts := new integer[fBinsCount];
foreach var v in x do
begin
var k := trunc((v-xmin)/w);
if k>=fBinsCount then k := fBinsCount-1;
if k<0 then k := 0;
counts[k] += 1;
end;
var brush := new SolidColorBrush(fColor);
brush.Opacity := fAlpha;
for var i:=0 to fBinsCount-1 do
begin
var x0 := xmin + i*w;
var x1 := x0 + w;
var h := counts[i];
var poly := new Polygon;
var pts := new PointCollection;
pts.Add(new Point(x0,0));
pts.Add(new Point(x0,h));
pts.Add(new Point(x1,h));
pts.Add(new Point(x1,0));
PlotWPF.SetPoints(poly, pts);
poly.Fill := brush;
poly.Stroke := Brushes.Black;
poly.StrokeThickness := 0.5;
fBins.Add(poly);
Children.Add(poly);
end;
fMaxCount := counts.Max;
end;
procedure SurfacePlot.SetData(labels: array of integer;
nx, ny: integer; xmin, xmax, ymin, ymax: real; pal: Palette);
begin
Children.Clear;
var dx := (xmax - xmin) / nx;
var dy := (ymax - ymin) / ny;
var k := 0;
for var iy := 0 to ny - 1 do
for var ix := 0 to nx - 1 do
begin
var lab := labels[k];
k += 1;
var clr := pal.Colors[Abs(lab) mod pal.Colors.Length];
var brush := new SolidColorBrush(clr);
var x0 := xmin + ix * dx;
var y0 := ymin + iy * dy;
var eps := dx * 0.01;
var x1 := x0 + dx + eps;
var y1 := y0 + dy + eps;
var poly := new Polygon;
var pts := new PointCollection;
pts.Add(new Point(x0, y0));
pts.Add(new Point(x0, y1));
pts.Add(new Point(x1, y1));
pts.Add(new Point(x1, y0));
PlotWPF.SetPoints(poly, pts);
poly.Fill := brush;
poly.StrokeThickness := 0;
Children.Add(poly);
end;
end;
static procedure Plot.DrawSurface(chart: ChartWPF;
labels: array of integer; nx, ny: integer;
xmin, xmax, ymin, ymax: real;
pal: Palette);
begin
EnsureAxes(chart);
var s := new SurfacePlot;
s.SetData(labels, nx, ny, xmin, xmax, ymin, ymax, pal);
AddSeries(chart, s);
end;
static function Plot.MakeGrid(xmin, xmax, ymin, ymax: real; nx, ny: integer): Matrix;
begin
Result := new Matrix(nx * ny, 2);
var k := 0;
for var iy := 0 to ny - 1 do
begin
var y := ymin + (ymax - ymin) * iy / (ny - 1);
for var ix := 0 to nx - 1 do
begin
var x := xmin + (xmax - xmin) * ix / (nx - 1);
Result[k, 0] := x;
Result[k, 1] := y;
k += 1;
end;
end;
end;
procedure Cell.Surface(x1, x2: array of real; nx, ny: integer; f: Matrix -> array of integer; pal: PlotML.Palette);
begin
Plot.RunUI(() ->
begin
EnsureChart;
var xmin := x1.Min - 0.5;
var xmax := x1.Max + 0.5;
var ymin := x2.Min - 0.5;
var ymax := x2.Max + 0.5;
var G := Plot.MakeGrid(xmin, xmax, ymin, ymax, nx, ny);
var labels := f(G);
var usePal := if pal <> nil then pal else palette;
Plot.DrawSurface(chart, labels, nx, ny, xmin, xmax, ymin, ymax, usePal);
end);
end;
static procedure Plot.Surface(labels: array of integer; nx, ny: integer; xmin, xmax, ymin, ymax: real; pal: PlotML.Palette);
begin
RunUI(() ->
begin
var usePal := if pal <> nil then pal else CurrentPalette;
DrawSurface(rootChart, labels, nx, ny, xmin, xmax, ymin, ymax, usePal);
end);
end;
static procedure Plot.Surface(x1, x2: array of real; nx, ny: integer; f: Matrix -> array of integer; pal: PlotML.Palette);
begin
RunUI(() ->
begin
var xmin := x1.Min - 0.5;
var xmax := x1.Max + 0.5;
var ymin := x2.Min - 0.5;
var ymax := x2.Max + 0.5;
var G := MakeGrid(xmin, xmax, ymin, ymax, nx, ny);
var labels := f(G);
var usePal := if pal <> nil then pal else CurrentPalette;
DrawSurface(rootChart, labels, nx, ny, xmin, xmax, ymin, ymax, usePal);
end);
end;
initialization
InitUI;
end.