TAChart: Use axis interval parameters in TDateTimeIntervalChartSource. Update axis demo.

git-svn-id: trunk@31871 -
This commit is contained in:
ask 2011-08-05 12:29:32 +00:00
parent e916132212
commit 21b9e581d4
4 changed files with 182 additions and 100 deletions

View File

@ -328,6 +328,10 @@ object Form1: TForm1
top = 216 top = 216
end end
object DateTimeIntervalChartSource1: TDateTimeIntervalChartSource object DateTimeIntervalChartSource1: TDateTimeIntervalChartSource
Params.Count = 10
Params.MaxLength = 150
Params.MinLength = 20
Params.Options = [aipUseCount, aipUseMaxLength, aipUseMinLength, aipUseNiceSteps]
left = 352 left = 352
top = 232 top = 232
end end

View File

@ -596,7 +596,8 @@ begin
Result.FAxisToGraph := @GetTransform.AxisToGraph; Result.FAxisToGraph := @GetTransform.AxisToGraph;
Result.FGraphToAxis := @GetTransform.GraphToAxis; Result.FGraphToAxis := @GetTransform.GraphToAxis;
Result.FGraphToImage := @FHelper.GraphToImage; Result.FGraphToImage := @FHelper.GraphToImage;
Result.FAxisIntervals := Intervals; Result.FIntervals := Intervals;
Result.FMinStep := 0;
end; end;
procedure TChartAxis.Measure( procedure TChartAxis.Measure(

View File

@ -94,14 +94,19 @@ type
TGraphToImageFunc = function (AX: Double): Integer of object; TGraphToImageFunc = function (AX: Double): Integer of object;
TValuesInRangeParams = record TValuesInRangeParams = object
FAxisIntervals: TChartAxisIntervalParams;
FAxisToGraph: TTransformFunc; FAxisToGraph: TTransformFunc;
FFormat: String; FFormat: String;
FGraphToAxis: TTransformFunc; FGraphToAxis: TTransformFunc;
FGraphToImage: TGraphToImageFunc; FGraphToImage: TGraphToImageFunc;
FIntervals: TChartAxisIntervalParams;
FMin, FMax: Double; FMin, FMax: Double;
FMinStep: Double;
FUseY: Boolean; FUseY: Boolean;
function CountToStep(ACount: Integer): Double; inline;
function IsAcceptableStep(AStep: Integer): Boolean; inline;
function ToImage(AX: Double): Integer; inline;
end; end;
{ TCustomChartSource } { TCustomChartSource }
@ -194,6 +199,28 @@ begin
AItem.YList[i] := 0; AItem.YList[i] := 0;
end; end;
{ TValuesInRangeParams }
function TValuesInRangeParams.CountToStep(ACount: Integer): Double;
begin
Result := Power(10, Floor(Log10((FMax - FMin) / ACount)));
end;
function TValuesInRangeParams.IsAcceptableStep(AStep: Integer): Boolean;
begin
with FIntervals do
Result := not (
(aipUseMinLength in Options) and (AStep < MinLength) or
(aipUseMaxLength in Options) and (AStep > MaxLength));
end;
function TValuesInRangeParams.ToImage(AX: Double): Integer;
begin
if not (aipGraphCoords in FIntervals.Options) then
AX := FAxisToGraph(AX);
Result := FGraphToImage(AX);
end;
{ TChartAxisIntervalParams } { TChartAxisIntervalParams }
procedure TChartAxisIntervalParams.Changed; procedure TChartAxisIntervalParams.Changed;

View File

@ -30,9 +30,10 @@ type
TIntervalChartSource = class(TCustomChartSource) TIntervalChartSource = class(TCustomChartSource)
strict private strict private
FParams: TChartAxisIntervalParams; FParams: TChartAxisIntervalParams;
procedure SetParams(AValue: TChartAxisIntervalParams);
strict protected
procedure CalculateIntervals( procedure CalculateIntervals(
AParams: TValuesInRangeParams; out ABestStart, ABestStep: Double); AParams: TValuesInRangeParams; out ABestStart, ABestStep: Double);
procedure SetParams(AValue: TChartAxisIntervalParams);
protected protected
function GetCount: Integer; override; function GetCount: Integer; override;
function GetItem(AIndex: Integer): PChartDataItem; override; function GetItem(AIndex: Integer): PChartDataItem; override;
@ -48,8 +49,8 @@ type
end; end;
TDateTimeStep = ( TDateTimeStep = (
dtsCentury, dtsDecade, dtsYear, dtsQuarter, dtsMonth, dtsWeek, dtsDay, dtsYear, dtsQuarter, dtsMonth, dtsWeek, dtsDay,
dtsHour, dtsTenMinutes, dtsMinute, dtsTenSeconds, dtsSecond, dtsMillisecond dtsHour, dtsMinute, dtsSecond, dtsMillisecond
); );
TDateTimeSteps = set of TDateTimeStep; TDateTimeSteps = set of TDateTimeStep;
@ -82,12 +83,32 @@ implementation
uses uses
DateUtils, Math, StrUtils, SysUtils; DateUtils, Math, StrUtils, SysUtils;
const
YEAR = 365.25;
DATE_STEP_INTERVALS: array [TDateTimeStep] of Double = (
YEAR, YEAR / 4, YEAR / 12, 7, 1,
OneHour, OneMinute, OneSecond, OneMillisecond
);
type type
TSourceIntervalParams = class(TChartAxisIntervalParams) TSourceIntervalParams = class(TChartAxisIntervalParams)
strict protected strict protected
procedure Changed; override; procedure Changed; override;
end; end;
TDateTimeIntervalsHelper = object
FBestStep: TDateTimeStep;
FBestStepCoeff: Double;
FOrigParams: TValuesInRangeParams;
FStep: TDateTimeStep;
FStepLen: Double;
function AxisToGraph(AX: Double): Double;
procedure CheckStep(AStepCoeff: Double);
function GraphToAxis(AX: Double): Double;
function NextValue(AValue: TDateTime): Double;
end;
procedure Register; procedure Register;
begin begin
RegisterComponents( RegisterComponents(
@ -96,6 +117,47 @@ begin
]); ]);
end; end;
function SafeRound(AValue: Double): Double; inline;
begin
Result := Int(AValue * 1e9) / 1e9;
end;
{ TDateTimeIntervalsHelper }
function TDateTimeIntervalsHelper.AxisToGraph(AX: Double): Double;
begin
Result := FOrigParams.FAxisToGraph(AX) * DATE_STEP_INTERVALS[FStep];
end;
procedure TDateTimeIntervalsHelper.CheckStep(AStepCoeff: Double);
begin
// Strict inequaltity is importatnt to avoid steps like "ten quarters".
if (1.0 <= AStepCoeff) and (AStepCoeff < FBestStepCoeff) then begin
FBestStepCoeff := AStepCoeff;
FBestStep := FStep;
FStepLen := DATE_STEP_INTERVALS[FBestStep] * FBestStepCoeff;
end;
end;
function TDateTimeIntervalsHelper.GraphToAxis(AX: Double): Double;
begin
Result := FOrigParams.FGraphToAxis(AX / DATE_STEP_INTERVALS[FStep]);
end;
function TDateTimeIntervalsHelper.NextValue(AValue: TDateTime): Double;
begin
case FBestStep of
dtsYear:
if FBestStepCoeff > 10 then
// DateTime arithmetics fails on large year numbers.
Result := AValue + FStepLen
else
Result := IncYear(AValue, Round(FBestStepCoeff));
dtsMonth: Result := IncMonth(AValue, Round(FBestStepCoeff));
otherwise Result := AValue + FStepLen;
end;
end;
{ TSourceIntervalParams } { TSourceIntervalParams }
procedure TSourceIntervalParams.Changed; procedure TSourceIntervalParams.Changed;
@ -111,20 +173,14 @@ end;
procedure TIntervalChartSource.CalculateIntervals( procedure TIntervalChartSource.CalculateIntervals(
AParams: TValuesInRangeParams; out ABestStart, ABestStep: Double); AParams: TValuesInRangeParams; out ABestStart, ABestStep: Double);
function A2I(AX: Double): Integer; inline;
begin
if not (aipGraphCoords in Params.Options) then
AX := AParams.FAxisToGraph(AX);
Result := AParams.FGraphToImage(AX);
end;
procedure CalcMinMaxCount(out AMinCount, AMaxCount: Integer); procedure CalcMinMaxCount(out AMinCount, AMaxCount: Integer);
var var
imageWidth, d: Integer; imageWidth, d: Integer;
begin begin
// If the axis transformation is non-linear, steps may not be equidistant. // If the axis transformation is non-linear, steps may not be equidistant.
// However, both minimax and maximin will be achieved on equal steps. // However, both minimax and maximin will be achieved on equal steps.
imageWidth := Abs(A2I(AParams.FMax) - A2I(AParams.FMin)); with AParams do
imageWidth := Abs(ToImage(FMax) - ToImage(FMin));
d := IfThen(aipUseMinLength in Params.Options, Max(Params.MinLength, 2), 2); d := IfThen(aipUseMinLength in Params.Options, Max(Params.MinLength, 2), 2);
AMaxCount := Max(imageWidth div d, 2); AMaxCount := Max(imageWidth div d, 2);
if aipUseMaxLength in Params.Options then if aipUseMaxLength in Params.Options then
@ -133,35 +189,24 @@ procedure TIntervalChartSource.CalculateIntervals(
AMinCount := 2; AMinCount := 2;
end; end;
function CountToStep(ACount: Integer): Double;
begin
Result := (AParams.FMax - AParams.FMin) / ACount;
Result := Power(10, Floor(Log10(Result)));
end;
procedure TryStep(AStep: Double; var ABestCount: Integer); procedure TryStep(AStep: Double; var ABestCount: Integer);
var var
m, start: Double; m, start: Double;
mi, prev, cnt: Integer; mi, prev, cnt: Integer;
begin begin
start := Floor(AParams.FMin / AStep) * AStep; start := Int(AParams.FMin / AStep) * AStep;
m := start; m := start;
prev := A2I(m); prev := AParams.ToImage(m);
cnt := 0; cnt := 0;
while m <= AParams.FMax do begin while m <= AParams.FMax do begin
mi := A2I(m + AStep); mi := AParams.ToImage(m + AStep);
prev := Abs(prev - mi); if not AParams.IsAcceptableStep(Abs(prev - mi)) then exit;
if
(aipUseMinLength in Params.Options) and (prev < Params.MinLength) or
(aipUseMaxLength in Params.Options) and (prev > Params.MaxLength)
then
exit;
m += AStep; m += AStep;
prev := mi; prev := mi;
cnt += 1; cnt += 1;
end; end;
if if
not (aipUseCount in Params.Options) or not (aipUseCount in Params.Options) or (ABestCount <= 0) or
(Abs(cnt - Params.Count) < Abs(ABestCount - Params.Count)) (Abs(cnt - Params.Count) < Abs(ABestCount - Params.Count))
then begin then begin
ABestStart := start - AStep; ABestStart := start - AStep;
@ -171,28 +216,28 @@ procedure TIntervalChartSource.CalculateIntervals(
end; end;
var var
minCount, maxCount, cnt: Integer; minCount, maxCount, bestCount: Integer;
s, sv: Double; s, sv: Double;
begin begin
CalcMinMaxCount(minCount, maxCount); CalcMinMaxCount(minCount, maxCount);
cnt := MaxInt; bestCount := 0;
if aipUseNiceSteps in Params.Options then begin if aipUseNiceSteps in Params.Options then begin
s := CountToStep(minCount) * 10; s := AParams.CountToStep(minCount) * 10;
while s >= CountToStep(maxCount) do begin while s >= Max(AParams.CountToStep(maxCount), AParams.FMinStep) do begin
for sv in Params.StepValues do for sv in Params.StepValues do
TryStep(s * sv, cnt); TryStep(s * sv, bestCount);
// We are not required to pick best count, so any one will do. // We are not required to pick best count, so any one will do.
if not (aipUseCount in Params.Options) and (cnt < MaxInt) then break; if not (aipUseCount in Params.Options) and (bestCount > 0) then break;
s *= 0.1; s *= 0.1;
end; end;
end; end;
if cnt < MaxInt then exit; if bestCount > 0 then exit;
// Either nice steps were not required, or we failed to find one. // Either nice steps were not required, or we failed to find one.
if aipUseCount in Params.Options then if aipUseCount in Params.Options then
cnt := EnsureRange(Params.Count, minCount, maxCount) bestCount := EnsureRange(Params.Count, minCount, maxCount)
else else
cnt := minCount; bestCount := minCount;
ABestStep := (AParams.FMax - AParams.FMin) / cnt; ABestStep := (AParams.FMax - AParams.FMin) / bestCount;
ABestStart := AParams.FMin - ABestStep; ABestStart := AParams.FMin - ABestStep;
end; end;
@ -241,6 +286,7 @@ var
i: Integer; i: Integer;
begin begin
if AParams.FMin >= AParams.FMax then exit; if AParams.FMin >= AParams.FMax then exit;
AParams.FIntervals := Params;
if aipGraphCoords in Params.Options then begin if aipGraphCoords in Params.Options then begin
AParams.FMin := AParams.FAxisToGraph(AParams.FMin); AParams.FMin := AParams.FAxisToGraph(AParams.FMin);
@ -279,100 +325,104 @@ end;
procedure TDateTimeIntervalChartSource.ValuesInRange( procedure TDateTimeIntervalChartSource.ValuesInRange(
AParams: TValuesInRangeParams; var AValues: TChartValueTextArray); AParams: TValuesInRangeParams; var AValues: TChartValueTextArray);
const
YEAR = 365.25;
STEP_INTERVALS: array [TDateTimeStep] of Double = (
100 * YEAR, 10 * YEAR, YEAR, YEAR / 4, YEAR / 12, 7, 1,
OneHour, 10 * OneMinute, OneMinute, 10 * OneSecond, OneSecond, OneMillisecond
);
MIN_STEPS = 4;
MAX_STEPS = 20;
var var
s: TDateTimeStep; helper: TDateTimeIntervalsHelper;
si, x, start: TDateTime;
prevSt: TSystemTime; prevSt: TSystemTime;
function FormatLabel: String; function FormatLabel(AValue: TDateTime): String;
var var
st: TSystemTime; st: TSystemTime;
begin begin
if DateTimeFormat <> '' then if DateTimeFormat <> '' then
exit(FormatDateTime(DateTimeFormat, x)); exit(FormatDateTime(DateTimeFormat, AValue));
DateTimeToSystemTime(x, st); DateTimeToSystemTime(AValue, st);
case s of case helper.FBestStep of
dtsCentury, dtsDecade, dtsYear: dtsYear:
Result := FormatDateTime('yyyy', x); Result := FormatDateTime('yyyy', AValue);
dtsQuarter: dtsQuarter:
Result := FormatDateTime('yyyy/', x) + IntToStr(Floor(x / si) mod 4 + 1); Result :=
IntToRoman(Floor(AValue / helper.FStepLen) mod 4 + 1) +
FormatDateTime('/yyyy', AValue);
dtsMonth: dtsMonth:
Result := FormatDateTime( Result := FormatDateTime(
IfThen(st.Year = prevSt.Year, 'mm', 'mm/yyyy'), x); IfThen(st.Year = prevSt.Year, 'mm', 'mm/yyyy'), AValue);
dtsWeek: dtsWeek:
Result := FormatDateTime('dd/mm', x); Result := FormatDateTime('dd/mm', AValue);
dtsDay: dtsDay:
Result := FormatDateTime( Result := FormatDateTime(
IfThen(st.Month = prevSt.Month, 'dd', 'dd/mm'), x); IfThen(st.Month = prevSt.Month, 'dd', 'dd/mm'), AValue);
dtsHour: dtsHour:
Result := FormatDateTime( Result := FormatDateTime(
IfThen(st.Day = prevSt.Day, 'hh:00', 'dd hh:00'), x); IfThen(st.Day = prevSt.Day, 'hh:00', 'dd hh:00'), AValue);
dtsTenMinutes, dtsMinute: dtsMinute:
Result := FormatDateTime( Result := FormatDateTime(
IfThen(st.Hour = prevSt.Hour, 'nn', 'hh:nn'), x); IfThen(st.Hour = prevSt.Hour, 'nn', 'hh:nn'), AValue);
dtsTenSeconds, dtsSecond: dtsSecond:
Result := FormatDateTime( Result := FormatDateTime(
IfThen(st.Minute = prevSt.Minute, 'ss', 'nn:ss'), x); IfThen(st.Minute = prevSt.Minute, 'ss', 'nn:ss'), AValue);
dtsMillisecond: dtsMillisecond:
Result := IntToStr(st.Millisecond) + 'ms'; Result :=
IfThen(st.Second = prevSt.Second, '', IntToStr(st.Second) + '.') +
IntToStr(st.Millisecond) + 'ms';
end; end;
prevSt := st; if InRange(AValue, helper.FOrigParams.FMin, helper.FOrigParams.FMax) then
prevSt := st;
end; end;
var procedure AddValue(AIndex: Integer; AValue: Double);
i, cnt: Integer; begin
r: Double; with AValues[AIndex] do begin
begin FValue := AValue;
with AParams do begin FText := Format(
r := FMax - FMin; AParams.FFormat, [AValue, 0.0, FormatLabel(AValue), 0.0, 0.0]);
if r / STEP_INTERVALS[dtsCentury] > MAX_STEPS then begin
FMin /= STEP_INTERVALS[dtsYear];
FMax /= STEP_INTERVALS[dtsYear];
inherited ValuesInRange(AParams, AValues);
exit;
end; end;
end; end;
s := Low(s);
while s < High(s) do begin const
si := STEP_INTERVALS[s]; MAX_COUNT = 1000; // Arbitraty limit to prevent OOM in case of a bug.
if (s in Steps) and (r / si > MIN_STEPS) then var
break; i, cnt: Integer;
Inc(s); x, start, stepLen: Double;
begin
if
(AParams.FMin >= AParams.FMax) or (aipGraphCoords in Params.options)
then
exit;
AParams.FIntervals := Params;
helper.FOrigParams := AParams;
AParams.FAxisToGraph := @helper.AxisToGraph;
AParams.FGraphToAxis := @helper.GraphToAxis;
AParams.FMinStep := 1.0;
helper.FBestStepCoeff := SafeInfinity;
for helper.FStep in Steps do begin
AParams.FMin := helper.FOrigParams.FMin / DATE_STEP_INTERVALS[helper.FStep];
AParams.FMax := helper.FOrigParams.FMax / DATE_STEP_INTERVALS[helper.FStep];
CalculateIntervals(AParams, start, stepLen);
helper.CheckStep(stepLen);
end; end;
start := Int(AParams.FMin / si - 1) * si;
x := start; if IsInfinite(helper.FBestStepCoeff) then exit;
with helper do
start := Int(FOrigParams.FMin / FStepLen - 1) * FStepLen;
cnt := 1; cnt := 1;
while x <= AParams.FMax do begin x := start;
while (x <= helper.FOrigParams.FMax) and (cnt < MAX_COUNT) do begin
cnt += 1; cnt += 1;
x += si; x := helper.NextValue(x);
end; end;
i := Length(AValues); i := Length(AValues);
SetLength(AValues, i + cnt); SetLength(AValues, i + cnt);
FillChar(prevSt, SizeOf(prevSt), $FF); FillChar(prevSt, SizeOf(prevSt), $FF);
x := start; x := start;
while x <= AParams.FMax do begin while (x <= helper.FOrigParams.FMax) and (i < cnt - 1) do begin
AValues[i].FValue := x; AddValue(i, x);
AValues[i].FText := Format(AParams.FFormat, [x, 0.0, FormatLabel, 0.0, 0.0]);
i += 1; i += 1;
case s of x := helper.NextValue(x);
dtsCentury: x := IncYear(x, 100);
dtsDecade: x := IncYear(x, 10);
dtsYear: x := IncYear(x);
dtsMonth: x := IncMonth(x);
otherwise x += si;
end;
end; end;
AValues[i].FValue := x; AddValue(i, x);
AValues[i].FText := Format(AParams.FFormat, [x, 0.0, FormatLabel, 0.0, 0.0]);
end; end;
end. end.