mirror of
https://gitlab.com/freepascal.org/lazarus/lazarus.git
synced 2025-05-01 14:24:36 +02:00
338 lines
9.5 KiB
ObjectPascal
338 lines
9.5 KiB
ObjectPascal
{
|
|
/***************************************************************************
|
|
TAChartLiveView.pas
|
|
-------------------
|
|
|
|
TChartLiveView is a component optimized for displaying a long array of incoming
|
|
data in a viewport showing only the most recent data while older data are
|
|
flowing out of the viewport to the left.
|
|
|
|
It was created on the basis of the following forum discussions:
|
|
- https://forum.lazarus.freepascal.org/index.php/topic,15037.html
|
|
- https://forum.lazarus.freepascal.org/index.php/topic,50759.0.html
|
|
- https://forum.lazarus.freepascal.org/index.php/topic,55266.html
|
|
|
|
See the file COPYING.modifiedLGPL.txt, included in this distribution,
|
|
for details about the license.
|
|
*****************************************************************************
|
|
|
|
Author: Werner Pamler
|
|
}
|
|
|
|
unit TAChartLiveView;
|
|
|
|
{$mode objfpc}{$H+}
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils, TAGraph, TAChartUtils, TAChartAxis;
|
|
|
|
type
|
|
TChartLiveViewExtentY = (lveAuto, lveFull, lveLogical);
|
|
|
|
{ TChartLiveView }
|
|
|
|
TChartLiveView = class(TComponent)
|
|
private
|
|
type
|
|
TLVAxisRange = record
|
|
Min, Max: double;
|
|
UseMin, UseMax: Boolean;
|
|
end;
|
|
private
|
|
FActive: Boolean;
|
|
FChart: TChart;
|
|
FExtentY: TChartLiveViewExtentY;
|
|
FListener: TListener;
|
|
FViewportSize: Double;
|
|
FAxisRanges: Array of TLVAxisRange;
|
|
procedure FullExtentChanged(Sender: TObject);
|
|
procedure SetActive(const AValue: Boolean);
|
|
procedure SetChart(const AValue: TChart);
|
|
procedure SetExtentY(const AValue: TChartLiveViewExtentY);
|
|
procedure SetViewportSize(const AValue: Double);
|
|
protected
|
|
procedure Notification(AComponent: TComponent; AOperation: TOperation); override;
|
|
procedure UpdateViewport; virtual;
|
|
public
|
|
constructor Create(AOwner: TComponent); override;
|
|
destructor Destroy; override;
|
|
procedure RestoreAxisRange(Axis: TChartAxis);
|
|
procedure RestoreAxisRanges;
|
|
procedure StoreAxisRange(Axis: TChartAxis);
|
|
procedure StoreAxisRanges;
|
|
published
|
|
property Active: Boolean read FActive write SetActive default false;
|
|
property Chart: TChart read FChart write SetChart default nil;
|
|
property ExtentY: TChartLiveViewExtentY read FExtentY write SetExtentY default lveAuto;
|
|
property ViewportSize: double read FViewportSize write SetViewportSize;
|
|
end;
|
|
|
|
procedure Register;
|
|
|
|
|
|
implementation
|
|
|
|
uses
|
|
Math, TAChartAxisUtils, TACustomSeries;
|
|
|
|
constructor TChartLiveView.Create(AOwner: TComponent);
|
|
begin
|
|
inherited;
|
|
FListener := TListener.Create(@FChart, @FullExtentChanged);
|
|
end;
|
|
|
|
destructor TChartLiveView.Destroy;
|
|
begin
|
|
RestoreAxisRanges;
|
|
FreeAndNil(FListener);
|
|
inherited;
|
|
end;
|
|
|
|
{ A new data point has been added to the chart so that the full extent changes.
|
|
As a consequence the viewport of the live view must be updated. }
|
|
procedure TChartLiveView.FullExtentChanged(Sender: TObject);
|
|
begin
|
|
if (not FActive) or (FChart = nil) then
|
|
exit;
|
|
UpdateViewport;
|
|
end;
|
|
|
|
procedure TChartLiveView.Notification(AComponent: TComponent; AOperation: TOperation);
|
|
begin
|
|
if (AOperation = opRemove) and (AComponent = FChart) then
|
|
begin
|
|
SetActive(false);
|
|
FChart := nil;
|
|
end;
|
|
inherited Notification(AComponent, AOperation);
|
|
end;
|
|
|
|
procedure TChartLiveView.RestoreAxisRange(Axis: TChartAxis);
|
|
begin
|
|
if Assigned(Axis) then
|
|
with FAxisRanges[Axis.Index] do begin
|
|
Axis.Range.Max := Max;
|
|
Axis.Range.Min := Min;
|
|
Axis.Range.UseMax := UseMax;
|
|
Axis.Range.UseMin := UseMin;
|
|
end;
|
|
end;
|
|
|
|
{ The ChartLiveView may change the Range properties of an axis. The original
|
|
values, before applying the live view, are restored here from internal
|
|
variables.
|
|
Be careful when calling this procedure in user code, it may disrupt the
|
|
operation of the live view. }
|
|
procedure TChartLiveView.RestoreAxisRanges;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
if FChart = nil then
|
|
exit;
|
|
|
|
for i := 0 to FChart.AxisList.Count-1 do
|
|
RestoreAxisRange(FChart.AxisList[i]);
|
|
end;
|
|
|
|
{ Activates the live view mode. Because the Range of the y axes can be changed
|
|
their current Range is stored before activating, and restored after
|
|
deactivating the mode. }
|
|
procedure TChartLiveView.SetActive(const AValue: Boolean);
|
|
begin
|
|
if FActive = AValue then exit;
|
|
|
|
FActive := AValue;
|
|
if FChart <> nil then
|
|
begin
|
|
if FActive then
|
|
StoreAxisRanges
|
|
else
|
|
RestoreAxisRanges;
|
|
end;
|
|
|
|
FullExtentChanged(nil);
|
|
end;
|
|
|
|
{ Attaches the chart on which the liveview operates. Installs a "listener"
|
|
object so that the liveview can be notified of a change in the chart's full
|
|
extent when a new data point has been added (method FullExtentChanged). }
|
|
procedure TChartLiveView.SetChart(const AValue: TChart);
|
|
begin
|
|
if FChart = AValue then exit;
|
|
|
|
if FListener.IsListening then
|
|
FChart.FullExtentBroadcaster.Unsubscribe(FListener);
|
|
FChart := AValue;
|
|
if FChart <> nil then
|
|
FChart.FullExtentBroadcaster.Subscribe(FListener);
|
|
StoreAxisRanges;
|
|
FullExtentChanged(Self);
|
|
end;
|
|
|
|
procedure TChartLiveview.SetExtentY(const AValue: TChartLiveViewExtentY);
|
|
begin
|
|
if FExtentY = AValue then exit;
|
|
if FExtentY = lveAuto then
|
|
RestoreAxisRanges;
|
|
FExtentY := AValue;
|
|
FullExtentChanged(nil);
|
|
end;
|
|
|
|
procedure TChartLiveView.SetViewportSize(const AValue: Double);
|
|
begin
|
|
if FViewportSize = AValue then exit;
|
|
FViewportSize := AValue;
|
|
FullExtentChanged(nil);
|
|
end;
|
|
|
|
procedure TChartLiveView.StoreAxisRange(Axis: TChartAxis);
|
|
begin
|
|
if Assigned(Axis) then
|
|
with FAxisRanges[Axis.Index] do begin
|
|
Max := Axis.Range.Max;
|
|
Min := Axis.Range.Min;
|
|
UseMax := Axis.Range.UseMax;
|
|
UseMin := Axis.Range.UseMin;
|
|
end;
|
|
end;
|
|
|
|
{ The ChartLiveView may change the Range properties of an axis. The original
|
|
values, before applying the live view, are stored here in internal variables.
|
|
Be careful when calling this procedure in user code, it may disrupt the
|
|
operation of the live view. }
|
|
procedure TChartLiveView.StoreAxisRanges;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
if FChart = nil then
|
|
exit;
|
|
SetLength(FAxisRanges, FChart.AxisList.Count);
|
|
for i := 0 to FChart.AxisList.Count-1 do
|
|
StoreAxisRange(FChart.AxisList[i]);
|
|
end;
|
|
|
|
{ "Workhorse" method of the component. It calculates the logical extent and
|
|
the axis ranges needed to display only the recent data values in the
|
|
given viewport. }
|
|
procedure TChartLiveView.UpdateViewport;
|
|
var
|
|
fext, lext: TDoubleRect; // "full extent", "logical extent" variables
|
|
w: double;
|
|
i, j: Integer;
|
|
ymin, ymax: Double;
|
|
dy: Double;
|
|
ser: TChartSeries;
|
|
axis: TChartAxis;
|
|
begin
|
|
if csDesigning in ComponentState then
|
|
exit;
|
|
|
|
if not FChart.ScalingValid then
|
|
exit;
|
|
|
|
if Length(FAxisRanges) = 0 then
|
|
StoreAxisRanges;
|
|
|
|
fext := FChart.GetFullExtent();
|
|
lext := FChart.LogicalExtent;
|
|
if FViewportSize = 0 then
|
|
w := lext.b.x - lext.a.x
|
|
else
|
|
w := FViewportSize;
|
|
// Move the extent to the right
|
|
lext.b.x := fext.b.x;
|
|
lext.a.x := lext.b.x - w;
|
|
if lext.a.x < fext.a.x then begin
|
|
lext.a.x := fext.a.x;
|
|
lext.b.x := lext.a.x + w;
|
|
end;
|
|
case FExtentY of
|
|
lveAuto:
|
|
// The aim of lveAuto is to determine the y-axis range according to
|
|
// the ymin/ymax of the series connected to the axis
|
|
begin
|
|
lext.a.y := fext.a.y;
|
|
lext.b.y := fext.b.y;
|
|
for i := 0 to FChart.AxisList.Count-1 do
|
|
begin
|
|
axis := FChart.AxisList[i];
|
|
// Ignore x-axes
|
|
if (axis.Alignment in [calTop, calBottom]) then
|
|
Continue;
|
|
ymax := -Infinity;
|
|
ymin := Infinity;
|
|
// Step through all active non-rotated series attached to this axis
|
|
for j := 0 to FChart.SeriesCount-1 do
|
|
begin
|
|
if FChart.Series[j] is TChartSeries then
|
|
begin
|
|
ser := TChartSeries(FChart.Series[j]);
|
|
if (not ser.Active) or (ser.GetAxisY <> axis) or ser.IsRotated then
|
|
continue;
|
|
ser.FindYRange(lext.a.x, lext.b.x, ymin, ymax);
|
|
end;
|
|
end;
|
|
// Only if an axis has no active non-rotated series, we have -infinity
|
|
if ymax > -Infinity then
|
|
begin
|
|
if ymax = ymin then
|
|
begin
|
|
if ymin = 0 then
|
|
begin
|
|
ymin := -1;
|
|
ymax := +1;
|
|
end
|
|
else
|
|
// Set the range to 10% around the value, take care of the sign!
|
|
begin
|
|
dy := abs(ymin) * 0.1;
|
|
ymin := ymin - dy;
|
|
ymax := ymax + dy;
|
|
end;
|
|
end;
|
|
end
|
|
else
|
|
begin
|
|
ymin := -1;
|
|
ymax := +1;
|
|
end;
|
|
// Only if the user did not set its own range we set the axis range
|
|
// determined above.
|
|
if (not FAxisRanges[i].UseMin) then
|
|
begin
|
|
axis.Range.Min := ymin;
|
|
axis.Range.UseMin := true; // we had stored the original UseMin in FAxisRanges
|
|
lext.a.y := Min(lext.a.y, axis.GetTransform.AxisToGraph(ymin));
|
|
end;
|
|
if (not FAxisRanges[i].UseMax) then
|
|
begin
|
|
axis.Range.Max := ymax;
|
|
axis.Range.UseMax := true; // we had stored the original UseMax in FAxisRanges
|
|
lext.b.y := Max(lext.b.y, axis.GetTransform.AxisToGraph(ymax));
|
|
end;
|
|
end; // series loop
|
|
end; // axes loop
|
|
|
|
lveFull:
|
|
begin
|
|
lext.a.y := fext.a.y;
|
|
lext.b.y := fext.b.y;
|
|
end;
|
|
|
|
lveLogical:
|
|
;
|
|
end;
|
|
|
|
FChart.LogicalExtent := lext;
|
|
end;
|
|
|
|
procedure Register;
|
|
begin
|
|
RegisterComponents(CHART_COMPONENT_IDE_PAGE, [TChartLiveView]);
|
|
end;
|
|
|
|
end.
|
|
|