1377 lines
33 KiB
ObjectPascal
1377 lines
33 KiB
ObjectPascal
/// Модуль визуализации на базе 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. |