TAChart: Rework of sorting by TCustomChartSource and TListChartSource. Issue #35356, patch by Marcin Wiazowski

git-svn-id: trunk@61248 -
This commit is contained in:
wp 2019-05-20 17:32:47 +00:00
parent 71fad749a4
commit 0fce68a18e
2 changed files with 251 additions and 127 deletions

View File

@ -212,6 +212,7 @@ type
out AUpperDelta, ALowerDelta: Double): Boolean;
function GetHasErrorBars(Which: Integer): Boolean;
function GetItem(AIndex: Integer): PChartDataItem; virtual; abstract;
function HasSameSorting(ASource: TCustomChartSource): Boolean; virtual;
procedure InvalidateCaches;
procedure SetSortBy(AValue: TChartSortBy); virtual;
procedure SetSortDir(AValue: TChartSortDir); virtual;
@ -283,14 +284,16 @@ type
procedure SetOnCompare(AValue: TChartSortCompare);
procedure SetSorted(AValue: Boolean);
protected
FCompareProc: TChartSortCompare;
FData: TFPList;
FSorted: Boolean;
function DefaultCompare(AItem1, AItem2: Pointer): Integer; virtual;
function DoCompare(AItem1, AItem2: Pointer): Integer; virtual;
procedure ExecSort(ACompare: TChartSortCompare); virtual;
procedure DoSort; virtual;
function GetCount: Integer; override;
function GetItem(AIndex: Integer): PChartDataItem; override;
function ItemAdd(AItem: PChartDataItem): Integer;
procedure ItemInsert(AIndex: Integer; AItem: PChartDataItem);
function ItemFind(AItem: PChartDataItem; L: Integer = 0; R: Integer = High(Integer)): Integer;
function ItemModified(AIndex: Integer): Integer;
procedure SetSortBy(AValue: TChartSortBy); override;
procedure SetSortDir(AValue: TChartSortDir); override;
procedure SetSortIndex(AValue: Cardinal); override;
@ -1298,6 +1301,20 @@ begin
end;
end;
function TCustomChartSource.HasSameSorting(ASource: TCustomChartSource): Boolean;
begin
case SortBy of
sbX, sbY:
Result := ASource.IsSorted and (ASource.SortBy = SortBy) and
(ASource.SortDir = SortDir) and (ASource.SortIndex = SortIndex);
sbColor, sbText:
Result := ASource.IsSorted and (ASource.SortBy = SortBy) and
(ASource.SortDir = SortDir);
sbCustom:
Result := false;
end;
end;
function TCustomChartSource.HasXErrorBars: Boolean;
begin
Result := GetHasErrorBars(0);
@ -1596,71 +1613,129 @@ begin
Result := CompareValue(x1, x2);
end;
function TCustomSortedChartSource.DefaultCompare(AItem1, AItem2: Pointer): Integer;
function TCustomSortedChartSource.DoCompare(AItem1, AItem2: Pointer): Integer;
var
item1: PChartDataItem absolute AItem1;
item2: PChartDataItem absolute AItem2;
d1, d2: Double;
begin
case FSortBy of
sbX: Result := CompareFloat(item1^.GetX(FSortIndex), item2^.GetX(FSortIndex));
sbY: Result := CompareFloat(item1^.GetY(FSortIndex), item2^.GetY(FSortIndex));
sbColor: Result := CompareValue(item1^.Color, item2^.Color);
sbText: Result := CompareText(item1^.Text, item2^.Text);
sbCustom: Result := FOnCompare(AItem1, AItem2);
end;
if FSortDir = sdDescending then Result := -Result;
case FSortBy of
sbX:
if FSortIndex = 0 then
Result := CompareFloat(item1^.X, item2^.X)
else
if FSortIndex < FXCount then begin
if FSortIndex <= Cardinal(Length(item1^.XList)) then
d1 := item1^.XList[FSortIndex - 1]
else
d1 := SafeNan;
if FSortIndex <= Cardinal(Length(item2^.XList)) then
d2 := item2^.XList[FSortIndex - 1]
else
d2 := SafeNan;
Result := CompareFloat(d1, d2);
end else
Result := 0;
sbY:
if FSortIndex = 0 then
Result := CompareFloat(item1^.Y, item2^.Y)
else
if FSortIndex < FYCount then begin
if FSortIndex <= Cardinal(Length(item1^.YList)) then
d1 := item1^.YList[FSortIndex - 1]
else
d1 := SafeNan;
if FSortIndex <= Cardinal(Length(item2^.YList)) then
d2 := item2^.YList[FSortIndex - 1]
else
d2 := SafeNan;
Result := CompareFloat(d1, d2);
end else
Result := 0;
sbColor:
Result := CompareValue(item1^.Color, item2^.Color);
sbText:
Result := CompareText(item1^.Text, item2^.Text);
sbCustom:
if Assigned(FOnCompare) then
Result := FOnCompare(AItem1, AItem2)
else
Result := 0;
end;
if FSortDir = sdDescending then Result := -Result;
end;
function TCustomSortedChartSource.DoCompare(AItem1, AItem2: Pointer): Integer;
begin
Result := FCompareProc(AItem1, AItem2);
end;
{ Built-in sorting algorithm of the ChartSource - a QuickSort algorithm, copied
from the Classes unit and modified. Modifications are:
- uses a DoCompare() virtual method for comparisons,
- does NOT exchange equal items - this would have some side effect here: let's
consider sorting by X, in the ascending order, for the following data points:
X=3, Text='ccc'
X=2, Text='bbb 1'
X=2, Text='bbb 2'
X=2, Text='bbb 3'
X=1, Text='aaa'
{ Built-in sorting algorithm of the ChartSource, a standard QuickSort.
Copied from the classes unit because the compare function must be a method. }
procedure TCustomSortedChartSource.ExecSort(ACompare: TChartSortCompare);
after sorting, data would be (note the reversed 'bbb' order):
X=1, Text='aaa'
X=2, Text='bbb 3'
X=2, Text='bbb 2'
X=2, Text='bbb 1'
X=3, Text='ccc'
after sorting AGAIN, data would be (note the original 'bbb' order):
X=1, Text='aaa'
X=2, Text='bbb 1'
X=2, Text='bbb 2'
X=2, Text='bbb 3'
X=3, Text='ccc'
}
procedure TCustomSortedChartSource.DoSort;
procedure QuickSort(L, R: Longint);
var
I, J: Longint;
P, Q: Pointer;
begin
repeat
I := L;
J := R;
P := FData.List^[(L + R) div 2];
repeat
while ACompare(P, FData.List^[I]) > 0 do
I := I + 1;
while ACompare(P, FData.List^[J]) < 0 do
J := J - 1;
If I <= J then
begin
Q := FData.List^[I];
FData.List^[I] := FData.List^[J];
FData.List^[J] := Q;
I := I + 1;
J := J - 1;
end;
until I > J;
if J - L < R - I then
begin
if L < J then
QuickSort(L, J);
L := I;
end
else
begin
if I < R then
QuickSort(I, R);
R := J;
end;
until L >= R;
repeat
I := L;
J := R;
P := FData.List^[(L + R) div 2];
repeat
while DoCompare(P, FData.List^[I]) > 0 do
I := I + 1;
while DoCompare(P, FData.List^[J]) < 0 do
J := J - 1;
if I <= J then
begin
// do NOT exchange equal items
if DoCompare(FData.List^[I], FData.List^[J]) <> 0 then begin
Q := FData.List^[I];
FData.List^[I] := FData.List^[J];
FData.List^[J] := Q;
end;
I := I + 1;
J := J - 1;
end;
until I > J;
if J - L < R - I then
begin
if L < J then
QuickSort(L, J);
L := I;
end
else
begin
if I < R then
QuickSort(I, R);
R := J;
end;
until L >= R;
end;
begin
if FData.Count < 2 then exit;
QuickSort(0, FData.Count-1);
QuickSort(0, FData.Count - 1);
end;
function TCustomSortedChartSource.GetCount: Integer;
@ -1673,6 +1748,78 @@ begin
Result := PChartDataItem(FData.Items[AIndex]);
end;
function TCustomSortedChartSource.ItemAdd(AItem: PChartDataItem): Integer;
begin
if IsSorted then begin
Result := ItemFind(AItem);
FData.Insert(Result, AItem);
end else
Result := FData.Add(AItem);
end;
procedure TCustomSortedChartSource.ItemInsert(AIndex: Integer; AItem: PChartDataItem);
begin
if IsSorted then
if AIndex <> ItemFind(AItem) then
raise ESortError.CreateFmt('%0:s.ItemInsert cannot insert data at the requested '+
'position, because source is sorted', [ClassName]);
FData.Insert(AIndex, AItem);
end;
function TCustomSortedChartSource.ItemFind(AItem: PChartDataItem; L: Integer = 0; R: Integer = High(Integer)): Integer;
var
I: Integer;
begin
if not IsSorted then
raise ESortError.CreateFmt('%0:s.ItemFind can be called only for sorted source', [ClassName]);
if R >= FData.Count then
R := FData.Count - 1;
// special optimization for adding sorted data at the end
if R >= 0 then
if DoCompare(FData.List^[R], AItem) <= 0 then
exit(R + 1);
// use binary search
if L < 0 then
L := 0;
while L <= R do
begin
I := L + (R - L) div 2;
if DoCompare(FData.List^[I], AItem) <= 0 then
L := I + 1
else
R := I - 1;
end;
Result := L;
end;
function TCustomSortedChartSource.ItemModified(AIndex: Integer): Integer;
begin
Result := AIndex;
if IsSorted then begin
if FData.Count < 2 then exit;
if (AIndex < 0) or (AIndex >= FData.Count) then exit;
if AIndex > 0 then
if DoCompare(FData.List^[AIndex - 1], FData.List^[AIndex]) > 0 then begin
Result := ItemFind(FData.List^[AIndex], 0, AIndex - 1);
// no Dec(Result) here, as it is below
FData.Move(AIndex, Result);
exit; // optimization: the item cannot be unsorted from both sides
// simultaneously, so we can exit now
end;
if AIndex < FData.Count - 1 then
if DoCompare(FData.List^[AIndex], FData.List^[AIndex + 1]) > 0 then begin
Result := ItemFind(FData.List^[AIndex], AIndex + 1, FData.Count - 1);
Dec(Result);
FData.Move(AIndex, Result);
end;
end;
end;
function TCustomSortedChartSource.IsSorted: Boolean;
begin
case FSortBy of
@ -1737,11 +1884,7 @@ begin
FSorted := SaveSorted;
end;
if FSortBy = sbCustom then
FCompareProc := FOnCompare
else
FCompareProc := @DefaultCompare;
ExecSort(@DoCompare);
DoSort;
Notify;
end;

View File

@ -27,8 +27,6 @@ type
FDataPoints: TStrings;
FXCountMin: Cardinal;
FYCountMin: Cardinal;
procedure AddAt(
APos: Integer; const AX, AY: Double; const ALabel: String; AColor: TChartColor);
procedure ClearCaches;
function NewItem: PChartDataItem;
procedure SetDataPoints(const AValue: TStrings);
@ -57,12 +55,12 @@ type
procedure Clear;
procedure CopyFrom(ASource: TCustomChartSource);
procedure Delete(AIndex: Integer);
procedure SetColor(AIndex: Integer; AColor: TChartColor);
procedure SetText(AIndex: Integer; const AValue: String);
procedure SetXList(AIndex: Integer; const AXList: array of Double);
function SetColor(AIndex: Integer; AColor: TChartColor): Integer;
function SetText(AIndex: Integer; const AValue: String): Integer;
function SetXList(AIndex: Integer; const AXList: array of Double): Integer;
function SetXValue(AIndex: Integer; const AValue: Double): Integer;
procedure SetYList(AIndex: Integer; const AYList: array of Double);
procedure SetYValue(AIndex: Integer; const AValue: Double);
function SetYList(AIndex: Integer; const AYList: array of Double): Integer;
function SetYValue(AIndex: Integer; const AValue: Double): Integer;
published
property DataPoints: TStrings read FDataPoints write SetDataPoints;
property XCount;
@ -373,7 +371,7 @@ begin
item := FSource.NewItem;
try
Parse(S, item);
FSource.FData.Insert(Index, item);
FSource.ItemInsert(Index, item);
except
Dispose(item);
raise;
@ -489,30 +487,20 @@ end;
function TListChartSource.Add(
const AX, AY: Double; const ALabel: String = '';
const AColor: TChartColor = clTAColor): Integer;
begin
Result := FData.Count;
if IsSortedByXAsc then
// Keep data points ordered by X coordinate.
// Note that this leads to O(N^2) time except
// for the case of adding already ordered points.
// So, is the user wants to add many (>10000) points to a graph,
// he should pre-sort them to avoid performance penalty.
while (Result > 0) and (Item[Result - 1]^.X > AX) do
Dec(Result);
AddAt(Result, AX, AY, ALabel, AColor);
end;
procedure TListChartSource.AddAt(
APos: Integer; const AX, AY: Double; const ALabel: String; AColor: TChartColor);
var
pcd: PChartDataItem;
begin
pcd := NewItem;
pcd^.X := AX;
pcd^.Y := AY;
pcd^.Color := AColor;
pcd^.Text := ALabel;
FData.Insert(APos, pcd);
try
pcd^.X := AX;
pcd^.Y := AY;
pcd^.Color := AColor;
pcd^.Text := ALabel;
Result := ItemAdd(pcd);
except
Dispose(pcd);
raise;
end;
UpdateCachesAfterAdd(AX, AY);
end;
@ -530,9 +518,9 @@ begin
try
Result := Add(AX[0], AY[0], ALabel, AColor);
if Length(AX) > 1 then
SetXList(Result, AX[1..High(AX)]);
Result := SetXList(Result, AX[1..High(AX)]);
if Length(AY) > 1 then
SetYList(Result, AY[1..High(AY)]);
Result := SetYList(Result, AY[1..High(AY)]);
finally
Dec(FUpdateCount);
end;
@ -552,7 +540,7 @@ begin
try
Result := Add(AX, AY[0], ALabel, AColor);
if Length(AY) > 1 then
SetYList(Result, AY[1..High(AY)]);
Result := SetYList(Result, AY[1..High(AY)]);
finally
Dec(FUpdateCount);
end;
@ -591,6 +579,7 @@ end;
procedure TListChartSource.CopyFrom(ASource: TCustomChartSource);
var
i: Integer;
pcd: PChartDataItem;
begin
if ASource.XCount < FXCountMin then
raise EXCountError.CreateFmt(rsSourceCountError2, [ClassName, FXCountMin, 'x']);
@ -602,23 +591,23 @@ begin
Clear;
XCount := ASource.XCount;
YCount := ASource.YCount;
for i := 0 to ASource.Count - 1 do
with ASource[i]^ do begin
AddAt(FData.Count, X, Y, Text, Color);
SetXList(FData.Count - 1, XList);
SetYList(FData.Count - 1, YList);
end;
FData.Capacity := ASource.Count;
if IsSorted then begin
if ASource.IsSorted and
(SortBy = TCustomChartSourceAccess(ASource).SortBy) and
(SortDir = TCustomChartSourceAccess(ASource).SortDir) and
(SortIndex = TCustomChartSourceAccess(ASource).SortIndex) and
(SortBy <> sbCustom)
then
exit;
Sort;
pcd := nil;
try // optimization: don't execute try..except..end in a loop
for i := 0 to ASource.Count - 1 do begin
pcd := NewItem;
pcd^ := ASource[i]^;
FData.Add(pcd); // don't use ItemAdd() here
pcd := nil;
end;
except
if pcd <> nil then
Dispose(pcd);
raise;
end;
if IsSorted and (not HasSameSorting(ASource)) then Sort;
finally
EndUpdate;
end;
@ -680,12 +669,13 @@ begin
if YCount > 1 then SetLength(Result^.YList, YCount - 1);
end;
procedure TListChartSource.SetColor(AIndex: Integer; AColor: TChartColor);
function TListChartSource.SetColor(AIndex: Integer; AColor: TChartColor): Integer;
begin
with Item[AIndex]^ do begin
if Color = AColor then exit;
if Color = AColor then exit(AIndex);
Color := AColor;
end;
Result := ItemModified(AIndex);
Notify;
end;
@ -701,12 +691,13 @@ begin
end;
end;
procedure TListChartSource.SetText(AIndex: Integer; const AValue: String);
function TListChartSource.SetText(AIndex: Integer; const AValue: String): Integer;
begin
with Item[AIndex]^ do begin
if Text = AValue then exit;
if Text = AValue then exit(AIndex);
Text := AValue;
end;
Result := ItemModified(AIndex);
Notify;
end;
@ -725,8 +716,8 @@ begin
Notify;
end;
procedure TListChartSource.SetXList(
AIndex: Integer; const AXList: array of Double);
function TListChartSource.SetXList(
AIndex: Integer; const AXList: array of Double): Integer;
var
i: Integer;
begin
@ -734,6 +725,7 @@ begin
for i := 0 to Min(High(AXList), High(XList)) do
XList[i] := AXList[i];
FXListExtentIsValid := false;
Result := ItemModified(AIndex);
Notify;
end;
@ -757,26 +749,13 @@ var
end;
begin
if IsSortedByXAsc then
if IsNan(AValue) then
raise EChartError.CreateFmt('X = NaN in sorted source %s', [NameOrClassName(Self)]);
Result := AIndex;
with Item[AIndex]^ do begin
if IsEquivalent(X, AValue) then exit; // IsEquivalent() can compare also NaNs
if IsEquivalent(X, AValue) then exit(AIndex); // IsEquivalent() can compare also NaNs
oldX := X;
X := AValue;
end;
UpdateExtent;
if IsSortedByXAsc then begin
if AValue > oldX then
while (Result < Count - 1) and (Item[Result + 1]^.X < AValue) do
Inc(Result)
else
while (Result > 0) and (Item[Result - 1]^.X > AValue) do
Dec(Result);
if Result <> AIndex then
FData.Move(AIndex, Result);
end;
Result := ItemModified(AIndex);
Notify;
end;
@ -795,8 +774,8 @@ begin
Notify;
end;
procedure TListChartSource.SetYList(
AIndex: Integer; const AYList: array of Double);
function TListChartSource.SetYList(
AIndex: Integer; const AYList: array of Double): Integer;
var
i: Integer;
begin
@ -805,10 +784,11 @@ begin
YList[i] := AYList[i];
FCumulativeExtentIsValid := false;
FYListExtentIsValid := false;
Result := ItemModified(AIndex);
Notify;
end;
procedure TListChartSource.SetYValue(AIndex: Integer; const AValue: Double);
function TListChartSource.SetYValue(AIndex: Integer; const AValue: Double): Integer;
var
oldY: Double;
@ -829,13 +809,14 @@ var
begin
with Item[AIndex]^ do begin
if IsEquivalent(Y, AValue) then exit; // IsEquivalent() can compare also NaNs
if IsEquivalent(Y, AValue) then exit(AIndex); // IsEquivalent() can compare also NaNs
oldY := Y;
Y := AValue;
end;
if FValuesTotalIsValid then
FValuesTotal += NumberOr(AValue) - NumberOr(oldY);
UpdateExtent;
Result := ItemModified(AIndex);
Notify;
end;