View Issue Details

IDProjectCategoryView StatusLast Update
0035344LazarusTAChartpublic2019-06-14 14:37
ReporterMarcin WiazowskiAssigned Towp 
PrioritynormalSeverityminorReproducibilityalways
Status closedResolutionreopened 
Product Version2.1 (SVN)Product Build60897 
Target Version2.2Fixed in Version 
Summary0035344: TAChart: extent limitation in zooming tools
DescriptionI have a strange feeling, that I missed something obvious - but I couldn't find a way to limit chart's extent, when using zooming tools.

To limit chart's extent, we can use Chart.Extent property - which works under normal circumstances. We can also use panning tool's LimitToExtent property.

But it seems, that no functionality allows to do same thing for zooming tools; zooming tools effectively only use Chart.ExtentSizeLimit property, which doesn't allow to limit chart extent's bounds.


So I'm attaching experiment.diff, which partially implements LimitToExtent functionality also in zooming tools. The idea is: if we have chart's full extent with X range [0..100], and the user selects zooming area X = [90..120], zooming tool should behave just like for selected area X = [90..100].

I'm not sure if the attached patch works properly with animated zoom; also some additional code is needed to handle RatioLimit functionality.
TagsNo tags attached.
Fixed in Revision60905, 61170, 61171
LazTarget2.2
WidgetsetWin32/Win64
Attached Files
  • experiment.diff (2,310 bytes)
    Index: components/tachart/tatools.pas
    ===================================================================
    --- components/tachart/tatools.pas	(revision 60897)
    +++ components/tachart/tatools.pas	(working copy)
    @@ -180,6 +180,9 @@
       TUserDefinedTool = class(TChartTool)
       end;
     
    +  TZoomDirection = (zdLeft, zdUp, zdRight, zdDown);
    +  TZoomDirectionSet = set of TZoomDirection;
    +
       { TBasicZoomTool }
     
       TBasicZoomTool = class(TChartTool)
    @@ -190,6 +193,7 @@
         FExtDst: TDoubleRect;
         FExtSrc: TDoubleRect;
         FFullZoom: Boolean;
    +    FLimitToExtent: TZoomDirectionSet;
         FTimer: TCustomTimer;
     
         procedure OnTimer(ASender: TObject);
    @@ -206,6 +210,8 @@
           read FAnimationInterval write FAnimationInterval default 0;
         property AnimationSteps: Cardinal
           read FAnimationSteps write FAnimationSteps default 0;
    +    property LimitToExtent: TZoomDirectionSet
    +      read FLimitToExtent write FLimitToExtent default [];
       end;
     
       TZoomRatioLimit = (zrlNone, zrlProportional, zrlFixedX, zrlFixedY);
    @@ -1126,12 +1132,32 @@
     end;
     
     procedure TBasicZoomTool.DoZoom(const ANewExtent: TDoubleRect; AFull: Boolean);
    +
    +  function ValidatedNewExtent: TDoubleRect;
    +  var
    +    fullExt: TDoubleRect;
    +  begin
    +    Result := ANewExtent;
    +    if LimitToExtent <> [] then begin
    +      fullExt := FChart.GetFullExtent;
    +      if (zdRight in LimitToExtent) and (Result.b.X > fullExt.b.X) then
    +        Result.b.X := fullExt.b.X;
    +      if (zdUp in LimitToExtent) and (Result.b.Y > fullExt.b.Y) then
    +        Result.b.Y := fullExt.b.Y;
    +      if (zdLeft in LimitToExtent) and (Result.a.X < fullExt.a.X) then
    +        Result.a.X := fullExt.a.X;
    +      if (zdDown in LimitToExtent) and (Result.a.Y < fullExt.a.Y) then
    +        Result.a.Y := fullExt.a.Y;
    +    end;
    +  end;
    +
    +
     begin
       if (AnimationInterval = 0) or (AnimationSteps = 0) then begin
         if AFull then
           FChart.ZoomFull
         else
    -      FChart.LogicalExtent := ANewExtent;
    +      FChart.LogicalExtent := ValidatedNewExtent;
         if IsActive then
           Deactivate;
         exit;
    @@ -1139,7 +1165,7 @@
       if not IsActive then
         Activate;
       FExtSrc := FChart.LogicalExtent;
    -  FExtDst := ANewExtent;
    +  FExtDst := ValidatedNewExtent;
       FFullZoom := AFull;
       FCurrentStep := 0;
       FTimer.Interval := AnimationInterval;
    
    experiment.diff (2,310 bytes)
  • Mod.png (7,788 bytes)
    Mod.png (7,788 bytes)
  • extent_limit_test.zip (3,598 bytes)
  • extent_limit_test_mod.zip (3,977 bytes)
  • final.diff (6,542 bytes)
    Index: components/tachart/tatools.pas
    ===================================================================
    --- components/tachart/tatools.pas	(revision 61066)
    +++ components/tachart/tatools.pas	(working copy)
    @@ -200,7 +200,7 @@
     
         procedure OnTimer(ASender: TObject);
       protected
    -    procedure DoZoom(const ANewExtent: TDoubleRect; AFull: Boolean);
    +    procedure DoZoom(ANewExtent: TDoubleRect; AFull: Boolean);
         function IsAnimating: Boolean; inline;
       public
         constructor Create(AOwner: TComponent); override;
    @@ -1157,33 +1157,125 @@
       inherited Destroy;
     end;
     
    -procedure TBasicZoomTool.DoZoom(const ANewExtent: TDoubleRect; AFull: Boolean);
    +procedure TBasicZoomTool.DoZoom(ANewExtent: TDoubleRect; AFull: Boolean);
     
    -  function ValidatedNewExtent: TDoubleRect;
    +  procedure ValidateNewSize(LimitLo, LimitHi: TZoomDirection;
    +    const PrevSize, NewSize, MaxSize, ImageMaxSize: Double; out Scale: Double;
    +    out AllowProportionalAdjustment: Boolean);
    +  begin
    +    // if new size is only a bit different than previous size, this may be due to
    +    // limited precision of floating-point calculations, so - if change in size
    +    // is smaller than half of the pixel - set Scale to 0, disable proportional
    +    // adjustments and exit; in this case, change in size will be reverted for
    +    // the current dimension, and adjusting the other dimension will be performed
    +    // independently
    +    if (NewSize > PrevSize * (1 - 0.5 / abs(ImageMaxSize))) and
    +       (NewSize < PrevSize * (1 + 0.5 / abs(ImageMaxSize))) then begin
    +      Scale := 0;
    +      AllowProportionalAdjustment := false;
    +      exit;
    +    end;
    +
    +    Scale := 1;
    +    AllowProportionalAdjustment := true;
    +
    +    // if new size is within the limit - allow change
    +    if NewSize <= MaxSize then exit;
    +
    +    // if size is not growing - allow change
    +    if NewSize <= PrevSize then exit;
    +
    +    // if there is no both-sides extent limitation - allow change
    +    if not (LimitLo in LimitToExtent) or not (LimitHi in LimitToExtent) then exit;
    +
    +    if PrevSize >= MaxSize then
    +      // if previous size already reaches or exceeds the limit - do NOT allow change
    +      Scale := 0
    +    else
    +      // if previous size is within the limit - allow change, but make the new size
    +      // smaller than requested
    +      Scale := (MaxSize - PrevSize) / (NewSize - PrevSize);
    +  end;
    +
    +  procedure AdjustNewSizeAndPosition(LimitLo, LimitHi: TZoomDirection;
    +    var NewSizeLo, NewSizeHi: Double; const MaxSizeLo, MaxSizeHi: Double);
       var
    -    fullExt: TDoubleRect;
    +    Diff: Double;
       begin
    -    Result := ANewExtent;
    -    if LimitToExtent <> [] then begin
    -      fullExt := FChart.GetFullExtent;
    -      if (zdRight in LimitToExtent) and (Result.b.X > fullExt.b.X) then
    -        Result.b.X := fullExt.b.X;
    -      if (zdUp in LimitToExtent) and (Result.b.Y > fullExt.b.Y) then
    -        Result.b.Y := fullExt.b.Y;
    -      if (zdLeft in LimitToExtent) and (Result.a.X < fullExt.a.X) then
    -        Result.a.X := fullExt.a.X;
    -      if (zdDown in LimitToExtent) and (Result.a.Y < fullExt.a.Y) then
    -        Result.a.Y := fullExt.a.Y;
    +    if (LimitLo in LimitToExtent) and (LimitHi in LimitToExtent) then begin
    +      Diff := NewSizeHi - NewSizeLo - (MaxSizeHi - MaxSizeLo);
    +      if Diff > 0 then begin
    +        NewSizeLo := MaxSizeLo - 0.5 * Diff;
    +        NewSizeHi := MaxSizeHi + 0.5 * Diff;
    +      end else
    +      if NewSizeLo < MaxSizeLo then begin
    +        NewSizeLo := MaxSizeLo;
    +        NewSizeHi := MaxSizeHi + Diff;
    +      end else
    +      if NewSizeHi > MaxSizeHi then begin
    +        NewSizeLo := MaxSizeLo - Diff;
    +        NewSizeHi := MaxSizeHi;
    +      end;
    +    end else
    +    if LimitLo in LimitToExtent then begin
    +      if NewSizeLo < MaxSizeLo then begin
    +        NewSizeHi := MaxSizeLo + (NewSizeHi - NewSizeLo);
    +        NewSizeLo := MaxSizeLo;
    +      end;
    +    end else
    +    if LimitHi in LimitToExtent then begin
    +      if NewSizeHi > MaxSizeHi then begin
    +        NewSizeLo := MaxSizeHi - (NewSizeHi - NewSizeLo);
    +        NewSizeHi := MaxSizeHi;
    +      end;
         end;
       end;
     
    +var
    +  FullExt: TDoubleRect;
    +  ScaleX, ScaleY: Double;
    +  AllowProportionalAdjustmentX, AllowProportionalAdjustmentY: Boolean;
    +begin
    +  if not AFull then
    +    // perform the actions below even when LimitToExtent is empty - this will
    +    // correct sub-pixel changes in viewport size (occuring due to limited
    +    // precision of floating-point calculations), which will result in a more
    +    // smooth visual behavior
    +    with ANewExtent do begin
    +      FullExt := FChart.GetFullExtent;
     
    -begin
    +      ValidateNewSize(zdLeft, zdRight, FChart.LogicalExtent.b.X - FChart.LogicalExtent.a.X,
    +        b.X - a.X, FullExt.b.X - FullExt.a.X,
    +        FChart.XGraphToImage(FullExt.b.X) - FChart.XGraphToImage(FullExt.a.X),
    +        ScaleX, AllowProportionalAdjustmentX);
    +      ValidateNewSize(zdDown, zdUp, FChart.LogicalExtent.b.Y - FChart.LogicalExtent.a.Y,
    +        b.Y - a.Y, FullExt.b.Y - FullExt.a.Y,
    +        FChart.YGraphToImage(FullExt.b.Y) - FChart.YGraphToImage(FullExt.a.Y),
    +        ScaleY, AllowProportionalAdjustmentY);
    +
    +      if AllowProportionalAdjustmentX and AllowProportionalAdjustmentY then
    +        if (Self is TBasicZoomStepTool) or // if has ZoomFactor and ZoomRatio -
    +                                           // zooming is always proportional
    +           ((Self is TZoomDragTool) and
    +            (TZoomDragTool(Self).RatioLimit = zrlProportional)) then begin
    +          ScaleX := Min(ScaleX, ScaleY);
    +          ScaleY := ScaleX;
    +        end;
    +
    +      a.X := WeightedAverage(FChart.LogicalExtent.a.X, a.X, ScaleX);
    +      b.X := WeightedAverage(FChart.LogicalExtent.b.X, b.X, ScaleX);
    +      a.Y := WeightedAverage(FChart.LogicalExtent.a.Y, a.Y, ScaleY);
    +      b.Y := WeightedAverage(FChart.LogicalExtent.b.Y, b.Y, ScaleY);
    +
    +      AdjustNewSizeAndPosition(zdLeft, zdRight, a.X, b.X, FullExt.a.X, FullExt.b.X);
    +      AdjustNewSizeAndPosition(zdDown, zdUp, a.Y, b.Y, FullExt.a.Y, FullExt.b.Y);
    +    end;
    +
       if (AnimationInterval = 0) or (AnimationSteps = 0) then begin
         if AFull then
           FChart.ZoomFull
         else
    -      FChart.LogicalExtent := ValidatedNewExtent;
    +      FChart.LogicalExtent := ANewExtent;
         if IsActive then
           Deactivate;
         exit;
    @@ -1191,7 +1283,7 @@
       if not IsActive then
         Activate;
       FExtSrc := FChart.LogicalExtent;
    -  FExtDst := ValidatedNewExtent;
    +  FExtDst := ANewExtent;
       FFullZoom := AFull;
       FCurrentStep := 0;
       FTimer.Interval := AnimationInterval;
    
    final.diff (6,542 bytes)
  • extent_limit_test_mod2.zip (4,149 bytes)
  • final_updated.diff (6,597 bytes)
    Index: components/tachart/tatools.pas
    ===================================================================
    --- components/tachart/tatools.pas	(revision 61066)
    +++ components/tachart/tatools.pas	(working copy)
    @@ -200,7 +200,7 @@
     
         procedure OnTimer(ASender: TObject);
       protected
    -    procedure DoZoom(const ANewExtent: TDoubleRect; AFull: Boolean);
    +    procedure DoZoom(ANewExtent: TDoubleRect; AFull: Boolean);
         function IsAnimating: Boolean; inline;
       public
         constructor Create(AOwner: TComponent); override;
    @@ -1157,33 +1157,126 @@
       inherited Destroy;
     end;
     
    -procedure TBasicZoomTool.DoZoom(const ANewExtent: TDoubleRect; AFull: Boolean);
    +procedure TBasicZoomTool.DoZoom(ANewExtent: TDoubleRect; AFull: Boolean);
     
    -  function ValidatedNewExtent: TDoubleRect;
    +  procedure ValidateNewSize(LimitLo, LimitHi: TZoomDirection;
    +    const PrevSize, NewSize, MaxSize, ImageMaxSize: Double; out Scale: Double;
    +    out AllowProportionalAdjustment: Boolean);
    +  begin
    +    // if new size is only a bit different than previous size, this may be due to
    +    // limited precision of floating-point calculations, so - if change in size
    +    // is smaller than half of the pixel - set Scale to 0, disable proportional
    +    // adjustments and exit; in this case, change in size will be reverted for
    +    // the current dimension, and adjusting the other dimension will be performed
    +    // independently
    +    if (NewSize > PrevSize * (1 - 0.5 / abs(ImageMaxSize))) and
    +       (NewSize < PrevSize * (1 + 0.5 / abs(ImageMaxSize))) then begin
    +      Scale := 0;
    +      AllowProportionalAdjustment := false;
    +      exit;
    +    end;
    +
    +    Scale := 1;
    +    AllowProportionalAdjustment := true;
    +
    +    // if new size is within the limit - allow change
    +    if NewSize <= MaxSize then exit;
    +
    +    // if size is not growing - allow change
    +    if NewSize <= PrevSize then exit;
    +
    +    // if there is no both-sides extent limitation - allow change
    +    if not (LimitLo in LimitToExtent) or not (LimitHi in LimitToExtent) then exit;
    +
    +    if PrevSize >= MaxSize then
    +      // if previous size already reaches or exceeds the limit - do NOT allow change
    +      Scale := 0
    +    else
    +      // if previous size is within the limit - allow change, but make the new size
    +      // smaller than requested
    +      Scale := (MaxSize - PrevSize) / (NewSize - PrevSize);
    +  end;
    +
    +  procedure AdjustNewSizeAndPosition(LimitLo, LimitHi: TZoomDirection;
    +    var NewSizeLo, NewSizeHi: Double; const MaxSizeLo, MaxSizeHi: Double);
       var
    -    fullExt: TDoubleRect;
    +    Diff: Double;
       begin
    -    Result := ANewExtent;
    -    if LimitToExtent <> [] then begin
    -      fullExt := FChart.GetFullExtent;
    -      if (zdRight in LimitToExtent) and (Result.b.X > fullExt.b.X) then
    -        Result.b.X := fullExt.b.X;
    -      if (zdUp in LimitToExtent) and (Result.b.Y > fullExt.b.Y) then
    -        Result.b.Y := fullExt.b.Y;
    -      if (zdLeft in LimitToExtent) and (Result.a.X < fullExt.a.X) then
    -        Result.a.X := fullExt.a.X;
    -      if (zdDown in LimitToExtent) and (Result.a.Y < fullExt.a.Y) then
    -        Result.a.Y := fullExt.a.Y;
    +    if (LimitLo in LimitToExtent) and (LimitHi in LimitToExtent) then begin
    +      Diff := NewSizeHi - NewSizeLo - (MaxSizeHi - MaxSizeLo);
    +      if Diff > 0 then begin
    +        NewSizeLo := MaxSizeLo - 0.5 * Diff;
    +        NewSizeHi := MaxSizeHi + 0.5 * Diff;
    +      end else
    +      if NewSizeLo < MaxSizeLo then begin
    +        NewSizeLo := MaxSizeLo;
    +        NewSizeHi := MaxSizeHi + Diff;
    +      end else
    +      if NewSizeHi > MaxSizeHi then begin
    +        NewSizeLo := MaxSizeLo - Diff;
    +        NewSizeHi := MaxSizeHi;
    +      end;
    +    end else
    +    if LimitLo in LimitToExtent then begin
    +      if NewSizeLo < MaxSizeLo then begin
    +        NewSizeHi := MaxSizeLo + (NewSizeHi - NewSizeLo);
    +        NewSizeLo := MaxSizeLo;
    +      end;
    +    end else
    +    if LimitHi in LimitToExtent then begin
    +      if NewSizeHi > MaxSizeHi then begin
    +        NewSizeLo := MaxSizeHi - (NewSizeHi - NewSizeLo);
    +        NewSizeHi := MaxSizeHi;
    +      end;
         end;
       end;
     
    +var
    +  FullExt: TDoubleRect;
    +  ScaleX, ScaleY: Double;
    +  AllowProportionalAdjustmentX, AllowProportionalAdjustmentY: Boolean;
    +begin
    +  if not AFull then
    +    // perform the actions below even when LimitToExtent is empty - this will
    +    // correct sub-pixel changes in viewport size (occuring due to limited
    +    // precision of floating-point calculations), which will result in a more
    +    // smooth visual behavior
    +    with ANewExtent do begin
    +      FullExt := FChart.GetFullExtent;
     
    -begin
    +      ValidateNewSize(zdLeft, zdRight, FChart.LogicalExtent.b.X - FChart.LogicalExtent.a.X,
    +        b.X - a.X, FullExt.b.X - FullExt.a.X,
    +        FChart.XGraphToImage(FullExt.b.X) - FChart.XGraphToImage(FullExt.a.X),
    +        ScaleX, AllowProportionalAdjustmentX);
    +      ValidateNewSize(zdDown, zdUp, FChart.LogicalExtent.b.Y - FChart.LogicalExtent.a.Y,
    +        b.Y - a.Y, FullExt.b.Y - FullExt.a.Y,
    +        FChart.YGraphToImage(FullExt.b.Y) - FChart.YGraphToImage(FullExt.a.Y),
    +        ScaleY, AllowProportionalAdjustmentY);
    +
    +      if AllowProportionalAdjustmentX and AllowProportionalAdjustmentY then
    +        if (Self is TBasicZoomStepTool) or // if has ZoomFactor and ZoomRatio -
    +                                           // zooming is always proportional
    +           ((Self is TZoomDragTool) and
    +            (TZoomDragTool(Self).AdjustSelection) and
    +            (TZoomDragTool(Self).RatioLimit = zrlProportional)) then begin
    +          ScaleX := Min(ScaleX, ScaleY);
    +          ScaleY := ScaleX;
    +        end;
    +
    +      a.X := WeightedAverage(FChart.LogicalExtent.a.X, a.X, ScaleX);
    +      b.X := WeightedAverage(FChart.LogicalExtent.b.X, b.X, ScaleX);
    +      a.Y := WeightedAverage(FChart.LogicalExtent.a.Y, a.Y, ScaleY);
    +      b.Y := WeightedAverage(FChart.LogicalExtent.b.Y, b.Y, ScaleY);
    +
    +      AdjustNewSizeAndPosition(zdLeft, zdRight, a.X, b.X, FullExt.a.X, FullExt.b.X);
    +      AdjustNewSizeAndPosition(zdDown, zdUp, a.Y, b.Y, FullExt.a.Y, FullExt.b.Y);
    +    end;
    +
       if (AnimationInterval = 0) or (AnimationSteps = 0) then begin
         if AFull then
           FChart.ZoomFull
         else
    -      FChart.LogicalExtent := ValidatedNewExtent;
    +      FChart.LogicalExtent := ANewExtent;
         if IsActive then
           Deactivate;
         exit;
    @@ -1191,7 +1284,7 @@
       if not IsActive then
         Activate;
       FExtSrc := FChart.LogicalExtent;
    -  FExtDst := ValidatedNewExtent;
    +  FExtDst := ANewExtent;
       FFullZoom := AFull;
       FCurrentStep := 0;
       FTimer.Interval := AnimationInterval;
    
    final_updated.diff (6,597 bytes)
  • final_updated2.diff (7,759 bytes)
    Index: components/tachart/tatools.pas
    ===================================================================
    --- components/tachart/tatools.pas	(revision 61162)
    +++ components/tachart/tatools.pas	(working copy)
    @@ -200,8 +200,9 @@
     
         procedure OnTimer(ASender: TObject);
       protected
    -    procedure DoZoom(const ANewExtent: TDoubleRect; AFull: Boolean);
    +    procedure DoZoom(ANewExtent: TDoubleRect; AFull: Boolean);
         function IsAnimating: Boolean; inline;
    +    function IsProportional: Boolean; virtual;
       public
         constructor Create(AOwner: TComponent); override;
         destructor Destroy; override;
    @@ -241,6 +242,8 @@
         procedure SetSelectionRect(AValue: TRect);
       strict protected
         procedure Cancel; override;
    +  protected
    +    function IsProportional: Boolean; override;
       public
         procedure MouseDown(APoint: TPoint); override;
         procedure MouseMove(APoint: TPoint); override;
    @@ -274,6 +277,8 @@
         function ZoomRatioIsStored: boolean;
       strict protected
         procedure DoZoomStep(const APoint: TPoint; const AFactor: TDoublePoint);
    +  protected
    +    function IsProportional: Boolean; override;
       public
         constructor Create(AOwner: TComponent); override;
       published
    @@ -1157,33 +1162,122 @@
       inherited Destroy;
     end;
     
    -procedure TBasicZoomTool.DoZoom(const ANewExtent: TDoubleRect; AFull: Boolean);
    +procedure TBasicZoomTool.DoZoom(ANewExtent: TDoubleRect; AFull: Boolean);
     
    -  function ValidatedNewExtent: TDoubleRect;
    +  procedure ValidateNewSize(LimitLo, LimitHi: TZoomDirection;
    +    const PrevSize, NewSize, MaxSize, ImageMaxSize: Double; out Scale: Double;
    +    out AllowProportionalAdjustment: Boolean);
    +  begin
    +    // if new size is only a bit different than previous size, this may be due to
    +    // limited precision of floating-point calculations, so - if change in size
    +    // is smaller than half of the pixel - set Scale to 0, disable proportional
    +    // adjustments and exit; in this case, change in size will be reverted for
    +    // the current dimension, and adjusting the other dimension will be performed
    +    // independently
    +    if (NewSize > PrevSize * (1 - 0.5 / abs(ImageMaxSize))) and
    +       (NewSize < PrevSize * (1 + 0.5 / abs(ImageMaxSize))) then begin
    +      Scale := 0;
    +      AllowProportionalAdjustment := false;
    +      exit;
    +    end;
    +
    +    Scale := 1;
    +    AllowProportionalAdjustment := true;
    +
    +    // if new size is within the limit - allow change
    +    if NewSize <= MaxSize then exit;
    +
    +    // if size is not growing - allow change
    +    if NewSize <= PrevSize then exit;
    +
    +    // if there is no both-sides extent limitation - allow change
    +    if not (LimitLo in LimitToExtent) or not (LimitHi in LimitToExtent) then exit;
    +
    +    if PrevSize >= MaxSize then
    +      // if previous size already reaches or exceeds the limit - do NOT allow change
    +      Scale := 0
    +    else
    +      // if previous size is within the limit - allow change, but make the new size
    +      // smaller than requested
    +      Scale := (MaxSize - PrevSize) / (NewSize - PrevSize);
    +  end;
    +
    +  procedure AdjustNewSizeAndPosition(LimitLo, LimitHi: TZoomDirection;
    +    var NewSizeLo, NewSizeHi: Double; const MaxSizeLo, MaxSizeHi: Double);
       var
    -    fullExt: TDoubleRect;
    +    Diff: Double;
       begin
    -    Result := ANewExtent;
    -    if LimitToExtent <> [] then begin
    -      fullExt := FChart.GetFullExtent;
    -      if (zdRight in LimitToExtent) and (Result.b.X > fullExt.b.X) then
    -        Result.b.X := fullExt.b.X;
    -      if (zdUp in LimitToExtent) and (Result.b.Y > fullExt.b.Y) then
    -        Result.b.Y := fullExt.b.Y;
    -      if (zdLeft in LimitToExtent) and (Result.a.X < fullExt.a.X) then
    -        Result.a.X := fullExt.a.X;
    -      if (zdDown in LimitToExtent) and (Result.a.Y < fullExt.a.Y) then
    -        Result.a.Y := fullExt.a.Y;
    +    if (LimitLo in LimitToExtent) and (LimitHi in LimitToExtent) then begin
    +      Diff := NewSizeHi - NewSizeLo - (MaxSizeHi - MaxSizeLo);
    +      if Diff > 0 then begin
    +        NewSizeLo := MaxSizeLo - 0.5 * Diff;
    +        NewSizeHi := MaxSizeHi + 0.5 * Diff;
    +      end else
    +      if NewSizeLo < MaxSizeLo then begin
    +        NewSizeLo := MaxSizeLo;
    +        NewSizeHi := MaxSizeHi + Diff;
    +      end else
    +      if NewSizeHi > MaxSizeHi then begin
    +        NewSizeLo := MaxSizeLo - Diff;
    +        NewSizeHi := MaxSizeHi;
    +      end;
    +    end else
    +    if LimitLo in LimitToExtent then begin
    +      if NewSizeLo < MaxSizeLo then begin
    +        NewSizeHi := MaxSizeLo + (NewSizeHi - NewSizeLo);
    +        NewSizeLo := MaxSizeLo;
    +      end;
    +    end else
    +    if LimitHi in LimitToExtent then begin
    +      if NewSizeHi > MaxSizeHi then begin
    +        NewSizeLo := MaxSizeHi - (NewSizeHi - NewSizeLo);
    +        NewSizeHi := MaxSizeHi;
    +      end;
         end;
       end;
     
    +var
    +  FullExt: TDoubleRect;
    +  ScaleX, ScaleY: Double;
    +  AllowProportionalAdjustmentX, AllowProportionalAdjustmentY: Boolean;
    +begin
    +  if not AFull then
    +    // perform the actions below even when LimitToExtent is empty - this will
    +    // correct sub-pixel changes in viewport size (occuring due to limited
    +    // precision of floating-point calculations), which will result in a more
    +    // smooth visual behavior
    +    with ANewExtent do begin
    +      FullExt := FChart.GetFullExtent;
     
    -begin
    +      ValidateNewSize(zdLeft, zdRight, FChart.LogicalExtent.b.X - FChart.LogicalExtent.a.X,
    +        b.X - a.X, FullExt.b.X - FullExt.a.X,
    +        FChart.XGraphToImage(FullExt.b.X) - FChart.XGraphToImage(FullExt.a.X),
    +        ScaleX, AllowProportionalAdjustmentX);
    +      ValidateNewSize(zdDown, zdUp, FChart.LogicalExtent.b.Y - FChart.LogicalExtent.a.Y,
    +        b.Y - a.Y, FullExt.b.Y - FullExt.a.Y,
    +        FChart.YGraphToImage(FullExt.b.Y) - FChart.YGraphToImage(FullExt.a.Y),
    +        ScaleY, AllowProportionalAdjustmentY);
    +
    +      if AllowProportionalAdjustmentX and AllowProportionalAdjustmentY and
    +         IsProportional then begin
    +          ScaleX := Min(ScaleX, ScaleY);
    +          ScaleY := ScaleX;
    +        end;
    +
    +      a.X := WeightedAverage(FChart.LogicalExtent.a.X, a.X, ScaleX);
    +      b.X := WeightedAverage(FChart.LogicalExtent.b.X, b.X, ScaleX);
    +      a.Y := WeightedAverage(FChart.LogicalExtent.a.Y, a.Y, ScaleY);
    +      b.Y := WeightedAverage(FChart.LogicalExtent.b.Y, b.Y, ScaleY);
    +
    +      AdjustNewSizeAndPosition(zdLeft, zdRight, a.X, b.X, FullExt.a.X, FullExt.b.X);
    +      AdjustNewSizeAndPosition(zdDown, zdUp, a.Y, b.Y, FullExt.a.Y, FullExt.b.Y);
    +    end;
    +
       if (AnimationInterval = 0) or (AnimationSteps = 0) then begin
         if AFull then
           FChart.ZoomFull
         else
    -      FChart.LogicalExtent := ValidatedNewExtent;
    +      FChart.LogicalExtent := ANewExtent;
         if IsActive then
           Deactivate;
         exit;
    @@ -1191,7 +1285,7 @@
       if not IsActive then
         Activate;
       FExtSrc := FChart.LogicalExtent;
    -  FExtDst := ValidatedNewExtent;
    +  FExtDst := ANewExtent;
       FFullZoom := AFull;
       FCurrentStep := 0;
       FTimer.Interval := AnimationInterval;
    @@ -1203,6 +1297,11 @@
       Result := FTimer.Enabled;
     end;
     
    +function TBasicZoomTool.IsProportional: Boolean;
    +begin
    +  Result := false;
    +end;
    +
     procedure TBasicZoomTool.OnTimer(ASender: TObject);
     var
       ext: TDoubleRect;
    @@ -1320,6 +1419,11 @@
       ADrawer.SetTransparency(0);
     end;
     
    +function TZoomDragTool.IsProportional: Boolean;
    +begin
    +  Result := AdjustSelection and (RatioLimit = zrlProportional);
    +end;
    +
     procedure TZoomDragTool.MouseDown(APoint: TPoint);
     begin
       if FChart.UsesBuiltinToolset and (not FChart.AllowZoom) then exit;
    @@ -1440,6 +1544,11 @@
       Handled;
     end;
     
    +function TBasicZoomStepTool.IsProportional: Boolean;
    +begin
    +  Result := true;
    +end;
    +
     function TBasicZoomStepTool.ZoomFactorIsStored: boolean;
     begin
       Result := FZoomFactor <> 1.0;
    
    final_updated2.diff (7,759 bytes)
  • unzoom.diff (1,674 bytes)
    Index: components/tachart/tatools.pas
    ===================================================================
    --- components/tachart/tatools.pas	(revision 61170)
    +++ components/tachart/tatools.pas	(working copy)
    @@ -1184,6 +1184,9 @@
         Scale := 1;
         AllowProportionalAdjustment := true;
     
    +    // if there is no both-sides extent limitation - allow change
    +    if not (LimitLo in LimitToExtent) or not (LimitHi in LimitToExtent) then exit;
    +
         // if new size is within the limit - allow change
         if NewSize <= MaxSize then exit;
     
    @@ -1190,15 +1193,16 @@
         // if size is not growing - allow change
         if NewSize <= PrevSize then exit;
     
    -    // if there is no both-sides extent limitation - allow change
    -    if not (LimitLo in LimitToExtent) or not (LimitHi in LimitToExtent) then exit;
    -
    -    if PrevSize >= MaxSize then
    -      // if previous size already reaches or exceeds the limit - do NOT allow change
    -      Scale := 0
    -    else
    -      // if previous size is within the limit - allow change, but make the new size
    -      // smaller than requested
    +    if PrevSize >= MaxSize then begin
    +      // if previous size already reaches or exceeds the limit - set Scale to 0,
    +      // disable proportional adjustments and exit; in this case, change in size
    +      // will be reverted for the current dimension, and adjusting the other
    +      // dimension will be performed independently
    +      Scale := 0;
    +      AllowProportionalAdjustment := false;
    +    end else
    +      // if previous size is within the limit - allow change, but make the new
    +      // size smaller than requested
           Scale := (MaxSize - PrevSize) / (NewSize - PrevSize);
       end;
     
    
    unzoom.diff (1,674 bytes)

Relationships

related to 0035634 feedbackwp TAChart: lack of cooperation between zooming tools and chart's ExtentSizeLimit 

Activities

Marcin Wiazowski

2019-04-08 18:15

reporter  

experiment.diff (2,310 bytes)
Index: components/tachart/tatools.pas
===================================================================
--- components/tachart/tatools.pas	(revision 60897)
+++ components/tachart/tatools.pas	(working copy)
@@ -180,6 +180,9 @@
   TUserDefinedTool = class(TChartTool)
   end;
 
+  TZoomDirection = (zdLeft, zdUp, zdRight, zdDown);
+  TZoomDirectionSet = set of TZoomDirection;
+
   { TBasicZoomTool }
 
   TBasicZoomTool = class(TChartTool)
@@ -190,6 +193,7 @@
     FExtDst: TDoubleRect;
     FExtSrc: TDoubleRect;
     FFullZoom: Boolean;
+    FLimitToExtent: TZoomDirectionSet;
     FTimer: TCustomTimer;
 
     procedure OnTimer(ASender: TObject);
@@ -206,6 +210,8 @@
       read FAnimationInterval write FAnimationInterval default 0;
     property AnimationSteps: Cardinal
       read FAnimationSteps write FAnimationSteps default 0;
+    property LimitToExtent: TZoomDirectionSet
+      read FLimitToExtent write FLimitToExtent default [];
   end;
 
   TZoomRatioLimit = (zrlNone, zrlProportional, zrlFixedX, zrlFixedY);
@@ -1126,12 +1132,32 @@
 end;
 
 procedure TBasicZoomTool.DoZoom(const ANewExtent: TDoubleRect; AFull: Boolean);
+
+  function ValidatedNewExtent: TDoubleRect;
+  var
+    fullExt: TDoubleRect;
+  begin
+    Result := ANewExtent;
+    if LimitToExtent <> [] then begin
+      fullExt := FChart.GetFullExtent;
+      if (zdRight in LimitToExtent) and (Result.b.X > fullExt.b.X) then
+        Result.b.X := fullExt.b.X;
+      if (zdUp in LimitToExtent) and (Result.b.Y > fullExt.b.Y) then
+        Result.b.Y := fullExt.b.Y;
+      if (zdLeft in LimitToExtent) and (Result.a.X < fullExt.a.X) then
+        Result.a.X := fullExt.a.X;
+      if (zdDown in LimitToExtent) and (Result.a.Y < fullExt.a.Y) then
+        Result.a.Y := fullExt.a.Y;
+    end;
+  end;
+
+
 begin
   if (AnimationInterval = 0) or (AnimationSteps = 0) then begin
     if AFull then
       FChart.ZoomFull
     else
-      FChart.LogicalExtent := ANewExtent;
+      FChart.LogicalExtent := ValidatedNewExtent;
     if IsActive then
       Deactivate;
     exit;
@@ -1139,7 +1165,7 @@
   if not IsActive then
     Activate;
   FExtSrc := FChart.LogicalExtent;
-  FExtDst := ANewExtent;
+  FExtDst := ValidatedNewExtent;
   FFullZoom := AFull;
   FCurrentStep := 0;
   FTimer.Interval := AnimationInterval;
experiment.diff (2,310 bytes)

wp

2019-04-09 00:54

developer   ~0115333

Looks good, tested zoomdrag, zoomclick, zoommousewheel. Animation ok, ZoomDrag.RatioLimit ok, Click/Mousewheel.ZoomFactor/ZoomRatio ok. See "extent-limit-test.zip".

I never got happy with the Chart.ExtentSizeLimit.

You should also look at TNavPanel, it should have a TPanDiectionSet like a PanTool. Currently it is possible to drag outside the full extent here.

Marcin Wiazowski

2019-04-09 23:01

reporter   ~0115362

I was trying hard, but I couldn't find any problem, so it seems, that the patch could be applied.

I haven't looked at TNavPanel yet.

wp

2019-04-10 10:43

developer   ~0115383

Applied, thanks. Resolving, if you want to fix TChartNavPanel put the patch into a separate report.

Marcin Wiazowski

2019-04-13 21:55

reporter  

Mod.png (7,788 bytes)
Mod.png (7,788 bytes)

Marcin Wiazowski

2019-04-13 22:05

reporter   ~0115479

The patch, that I provided, is still not ideal. I attached here your demo, modified a bit.

This time, data source contains data points within vertical range Y = 0 .. 100 (LogicalExtent), but also contains labels - which (along with chart margins) enlarge the final extent (CurrentExtent) to about Y = -2.2 .. 122.6 (this can be observed on the window's title bar).



So the question is: what should be done, if the user selects zooming range, that is entirely outside the logical extent - for example with Y = 105 .. 115?

Currently, after launching the application and selecting "Limit to extent" (with animation disabled), strange things occur - this is because the lower bound (105) is not touched, but upper bound (115) is trimmed to 100...



I think, that the best solution for all cases, when any logical extent's bound is crossed, is: shift the selected extent, to be fully contained within the existing logical extent; in the example that I gave: shift the selected extent down, from 105 .. 115 to to 90 .. 100.

This is not an ideal solution (there is probably no ideal solution in this case at all), but has some advantages:

1) Will work properly also in zrlProportional mode - proportions will be retained.

2) Quite natural effect will be achieved, when the user makes a selection around some label (mark). Generally, outside the logical extent, only labels (marks) can exist, so the only thing, that the user may want to zoom, is a label. For example, on the attached Mod.png, the user may make a selection around the "bbb" label, which will give Y range about 110 .. 120. This range will be then shifted to Y = 90 .. 100, then an additional margin for the label will be added by the TAChart's machinery, so - finally - the user will see vertical range about 90 .. 103 - which will contain some area, data point and also the "bbb" label.



Please let me know, if I should implement this - or maybe you have any suggestions?

wp

2019-04-13 23:54

developer   ~0115482

go ahead

Marcin Wiazowski

2019-04-14 22:49

reporter   ~0115508

There is a bug in both test programs attached here:


procedure TForm1.CheckBox3Change(Sender: TObject);
begin
  if Checkbox2.Checked then begin <==== should be Checkbox3
    ...

procedure TForm1.CheckBox4Change(Sender: TObject);
begin
  if Checkbox3.Checked then begin <==== should be Checkbox4
    ...


Maybe you could fix and reupload.

wp

2019-04-14 23:24

developer  

extent_limit_test.zip (3,598 bytes)

wp

2019-04-14 23:24

developer  

extent_limit_test_mod.zip (3,977 bytes)

wp

2019-04-14 23:25

developer   ~0115510

Done

Marcin Wiazowski

2019-04-27 21:02

reporter   ~0115856

This was like a nightmare, but I finally created a piece of code, that seems to work correctly in all cases. There are many comments in the code, so I'm placing only some general description here.


Zooming tools may work in two modes: proportional mode (tools having ZoomFactor/ZoomRatio properties, and also zoom drag tool with RatioLimit = zrlProportional) and non-proportional mode (zoom drag tool with RatioLimit <> zrlProportional). When area to be zoomed needs to be corrected, in proportional modes - of course - proportions must be still retained after the correction.


Some problem arisen with precision of floating point calculations: let's assume, that we have zoom mouse wheel tool, zooming only in X direction:

  ChartToolset1ZoomMouseWheelTool1.ZoomFactor := 1.1;
  ChartToolset1ZoomMouseWheelTool1.ZoomRatio := 0.909090;

When currently displayed range is X = 0 .. 1 and Y = 0 .. 100, after using the mouse wheel we may expect the new range to be, for example, X = -0.05 .. 1.05 and Y = 0 .. 100. But, due to floating point calculations, we may end up with Y = 0 .. 100.0000001; if full extent's Y range is Y = 0 .. 100, and both [pdUp, pdDown] are set in LimitToExtent, the whole change in Y range must be reverted - which, proportionally, reverts also X range to its previous state, so there is no zooming at all...

Solution: if effective change in the displayed range would be smaller than half of the chart's pixel, the change is just reverted before it will be able to alter the other dimension proportionally. Additional bonus: more smooth visual behavior.


Even when LimitToExtent is in use, the user may perform some over-zooming by assigning some large extent to LogicalExtent. In this case, zooming tools work only in one direction - they allow to zoom in, and don't allow to zoom out as long as the currently displayed range is larger than full extent's range. This case can be tested by using the one-more-time modified "extent_limit_test_mod2" test application, by using an "OverZooming" button.


After implementing all these changes, I assume the "LimitToExtent" functionality in zooming tools to be fully implemented.

final.diff (6,542 bytes)
Index: components/tachart/tatools.pas
===================================================================
--- components/tachart/tatools.pas	(revision 61066)
+++ components/tachart/tatools.pas	(working copy)
@@ -200,7 +200,7 @@
 
     procedure OnTimer(ASender: TObject);
   protected
-    procedure DoZoom(const ANewExtent: TDoubleRect; AFull: Boolean);
+    procedure DoZoom(ANewExtent: TDoubleRect; AFull: Boolean);
     function IsAnimating: Boolean; inline;
   public
     constructor Create(AOwner: TComponent); override;
@@ -1157,33 +1157,125 @@
   inherited Destroy;
 end;
 
-procedure TBasicZoomTool.DoZoom(const ANewExtent: TDoubleRect; AFull: Boolean);
+procedure TBasicZoomTool.DoZoom(ANewExtent: TDoubleRect; AFull: Boolean);
 
-  function ValidatedNewExtent: TDoubleRect;
+  procedure ValidateNewSize(LimitLo, LimitHi: TZoomDirection;
+    const PrevSize, NewSize, MaxSize, ImageMaxSize: Double; out Scale: Double;
+    out AllowProportionalAdjustment: Boolean);
+  begin
+    // if new size is only a bit different than previous size, this may be due to
+    // limited precision of floating-point calculations, so - if change in size
+    // is smaller than half of the pixel - set Scale to 0, disable proportional
+    // adjustments and exit; in this case, change in size will be reverted for
+    // the current dimension, and adjusting the other dimension will be performed
+    // independently
+    if (NewSize > PrevSize * (1 - 0.5 / abs(ImageMaxSize))) and
+       (NewSize < PrevSize * (1 + 0.5 / abs(ImageMaxSize))) then begin
+      Scale := 0;
+      AllowProportionalAdjustment := false;
+      exit;
+    end;
+
+    Scale := 1;
+    AllowProportionalAdjustment := true;
+
+    // if new size is within the limit - allow change
+    if NewSize <= MaxSize then exit;
+
+    // if size is not growing - allow change
+    if NewSize <= PrevSize then exit;
+
+    // if there is no both-sides extent limitation - allow change
+    if not (LimitLo in LimitToExtent) or not (LimitHi in LimitToExtent) then exit;
+
+    if PrevSize >= MaxSize then
+      // if previous size already reaches or exceeds the limit - do NOT allow change
+      Scale := 0
+    else
+      // if previous size is within the limit - allow change, but make the new size
+      // smaller than requested
+      Scale := (MaxSize - PrevSize) / (NewSize - PrevSize);
+  end;
+
+  procedure AdjustNewSizeAndPosition(LimitLo, LimitHi: TZoomDirection;
+    var NewSizeLo, NewSizeHi: Double; const MaxSizeLo, MaxSizeHi: Double);
   var
-    fullExt: TDoubleRect;
+    Diff: Double;
   begin
-    Result := ANewExtent;
-    if LimitToExtent <> [] then begin
-      fullExt := FChart.GetFullExtent;
-      if (zdRight in LimitToExtent) and (Result.b.X > fullExt.b.X) then
-        Result.b.X := fullExt.b.X;
-      if (zdUp in LimitToExtent) and (Result.b.Y > fullExt.b.Y) then
-        Result.b.Y := fullExt.b.Y;
-      if (zdLeft in LimitToExtent) and (Result.a.X < fullExt.a.X) then
-        Result.a.X := fullExt.a.X;
-      if (zdDown in LimitToExtent) and (Result.a.Y < fullExt.a.Y) then
-        Result.a.Y := fullExt.a.Y;
+    if (LimitLo in LimitToExtent) and (LimitHi in LimitToExtent) then begin
+      Diff := NewSizeHi - NewSizeLo - (MaxSizeHi - MaxSizeLo);
+      if Diff > 0 then begin
+        NewSizeLo := MaxSizeLo - 0.5 * Diff;
+        NewSizeHi := MaxSizeHi + 0.5 * Diff;
+      end else
+      if NewSizeLo < MaxSizeLo then begin
+        NewSizeLo := MaxSizeLo;
+        NewSizeHi := MaxSizeHi + Diff;
+      end else
+      if NewSizeHi > MaxSizeHi then begin
+        NewSizeLo := MaxSizeLo - Diff;
+        NewSizeHi := MaxSizeHi;
+      end;
+    end else
+    if LimitLo in LimitToExtent then begin
+      if NewSizeLo < MaxSizeLo then begin
+        NewSizeHi := MaxSizeLo + (NewSizeHi - NewSizeLo);
+        NewSizeLo := MaxSizeLo;
+      end;
+    end else
+    if LimitHi in LimitToExtent then begin
+      if NewSizeHi > MaxSizeHi then begin
+        NewSizeLo := MaxSizeHi - (NewSizeHi - NewSizeLo);
+        NewSizeHi := MaxSizeHi;
+      end;
     end;
   end;
 
+var
+  FullExt: TDoubleRect;
+  ScaleX, ScaleY: Double;
+  AllowProportionalAdjustmentX, AllowProportionalAdjustmentY: Boolean;
+begin
+  if not AFull then
+    // perform the actions below even when LimitToExtent is empty - this will
+    // correct sub-pixel changes in viewport size (occuring due to limited
+    // precision of floating-point calculations), which will result in a more
+    // smooth visual behavior
+    with ANewExtent do begin
+      FullExt := FChart.GetFullExtent;
 
-begin
+      ValidateNewSize(zdLeft, zdRight, FChart.LogicalExtent.b.X - FChart.LogicalExtent.a.X,
+        b.X - a.X, FullExt.b.X - FullExt.a.X,
+        FChart.XGraphToImage(FullExt.b.X) - FChart.XGraphToImage(FullExt.a.X),
+        ScaleX, AllowProportionalAdjustmentX);
+      ValidateNewSize(zdDown, zdUp, FChart.LogicalExtent.b.Y - FChart.LogicalExtent.a.Y,
+        b.Y - a.Y, FullExt.b.Y - FullExt.a.Y,
+        FChart.YGraphToImage(FullExt.b.Y) - FChart.YGraphToImage(FullExt.a.Y),
+        ScaleY, AllowProportionalAdjustmentY);
+
+      if AllowProportionalAdjustmentX and AllowProportionalAdjustmentY then
+        if (Self is TBasicZoomStepTool) or // if has ZoomFactor and ZoomRatio -
+                                           // zooming is always proportional
+           ((Self is TZoomDragTool) and
+            (TZoomDragTool(Self).RatioLimit = zrlProportional)) then begin
+          ScaleX := Min(ScaleX, ScaleY);
+          ScaleY := ScaleX;
+        end;
+
+      a.X := WeightedAverage(FChart.LogicalExtent.a.X, a.X, ScaleX);
+      b.X := WeightedAverage(FChart.LogicalExtent.b.X, b.X, ScaleX);
+      a.Y := WeightedAverage(FChart.LogicalExtent.a.Y, a.Y, ScaleY);
+      b.Y := WeightedAverage(FChart.LogicalExtent.b.Y, b.Y, ScaleY);
+
+      AdjustNewSizeAndPosition(zdLeft, zdRight, a.X, b.X, FullExt.a.X, FullExt.b.X);
+      AdjustNewSizeAndPosition(zdDown, zdUp, a.Y, b.Y, FullExt.a.Y, FullExt.b.Y);
+    end;
+
   if (AnimationInterval = 0) or (AnimationSteps = 0) then begin
     if AFull then
       FChart.ZoomFull
     else
-      FChart.LogicalExtent := ValidatedNewExtent;
+      FChart.LogicalExtent := ANewExtent;
     if IsActive then
       Deactivate;
     exit;
@@ -1191,7 +1283,7 @@
   if not IsActive then
     Activate;
   FExtSrc := FChart.LogicalExtent;
-  FExtDst := ValidatedNewExtent;
+  FExtDst := ANewExtent;
   FFullZoom := AFull;
   FCurrentStep := 0;
   FTimer.Interval := AnimationInterval;
final.diff (6,542 bytes)
extent_limit_test_mod2.zip (4,149 bytes)

Marcin Wiazowski

2019-04-28 01:59

reporter   ~0115864

I'm attaching final_updated.diff, where I fixed some issue.

final_updated.diff (6,597 bytes)
Index: components/tachart/tatools.pas
===================================================================
--- components/tachart/tatools.pas	(revision 61066)
+++ components/tachart/tatools.pas	(working copy)
@@ -200,7 +200,7 @@
 
     procedure OnTimer(ASender: TObject);
   protected
-    procedure DoZoom(const ANewExtent: TDoubleRect; AFull: Boolean);
+    procedure DoZoom(ANewExtent: TDoubleRect; AFull: Boolean);
     function IsAnimating: Boolean; inline;
   public
     constructor Create(AOwner: TComponent); override;
@@ -1157,33 +1157,126 @@
   inherited Destroy;
 end;
 
-procedure TBasicZoomTool.DoZoom(const ANewExtent: TDoubleRect; AFull: Boolean);
+procedure TBasicZoomTool.DoZoom(ANewExtent: TDoubleRect; AFull: Boolean);
 
-  function ValidatedNewExtent: TDoubleRect;
+  procedure ValidateNewSize(LimitLo, LimitHi: TZoomDirection;
+    const PrevSize, NewSize, MaxSize, ImageMaxSize: Double; out Scale: Double;
+    out AllowProportionalAdjustment: Boolean);
+  begin
+    // if new size is only a bit different than previous size, this may be due to
+    // limited precision of floating-point calculations, so - if change in size
+    // is smaller than half of the pixel - set Scale to 0, disable proportional
+    // adjustments and exit; in this case, change in size will be reverted for
+    // the current dimension, and adjusting the other dimension will be performed
+    // independently
+    if (NewSize > PrevSize * (1 - 0.5 / abs(ImageMaxSize))) and
+       (NewSize < PrevSize * (1 + 0.5 / abs(ImageMaxSize))) then begin
+      Scale := 0;
+      AllowProportionalAdjustment := false;
+      exit;
+    end;
+
+    Scale := 1;
+    AllowProportionalAdjustment := true;
+
+    // if new size is within the limit - allow change
+    if NewSize <= MaxSize then exit;
+
+    // if size is not growing - allow change
+    if NewSize <= PrevSize then exit;
+
+    // if there is no both-sides extent limitation - allow change
+    if not (LimitLo in LimitToExtent) or not (LimitHi in LimitToExtent) then exit;
+
+    if PrevSize >= MaxSize then
+      // if previous size already reaches or exceeds the limit - do NOT allow change
+      Scale := 0
+    else
+      // if previous size is within the limit - allow change, but make the new size
+      // smaller than requested
+      Scale := (MaxSize - PrevSize) / (NewSize - PrevSize);
+  end;
+
+  procedure AdjustNewSizeAndPosition(LimitLo, LimitHi: TZoomDirection;
+    var NewSizeLo, NewSizeHi: Double; const MaxSizeLo, MaxSizeHi: Double);
   var
-    fullExt: TDoubleRect;
+    Diff: Double;
   begin
-    Result := ANewExtent;
-    if LimitToExtent <> [] then begin
-      fullExt := FChart.GetFullExtent;
-      if (zdRight in LimitToExtent) and (Result.b.X > fullExt.b.X) then
-        Result.b.X := fullExt.b.X;
-      if (zdUp in LimitToExtent) and (Result.b.Y > fullExt.b.Y) then
-        Result.b.Y := fullExt.b.Y;
-      if (zdLeft in LimitToExtent) and (Result.a.X < fullExt.a.X) then
-        Result.a.X := fullExt.a.X;
-      if (zdDown in LimitToExtent) and (Result.a.Y < fullExt.a.Y) then
-        Result.a.Y := fullExt.a.Y;
+    if (LimitLo in LimitToExtent) and (LimitHi in LimitToExtent) then begin
+      Diff := NewSizeHi - NewSizeLo - (MaxSizeHi - MaxSizeLo);
+      if Diff > 0 then begin
+        NewSizeLo := MaxSizeLo - 0.5 * Diff;
+        NewSizeHi := MaxSizeHi + 0.5 * Diff;
+      end else
+      if NewSizeLo < MaxSizeLo then begin
+        NewSizeLo := MaxSizeLo;
+        NewSizeHi := MaxSizeHi + Diff;
+      end else
+      if NewSizeHi > MaxSizeHi then begin
+        NewSizeLo := MaxSizeLo - Diff;
+        NewSizeHi := MaxSizeHi;
+      end;
+    end else
+    if LimitLo in LimitToExtent then begin
+      if NewSizeLo < MaxSizeLo then begin
+        NewSizeHi := MaxSizeLo + (NewSizeHi - NewSizeLo);
+        NewSizeLo := MaxSizeLo;
+      end;
+    end else
+    if LimitHi in LimitToExtent then begin
+      if NewSizeHi > MaxSizeHi then begin
+        NewSizeLo := MaxSizeHi - (NewSizeHi - NewSizeLo);
+        NewSizeHi := MaxSizeHi;
+      end;
     end;
   end;
 
+var
+  FullExt: TDoubleRect;
+  ScaleX, ScaleY: Double;
+  AllowProportionalAdjustmentX, AllowProportionalAdjustmentY: Boolean;
+begin
+  if not AFull then
+    // perform the actions below even when LimitToExtent is empty - this will
+    // correct sub-pixel changes in viewport size (occuring due to limited
+    // precision of floating-point calculations), which will result in a more
+    // smooth visual behavior
+    with ANewExtent do begin
+      FullExt := FChart.GetFullExtent;
 
-begin
+      ValidateNewSize(zdLeft, zdRight, FChart.LogicalExtent.b.X - FChart.LogicalExtent.a.X,
+        b.X - a.X, FullExt.b.X - FullExt.a.X,
+        FChart.XGraphToImage(FullExt.b.X) - FChart.XGraphToImage(FullExt.a.X),
+        ScaleX, AllowProportionalAdjustmentX);
+      ValidateNewSize(zdDown, zdUp, FChart.LogicalExtent.b.Y - FChart.LogicalExtent.a.Y,
+        b.Y - a.Y, FullExt.b.Y - FullExt.a.Y,
+        FChart.YGraphToImage(FullExt.b.Y) - FChart.YGraphToImage(FullExt.a.Y),
+        ScaleY, AllowProportionalAdjustmentY);
+
+      if AllowProportionalAdjustmentX and AllowProportionalAdjustmentY then
+        if (Self is TBasicZoomStepTool) or // if has ZoomFactor and ZoomRatio -
+                                           // zooming is always proportional
+           ((Self is TZoomDragTool) and
+            (TZoomDragTool(Self).AdjustSelection) and
+            (TZoomDragTool(Self).RatioLimit = zrlProportional)) then begin
+          ScaleX := Min(ScaleX, ScaleY);
+          ScaleY := ScaleX;
+        end;
+
+      a.X := WeightedAverage(FChart.LogicalExtent.a.X, a.X, ScaleX);
+      b.X := WeightedAverage(FChart.LogicalExtent.b.X, b.X, ScaleX);
+      a.Y := WeightedAverage(FChart.LogicalExtent.a.Y, a.Y, ScaleY);
+      b.Y := WeightedAverage(FChart.LogicalExtent.b.Y, b.Y, ScaleY);
+
+      AdjustNewSizeAndPosition(zdLeft, zdRight, a.X, b.X, FullExt.a.X, FullExt.b.X);
+      AdjustNewSizeAndPosition(zdDown, zdUp, a.Y, b.Y, FullExt.a.Y, FullExt.b.Y);
+    end;
+
   if (AnimationInterval = 0) or (AnimationSteps = 0) then begin
     if AFull then
       FChart.ZoomFull
     else
-      FChart.LogicalExtent := ValidatedNewExtent;
+      FChart.LogicalExtent := ANewExtent;
     if IsActive then
       Deactivate;
     exit;
@@ -1191,7 +1284,7 @@
   if not IsActive then
     Activate;
   FExtSrc := FChart.LogicalExtent;
-  FExtDst := ValidatedNewExtent;
+  FExtDst := ANewExtent;
   FFullZoom := AFull;
   FCurrentStep := 0;
   FTimer.Interval := AnimationInterval;
final_updated.diff (6,597 bytes)

wp

2019-04-30 11:20

developer   ~0115917

Sorry for the nightmare, but my first impression is to reject the new patch. Your note in msg 0035344:0115479 suggests that you want to address the case that a label outside the data area is zoomed. Yes, this works better now. But when extent limitation is not active and I do the same there is still the case that the zoomed label is invisible. Also, when I zoom the label "ggg" which is completely inside the extent the label does not become visible (because it is too far from the data point in the zoomed case). So, as a user, I am more confused than before. Why does it work in one case, but not in the other?

I think the problem is deeper, in the fundamental design decision how labels are handled. And fixing this would mean a dramatic change which I doubt is worth the effort. Therefore, I tend to accept the present situation (and document that zooming around labels may not behave as expected); this would also affect issue 0035002.

Marcin Wiazowski

2019-05-01 03:48

reporter   ~0115937

Oh, please note, that the problem with zooming labels, located outside the data area, already exists, even without applying final_updated.diff.


The final_updated.diff is not related to the label zooming issue, and is also much more general: it addresses the (currently) incomplete implementation of LimitToExtent in all mouse drag, mouse click and mouse wheel zooming tools (while issue with label zooming exists only for mouse drag tool). What's more, the patch does its work also when marks are absent (not visible) or when auto margins are off.

In other words, final_updated.diff does not introduce the problem with label zooming - rather the already-existing label zooming issue manifests also here - with or without the final_updated.diff; I think, that I could probably create one or two examples of appearing that label zooming issue also in other code, without using LimitToExtent at all.


I agree, that something, that I described as an advantage, may be also considered to be a disadvantage...

However, thanks to my last experiences with zooming tools, I think that made some observations that might be helpful with label zooming issue; I'm not saying, that I'll show a complete solution, but I made some observations, that might be useful - I'll describe them in 0035002, after some additional testing (probably at the beginning of the next week). I think that the problem with label zooming is rather in inventing a right solution, than in implementing it.

wp

2019-05-03 19:29

reporter   ~0115983

Last edited: 2019-05-05 13:22

View 3 revisions

I did not say that the patch introduces a new issue with label zooming, I only said that label zooming is still confusing and maybe even more confusing as before.

Your text in note 0115479 made me believe that it covers only label zooming as well as some "overzooming" feature of which I am unclear whether it is useful or not.

Please provide sample projects so that I can see the "incomplete implementation of LimitToExtent" which you addressed by the final_updated patch.

I don't like that TBasicZoomTool checks the type of the current instance ("if (Self is TZoomDragTool)...") - this is not good object-oriented programming style in my eyes; the basic class should not know anything about the derived classes what has not been prepared by the basic class. Introduce appropriate virtual methods instead.

Marcin Wiazowski

2019-05-06 01:34

reporter   ~0116034

A problem with the current implementation can be observed by using the attached "extent_limit_test_mod2" application:
- full extent's Y range is 0 .. 100, and - after adding margins for marks - Y range is about 0 .. 120,
- now make both "Extent limit: Up" and "Extent limit: Down" checked,
- and, by using zoom drag tool, select area with X range 0 .. 1 and Y range 110 .. 120.

Result: zoomed Y range is about 0 .. 240, which is far above the full extent's height (even with added margin for marks) and is just wrong.

After patching, selected Y range is about 90 .. 102 - so the initial selection of height 120 - 110 = 10 is moved to be back in the allowed range, plus some margin for marks is then added.



I updated the patch code, as you advised (final_updated2.diff). Thank you for the suggestion - I also didn't like that code.

final_updated2.diff (7,759 bytes)
Index: components/tachart/tatools.pas
===================================================================
--- components/tachart/tatools.pas	(revision 61162)
+++ components/tachart/tatools.pas	(working copy)
@@ -200,8 +200,9 @@
 
     procedure OnTimer(ASender: TObject);
   protected
-    procedure DoZoom(const ANewExtent: TDoubleRect; AFull: Boolean);
+    procedure DoZoom(ANewExtent: TDoubleRect; AFull: Boolean);
     function IsAnimating: Boolean; inline;
+    function IsProportional: Boolean; virtual;
   public
     constructor Create(AOwner: TComponent); override;
     destructor Destroy; override;
@@ -241,6 +242,8 @@
     procedure SetSelectionRect(AValue: TRect);
   strict protected
     procedure Cancel; override;
+  protected
+    function IsProportional: Boolean; override;
   public
     procedure MouseDown(APoint: TPoint); override;
     procedure MouseMove(APoint: TPoint); override;
@@ -274,6 +277,8 @@
     function ZoomRatioIsStored: boolean;
   strict protected
     procedure DoZoomStep(const APoint: TPoint; const AFactor: TDoublePoint);
+  protected
+    function IsProportional: Boolean; override;
   public
     constructor Create(AOwner: TComponent); override;
   published
@@ -1157,33 +1162,122 @@
   inherited Destroy;
 end;
 
-procedure TBasicZoomTool.DoZoom(const ANewExtent: TDoubleRect; AFull: Boolean);
+procedure TBasicZoomTool.DoZoom(ANewExtent: TDoubleRect; AFull: Boolean);
 
-  function ValidatedNewExtent: TDoubleRect;
+  procedure ValidateNewSize(LimitLo, LimitHi: TZoomDirection;
+    const PrevSize, NewSize, MaxSize, ImageMaxSize: Double; out Scale: Double;
+    out AllowProportionalAdjustment: Boolean);
+  begin
+    // if new size is only a bit different than previous size, this may be due to
+    // limited precision of floating-point calculations, so - if change in size
+    // is smaller than half of the pixel - set Scale to 0, disable proportional
+    // adjustments and exit; in this case, change in size will be reverted for
+    // the current dimension, and adjusting the other dimension will be performed
+    // independently
+    if (NewSize > PrevSize * (1 - 0.5 / abs(ImageMaxSize))) and
+       (NewSize < PrevSize * (1 + 0.5 / abs(ImageMaxSize))) then begin
+      Scale := 0;
+      AllowProportionalAdjustment := false;
+      exit;
+    end;
+
+    Scale := 1;
+    AllowProportionalAdjustment := true;
+
+    // if new size is within the limit - allow change
+    if NewSize <= MaxSize then exit;
+
+    // if size is not growing - allow change
+    if NewSize <= PrevSize then exit;
+
+    // if there is no both-sides extent limitation - allow change
+    if not (LimitLo in LimitToExtent) or not (LimitHi in LimitToExtent) then exit;
+
+    if PrevSize >= MaxSize then
+      // if previous size already reaches or exceeds the limit - do NOT allow change
+      Scale := 0
+    else
+      // if previous size is within the limit - allow change, but make the new size
+      // smaller than requested
+      Scale := (MaxSize - PrevSize) / (NewSize - PrevSize);
+  end;
+
+  procedure AdjustNewSizeAndPosition(LimitLo, LimitHi: TZoomDirection;
+    var NewSizeLo, NewSizeHi: Double; const MaxSizeLo, MaxSizeHi: Double);
   var
-    fullExt: TDoubleRect;
+    Diff: Double;
   begin
-    Result := ANewExtent;
-    if LimitToExtent <> [] then begin
-      fullExt := FChart.GetFullExtent;
-      if (zdRight in LimitToExtent) and (Result.b.X > fullExt.b.X) then
-        Result.b.X := fullExt.b.X;
-      if (zdUp in LimitToExtent) and (Result.b.Y > fullExt.b.Y) then
-        Result.b.Y := fullExt.b.Y;
-      if (zdLeft in LimitToExtent) and (Result.a.X < fullExt.a.X) then
-        Result.a.X := fullExt.a.X;
-      if (zdDown in LimitToExtent) and (Result.a.Y < fullExt.a.Y) then
-        Result.a.Y := fullExt.a.Y;
+    if (LimitLo in LimitToExtent) and (LimitHi in LimitToExtent) then begin
+      Diff := NewSizeHi - NewSizeLo - (MaxSizeHi - MaxSizeLo);
+      if Diff > 0 then begin
+        NewSizeLo := MaxSizeLo - 0.5 * Diff;
+        NewSizeHi := MaxSizeHi + 0.5 * Diff;
+      end else
+      if NewSizeLo < MaxSizeLo then begin
+        NewSizeLo := MaxSizeLo;
+        NewSizeHi := MaxSizeHi + Diff;
+      end else
+      if NewSizeHi > MaxSizeHi then begin
+        NewSizeLo := MaxSizeLo - Diff;
+        NewSizeHi := MaxSizeHi;
+      end;
+    end else
+    if LimitLo in LimitToExtent then begin
+      if NewSizeLo < MaxSizeLo then begin
+        NewSizeHi := MaxSizeLo + (NewSizeHi - NewSizeLo);
+        NewSizeLo := MaxSizeLo;
+      end;
+    end else
+    if LimitHi in LimitToExtent then begin
+      if NewSizeHi > MaxSizeHi then begin
+        NewSizeLo := MaxSizeHi - (NewSizeHi - NewSizeLo);
+        NewSizeHi := MaxSizeHi;
+      end;
     end;
   end;
 
+var
+  FullExt: TDoubleRect;
+  ScaleX, ScaleY: Double;
+  AllowProportionalAdjustmentX, AllowProportionalAdjustmentY: Boolean;
+begin
+  if not AFull then
+    // perform the actions below even when LimitToExtent is empty - this will
+    // correct sub-pixel changes in viewport size (occuring due to limited
+    // precision of floating-point calculations), which will result in a more
+    // smooth visual behavior
+    with ANewExtent do begin
+      FullExt := FChart.GetFullExtent;
 
-begin
+      ValidateNewSize(zdLeft, zdRight, FChart.LogicalExtent.b.X - FChart.LogicalExtent.a.X,
+        b.X - a.X, FullExt.b.X - FullExt.a.X,
+        FChart.XGraphToImage(FullExt.b.X) - FChart.XGraphToImage(FullExt.a.X),
+        ScaleX, AllowProportionalAdjustmentX);
+      ValidateNewSize(zdDown, zdUp, FChart.LogicalExtent.b.Y - FChart.LogicalExtent.a.Y,
+        b.Y - a.Y, FullExt.b.Y - FullExt.a.Y,
+        FChart.YGraphToImage(FullExt.b.Y) - FChart.YGraphToImage(FullExt.a.Y),
+        ScaleY, AllowProportionalAdjustmentY);
+
+      if AllowProportionalAdjustmentX and AllowProportionalAdjustmentY and
+         IsProportional then begin
+          ScaleX := Min(ScaleX, ScaleY);
+          ScaleY := ScaleX;
+        end;
+
+      a.X := WeightedAverage(FChart.LogicalExtent.a.X, a.X, ScaleX);
+      b.X := WeightedAverage(FChart.LogicalExtent.b.X, b.X, ScaleX);
+      a.Y := WeightedAverage(FChart.LogicalExtent.a.Y, a.Y, ScaleY);
+      b.Y := WeightedAverage(FChart.LogicalExtent.b.Y, b.Y, ScaleY);
+
+      AdjustNewSizeAndPosition(zdLeft, zdRight, a.X, b.X, FullExt.a.X, FullExt.b.X);
+      AdjustNewSizeAndPosition(zdDown, zdUp, a.Y, b.Y, FullExt.a.Y, FullExt.b.Y);
+    end;
+
   if (AnimationInterval = 0) or (AnimationSteps = 0) then begin
     if AFull then
       FChart.ZoomFull
     else
-      FChart.LogicalExtent := ValidatedNewExtent;
+      FChart.LogicalExtent := ANewExtent;
     if IsActive then
       Deactivate;
     exit;
@@ -1191,7 +1285,7 @@
   if not IsActive then
     Activate;
   FExtSrc := FChart.LogicalExtent;
-  FExtDst := ValidatedNewExtent;
+  FExtDst := ANewExtent;
   FFullZoom := AFull;
   FCurrentStep := 0;
   FTimer.Interval := AnimationInterval;
@@ -1203,6 +1297,11 @@
   Result := FTimer.Enabled;
 end;
 
+function TBasicZoomTool.IsProportional: Boolean;
+begin
+  Result := false;
+end;
+
 procedure TBasicZoomTool.OnTimer(ASender: TObject);
 var
   ext: TDoubleRect;
@@ -1320,6 +1419,11 @@
   ADrawer.SetTransparency(0);
 end;
 
+function TZoomDragTool.IsProportional: Boolean;
+begin
+  Result := AdjustSelection and (RatioLimit = zrlProportional);
+end;
+
 procedure TZoomDragTool.MouseDown(APoint: TPoint);
 begin
   if FChart.UsesBuiltinToolset and (not FChart.AllowZoom) then exit;
@@ -1440,6 +1544,11 @@
   Handled;
 end;
 
+function TBasicZoomStepTool.IsProportional: Boolean;
+begin
+  Result := true;
+end;
+
 function TBasicZoomStepTool.ZoomFactorIsStored: boolean;
 begin
   Result := FZoomFactor <> 1.0;
final_updated2.diff (7,759 bytes)

wp

2019-05-07 12:13

reporter   ~0116061

Last edited: 2019-05-07 12:21

View 2 revisions

Yes, this demo is convincing, I applied the final_updated2.diff.

When playing with it I came to this situation which can appear to be a bit puzzling from a user's point of view: Run the "extent_limit_test_mod2" demo. Check all "Extent limit" boxes. Using the drag method draw an elongated box around some point, e.g X = 0.1.. 0.4, Y = 45..55. Now use the mousewheel to zoom out again. Because of the elongated shape of the previous zoom rect the horizontal limit is reached eventually before the vertical zoom limit, and zooming comes to a stop because the fixed zoom ratio parameters and frozen horizontal limits do not allow any further unzooming. But I think this may be confusing to the user because the chart is not fully unzoomed here. Probably the user just wants to unzoom further even if the zoom steps are different from the initial setup now. For example, when I switch the ZoomMouseWheel parameters to "y only" at this stage, further zooming is possible until the chart is fully unzoomed and all extent limits are reached. And this looks quite natural to me.

Therefore, I'd propose to re-adjust for the ZoomClick and MouseWheelZoom tools the ZoomFactor and ZoomRatio when the horizontal or vertical ExtentLimits prevent any further unzooming so that in the end the state with all ExtentLimits can be reached. Of course, the original ZoomFactor/ZoomRatio should be stored and re-used when a limitation is no longer active.

Marcin Wiazowski

2019-05-07 14:44

reporter   ~0116063

Last edited: 2019-05-07 14:44

View 2 revisions

Useful observation.

The attached unzoom.diff solves the problem. Solution is quite simple - if the current size (in some direction) is equal to the maximum allowed size, or even larger (the "OverZooming" case), and is going to become even larger - reject any changes for that dimension, and allow the other dimension to be handled independently (in short: just break the proportionality rule).

The attached patch makes also some small optimization, by the way.



unzoom.diff (1,674 bytes)
Index: components/tachart/tatools.pas
===================================================================
--- components/tachart/tatools.pas	(revision 61170)
+++ components/tachart/tatools.pas	(working copy)
@@ -1184,6 +1184,9 @@
     Scale := 1;
     AllowProportionalAdjustment := true;
 
+    // if there is no both-sides extent limitation - allow change
+    if not (LimitLo in LimitToExtent) or not (LimitHi in LimitToExtent) then exit;
+
     // if new size is within the limit - allow change
     if NewSize <= MaxSize then exit;
 
@@ -1190,15 +1193,16 @@
     // if size is not growing - allow change
     if NewSize <= PrevSize then exit;
 
-    // if there is no both-sides extent limitation - allow change
-    if not (LimitLo in LimitToExtent) or not (LimitHi in LimitToExtent) then exit;
-
-    if PrevSize >= MaxSize then
-      // if previous size already reaches or exceeds the limit - do NOT allow change
-      Scale := 0
-    else
-      // if previous size is within the limit - allow change, but make the new size
-      // smaller than requested
+    if PrevSize >= MaxSize then begin
+      // if previous size already reaches or exceeds the limit - set Scale to 0,
+      // disable proportional adjustments and exit; in this case, change in size
+      // will be reverted for the current dimension, and adjusting the other
+      // dimension will be performed independently
+      Scale := 0;
+      AllowProportionalAdjustment := false;
+    end else
+      // if previous size is within the limit - allow change, but make the new
+      // size smaller than requested
       Scale := (MaxSize - PrevSize) / (NewSize - PrevSize);
   end;
 
unzoom.diff (1,674 bytes)

wp

2019-05-07 16:03

reporter   ~0116067

Perfect, thank you.

Marcin Wiazowski

2019-05-07 21:44

reporter   ~0116070

Thanks!

Issue History

Date Modified Username Field Change
2019-04-08 18:15 Marcin Wiazowski New Issue
2019-04-08 18:15 Marcin Wiazowski File Added: experiment.diff
2019-04-08 22:08 wp Assigned To => wp
2019-04-08 22:08 wp Status new => assigned
2019-04-09 00:54 wp Note Added: 0115333
2019-04-09 00:55 wp File Added: extent-limit-test.zip
2019-04-09 23:01 Marcin Wiazowski Note Added: 0115362
2019-04-10 10:43 wp Fixed in Revision => 60905
2019-04-10 10:43 wp LazTarget => 2.2
2019-04-10 10:43 wp Note Added: 0115383
2019-04-10 10:43 wp Status assigned => resolved
2019-04-10 10:43 wp Resolution open => fixed
2019-04-10 10:43 wp Target Version => 2.2
2019-04-13 21:55 Marcin Wiazowski File Added: extent_limit_test_mod.zip
2019-04-13 21:55 Marcin Wiazowski File Added: Mod.png
2019-04-13 22:05 Marcin Wiazowski Note Added: 0115479
2019-04-13 22:05 Marcin Wiazowski Status resolved => assigned
2019-04-13 22:05 Marcin Wiazowski Resolution fixed => reopened
2019-04-13 23:54 wp Note Added: 0115482
2019-04-14 22:49 Marcin Wiazowski Note Added: 0115508
2019-04-14 23:23 wp File Deleted: extent-limit-test.zip
2019-04-14 23:23 wp File Deleted: extent_limit_test_mod.zip
2019-04-14 23:24 wp File Added: extent_limit_test.zip
2019-04-14 23:24 wp File Added: extent_limit_test_mod.zip
2019-04-14 23:25 wp Note Added: 0115510
2019-04-27 21:02 Marcin Wiazowski File Added: final.diff
2019-04-27 21:02 Marcin Wiazowski File Added: extent_limit_test_mod2.zip
2019-04-27 21:02 Marcin Wiazowski Note Added: 0115856
2019-04-28 01:59 Marcin Wiazowski File Added: final_updated.diff
2019-04-28 01:59 Marcin Wiazowski Note Added: 0115864
2019-04-30 11:20 wp Note Added: 0115917
2019-05-01 03:48 Marcin Wiazowski Note Added: 0115937
2019-05-03 19:29 wp Note Added: 0115983
2019-05-03 19:33 wp Note Edited: 0115983 View Revisions
2019-05-05 13:22 wp Note Edited: 0115983 View Revisions
2019-05-06 01:34 Marcin Wiazowski File Added: final_updated2.diff
2019-05-06 01:34 Marcin Wiazowski Note Added: 0116034
2019-05-07 12:13 wp Note Added: 0116061
2019-05-07 12:21 wp Note Edited: 0116061 View Revisions
2019-05-07 14:44 Marcin Wiazowski File Added: unzoom.diff
2019-05-07 14:44 Marcin Wiazowski Note Added: 0116063
2019-05-07 14:44 Marcin Wiazowski Note Edited: 0116063 View Revisions
2019-05-07 16:03 wp Status assigned => resolved
2019-05-07 16:03 wp Fixed in Revision 60905 => 60905, 61170, 61171
2019-05-07 16:03 wp Widgetset Win32/Win64 => Win32/Win64
2019-05-07 16:03 wp Note Added: 0116067
2019-05-07 21:44 Marcin Wiazowski Status resolved => closed
2019-05-07 21:44 Marcin Wiazowski Note Added: 0116070
2019-06-14 14:37 wp Relationship added related to 0035634