File: tachartliveview.pas

package info (click to toggle)
lazarus 4.0%2Bdfsg-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 275,760 kB
  • sloc: pascal: 2,341,904; xml: 509,420; makefile: 348,726; cpp: 93,608; sh: 3,387; java: 609; perl: 297; sql: 222; ansic: 137
file content (337 lines) | stat: -rw-r--r-- 9,720 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
{
 /***************************************************************************
                              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.